Feature: Web screen wall for multi-device live monitoring
Add a third view to the web UI, the screen wall, which shows several devices at once in one tab with 1x1 / 2x2 / 3x3 / 4x4 / 5x5 layouts and a device sidebar on the left. Tiles are monitoring-only; clicking one enlarges that device into the existing single-screen page for control, and Back returns to the wall. The same device can be added several times: all tiles of one device share a single decoder and a single stream. The wall decodes at most 9 streams on desktop and 4 on touch devices. Tiles beyond the budget keep receiving frames but have their decoder closed and show the last frame plus a Paused badge; they are promoted automatically when a live tile goes away. Subscriptions are reference counted on the client (streams map with owner sets), so a stream is only connected on the 0 -> 1 transition and disconnected on 1 -> 0, and the wall and the single-screen page can share one device without tearing the stream down. Server side, WebClient now holds a set of watched devices instead of a single id. A new multiWatch command flag selects set semantics, while clients without it keep the old replace-all behaviour, so an old frontend against the new server is unchanged. connect and disconnect responses echo id and req so the frontend can route them, input commands carry an id and are resolved against the watch set (dropped when the set has several devices and no id), and the per-client watch set is capped at 32. For fast recovery after a pause or when joining late, the server caches each device's raw IDR payload and replays it on connect and on a new keyframe command, then asks the client to force a new IDR. The client gets COMMAND_SCREEN_KEYFRAME_REQ = 169 and an atomic request flag that is only cleared after an IDR actually came out of the encoder, so a failed capture or EAGAIN does not lose the request. The frontend retries the keyframe request up to three times; an old client that ignores the command simply recovers on the next natural GOP. Also fix the keyframe cache double wrapping: BroadcastH264Frame used to cache the full packet header and HandleConnect wrapped it again. The cache now stores data + 9, len - 9 and every sender wraps it through BuildFramePacket, and the cache is cleared on cold start and on resolution change. StopRemoteDesktop is always called outside m_ClientsMutex, with the ids collected under the lock first. Co-Authored-By: deepseek-flash
This commit is contained in:
1 parent
04f4eb9947
commit
8eab26c1f6
6 files changed
+1790
-180
No files matched your search
+310
-132
@@ -45,6 +45,27 @@ static void ReplaceAll(std::string& s, const std::string& from, const std::strin
|
||||
}
|
||||
}
|
||||
|
||||
// 设备 ID 在 JSON 里以字符串传输(避免 64 位精度丢失),但兼容数字写法。
|
||||
static uint64_t ParseDeviceIdValue(const Json::Value& v) {
|
||||
if (v.isString()) return strtoull(v.asString().c_str(), nullptr, 10);
|
||||
if (v.isUInt64()) return v.asUInt64();
|
||||
if (v.isInt64()) return (uint64_t)v.asInt64();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t ParseDeviceIdField(const Json::Value& root, const char* key) {
|
||||
return ParseDeviceIdValue(root[key]);
|
||||
}
|
||||
|
||||
// req 是前端生成的关联号;字符串/数字都接受,原样回显。
|
||||
static std::string ParseReqField(const Json::Value& root) {
|
||||
const Json::Value& v = root["req"];
|
||||
if (v.isString()) return v.asString();
|
||||
if (v.isUInt64()) return std::to_string(v.asUInt64());
|
||||
if (v.isInt64()) return std::to_string(v.asInt64());
|
||||
return "";
|
||||
}
|
||||
|
||||
// Challenge-response nonce storage (prevents replay attacks)
|
||||
static std::map<void*, std::string> s_ClientNonces;
|
||||
static std::mutex s_NonceMutex;
|
||||
@@ -337,6 +358,10 @@ void CWebService::ServerThread(int port) {
|
||||
Json::Value challenge;
|
||||
challenge["cmd"] = "challenge";
|
||||
challenge["nonce"] = nonce;
|
||||
// 能力协商:新前端据此启用屏幕墙(多设备订阅)与 keyframe 请求。
|
||||
// 缺少这些字段的服务端(Go / 旧 C++ 二进制)会让新前端自动降级。
|
||||
challenge["caps"]["multiWatch"] = true;
|
||||
challenge["caps"]["keyframeRequest"] = true;
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, challenge));
|
||||
@@ -379,14 +404,13 @@ void CWebService::ServerThread(int port) {
|
||||
} else if (cmd == "get_devices") {
|
||||
HandleGetDevices(ws_ptr, token);
|
||||
} else if (cmd == "connect") {
|
||||
// Support both string and number ID formats
|
||||
std::string id_str = root.get("id", "").asString();
|
||||
uint64_t device_id = id_str.empty() ? 0 : strtoull(id_str.c_str(), nullptr, 10);
|
||||
HandleConnect(ws_ptr, token, device_id);
|
||||
HandleConnect(ws_ptr, token, ParseDeviceIdField(root, "id"),
|
||||
ParseReqField(root), root.get("multiWatch", false).asBool());
|
||||
} else if (cmd == "disconnect") {
|
||||
std::string disc_id_str = root.get("id", "").asString();
|
||||
uint64_t disc_device_id = disc_id_str.empty() ? 0 : strtoull(disc_id_str.c_str(), nullptr, 10);
|
||||
HandleDisconnect(ws_ptr, token, disc_device_id);
|
||||
HandleDisconnect(ws_ptr, token, ParseDeviceIdField(root, "id"),
|
||||
ParseReqField(root), root.get("multiWatch", false).asBool());
|
||||
} else if (cmd == "keyframe") {
|
||||
HandleKeyframe(ws_ptr, token, ParseDeviceIdField(root, "id"), ParseReqField(root));
|
||||
} else if (cmd == "ping") {
|
||||
HandlePing(ws_ptr, token);
|
||||
} else if (cmd == "mouse") {
|
||||
@@ -396,11 +420,11 @@ void CWebService::ServerThread(int port) {
|
||||
} else if (cmd == "clipboard_set") {
|
||||
HandleClipboardSet(ws_ptr, msg);
|
||||
} else if (cmd == "clipboard_get") {
|
||||
HandleClipboardGet(ws_ptr, token);
|
||||
HandleClipboardGet(ws_ptr, token, ParseDeviceIdField(root, "id"));
|
||||
} else if (cmd == "rdp_reset") {
|
||||
HandleRdpReset(ws_ptr, token);
|
||||
HandleRdpReset(ws_ptr, token, ParseDeviceIdField(root, "id"));
|
||||
} else if (cmd == "audio_toggle") {
|
||||
HandleAudioToggle(ws_ptr, token);
|
||||
HandleAudioToggle(ws_ptr, token, ParseDeviceIdField(root, "id"));
|
||||
} else if (cmd == "get_salt") {
|
||||
HandleGetSalt(ws_ptr, msg);
|
||||
} else if (cmd == "create_user") {
|
||||
@@ -464,7 +488,7 @@ void CWebService::HeartbeatThread() {
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (const auto& [ws_ptr, client] : m_Clients) {
|
||||
// Only check clients that are watching a device (streaming video)
|
||||
if (client.watch_device_id > 0 && client.last_activity > 0) {
|
||||
if (!client.watch_device_ids.empty() && client.last_activity > 0) {
|
||||
uint64_t elapsed = now - client.last_activity;
|
||||
if (elapsed > HEARTBEAT_TIMEOUT_SEC) {
|
||||
timedOutClients.push_back(ws_ptr);
|
||||
@@ -625,22 +649,36 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
|
||||
SendText(ws_ptr, BuildDeviceListJson(username));
|
||||
}
|
||||
|
||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req, bool multi_watch) {
|
||||
// 所有响应都带 id/req:屏幕墙有多个并发 connect,前端必须能按设备/请求号路由。
|
||||
auto send_error = [&](const std::string& err) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "connect_result";
|
||||
res["ok"] = false;
|
||||
res["msg"] = err;
|
||||
res["id"] = std::to_string(device_id);
|
||||
if (!req.empty()) res["req"] = req;
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
};
|
||||
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Invalid token"));
|
||||
send_error("Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check device exists and is online
|
||||
if (!m_pParentDlg) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Service not ready"));
|
||||
send_error("Service not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||
if (!ctx) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Device not online"));
|
||||
send_error("Device not online");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -665,7 +703,7 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
}
|
||||
}
|
||||
if (!hasAccess) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
|
||||
send_error("Permission denied");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -675,30 +713,76 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
// remote_open, cleared on session teardown), so AI input cannot be injected into a
|
||||
// picture a human is watching/controlling.
|
||||
if (IsMcpTriggered(device_id)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Device busy: remote control session active"));
|
||||
send_error("Device busy: remote control session active");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check max clients per device
|
||||
int current_count = GetWebClientCount(device_id);
|
||||
if (current_count >= m_nMaxClientsPerDevice) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Too many viewers"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update client state
|
||||
// 该 ws 是否已经订阅这台设备(屏幕墙点击 tile 放大时会重复 connect 以拿
|
||||
// 分辨率/关键帧)。重复 connect 必须跳过每设备 viewer 上限检查。
|
||||
bool already = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
it->second.watch_device_id = device_id;
|
||||
already = it->second.watch_device_ids.count(device_id) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Start remote desktop session if this is the first web viewer
|
||||
if (current_count == 0) {
|
||||
// Check max clients per device
|
||||
int current_count = GetWebClientCount(device_id);
|
||||
if (!already && current_count >= m_nMaxClientsPerDevice) {
|
||||
send_error("Too many viewers");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update client state. 新前端(multiWatch=true)向集合里插入;旧前端保持
|
||||
// 单设备语义:connect 替换整个集合并停掉其它设备,避免集合无限增长。
|
||||
std::vector<uint64_t> to_stop;
|
||||
bool too_many_watch = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
auto& watch = it->second.watch_device_ids;
|
||||
if (!multi_watch) {
|
||||
for (uint64_t id : watch) {
|
||||
if (id != device_id) to_stop.push_back(id);
|
||||
}
|
||||
watch.clear();
|
||||
}
|
||||
if (already) {
|
||||
watch.insert(device_id); // already true 时必然在集合内,这里只是兜底
|
||||
} else if (watch.size() >= kMaxWatchPerClient) {
|
||||
too_many_watch = true;
|
||||
} else {
|
||||
watch.insert(device_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (too_many_watch) {
|
||||
send_error("Too many devices on one connection");
|
||||
return;
|
||||
}
|
||||
// StopRemoteDesktop 会再次锁 m_ClientsMutex,必须在锁外调用。
|
||||
for (uint64_t id : to_stop) {
|
||||
StopRemoteDesktop(id);
|
||||
}
|
||||
|
||||
// Start remote desktop session if this is the first web viewer. 若之前已订阅
|
||||
// 但捕获已意外结束(例如 MFC 对话框关闭),补一次冷启动。
|
||||
bool need_start = (current_count == 0) ||
|
||||
(already && GetScreenContext(device_id) == nullptr);
|
||||
if (need_start) {
|
||||
if (!StartRemoteDesktop(device_id)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Failed to start remote desktop"));
|
||||
// 回滚本次插入,避免一个没有屏幕会话的设备长期占着订阅/计数。
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
it->second.watch_device_ids.erase(device_id);
|
||||
}
|
||||
}
|
||||
send_error("Failed to start remote desktop");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -724,6 +808,8 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
Json::Value res;
|
||||
res["cmd"] = "connect_result";
|
||||
res["ok"] = true;
|
||||
res["id"] = std::to_string(device_id);
|
||||
if (!req.empty()) res["req"] = req;
|
||||
// Only include dimensions if we have valid cached values
|
||||
// Otherwise, client will wait for resolution_changed message
|
||||
if (width > 0 && height > 0) {
|
||||
@@ -741,48 +827,72 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
|
||||
// Send cached keyframe if available
|
||||
// Send cached keyframe if available. 缓存里是 raw H.264 payload(见
|
||||
// BroadcastH264Frame),这里统一用 BuildFramePacket 封装。
|
||||
std::vector<uint8_t> cached;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||
auto it = m_DeviceCache.find(device_id);
|
||||
if (it != m_DeviceCache.end() && !it->second->keyframe_cache.empty()) {
|
||||
if (it != m_DeviceCache.end()) {
|
||||
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
|
||||
auto packet = BuildFramePacket(device_id, true,
|
||||
it->second->keyframe_cache.data(),
|
||||
it->second->keyframe_cache.size());
|
||||
SendBinary(ws_ptr, packet.data(), packet.size());
|
||||
cached = it->second->keyframe_cache;
|
||||
}
|
||||
}
|
||||
if (!cached.empty()) {
|
||||
auto packet = BuildFramePacket(device_id, true, cached.data(), cached.size());
|
||||
SendBinary(ws_ptr, packet.data(), packet.size());
|
||||
}
|
||||
}
|
||||
|
||||
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
|
||||
void CWebService::HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id,
|
||||
const std::string& req, bool multi_watch) {
|
||||
auto send_reply = [&](bool ok, const std::string& err) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "disconnect_result";
|
||||
res["ok"] = ok;
|
||||
if (!err.empty()) res["msg"] = err;
|
||||
if (requested_device_id > 0) res["id"] = std::to_string(requested_device_id);
|
||||
if (!req.empty()) res["req"] = req;
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
};
|
||||
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("disconnect_result", false, "Invalid token"));
|
||||
send_reply(false, "Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the device_id before clearing watch state
|
||||
uint64_t device_id = 0;
|
||||
std::vector<uint64_t> to_stop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
// Only disconnect if no specific device requested, or if it matches current device
|
||||
// This prevents race condition when quickly switching devices
|
||||
if (requested_device_id == 0 || it->second.watch_device_id == requested_device_id) {
|
||||
device_id = it->second.watch_device_id;
|
||||
it->second.watch_device_id = 0;
|
||||
auto& watch = it->second.watch_device_ids;
|
||||
if (multi_watch && requested_device_id != 0) {
|
||||
// 屏幕墙:只取消这一路订阅,其它设备继续。
|
||||
if (watch.erase(requested_device_id) > 0) {
|
||||
it->second.keyframe_req_at.erase(requested_device_id);
|
||||
to_stop.push_back(requested_device_id);
|
||||
}
|
||||
} else if (requested_device_id == 0 || watch.count(requested_device_id) > 0) {
|
||||
// 旧前端 / 全量断开:保持原来的“请求 id 必须匹配”的竞态保护,
|
||||
// 快速切换设备时不会误清刚建立的新订阅。
|
||||
to_stop.assign(watch.begin(), watch.end());
|
||||
watch.clear();
|
||||
it->second.keyframe_req_at.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close remote desktop if this was the last viewer
|
||||
if (device_id > 0) {
|
||||
StopRemoteDesktop(device_id);
|
||||
// Close remote desktop if this was the last viewer. StopRemoteDesktop 会再次
|
||||
// 锁 m_ClientsMutex,必须在锁外调用。
|
||||
for (uint64_t id : to_stop) {
|
||||
StopRemoteDesktop(id);
|
||||
}
|
||||
|
||||
SendText(ws_ptr, BuildJsonResponse("disconnect_result", true));
|
||||
send_reply(true, "");
|
||||
}
|
||||
|
||||
void CWebService::HandlePing(void* ws_ptr, const std::string& token) {
|
||||
@@ -795,6 +905,22 @@ void CWebService::HandlePing(void* ws_ptr, const std::string& token) {
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
}
|
||||
|
||||
uint64_t CWebService::ResolveWatchDevice(void* ws_ptr, uint64_t requested_device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it == m_Clients.end()) return 0;
|
||||
const auto& watch = it->second.watch_device_ids;
|
||||
|
||||
if (requested_device_id != 0) {
|
||||
// 显式 id 必须属于本连接的订阅集合,防止越权向别的设备注入输入。
|
||||
return watch.count(requested_device_id) ? requested_device_id : 0;
|
||||
}
|
||||
// 旧前端不带 id:只有集合恰好一台设备时才回退。多设备时绝不猜,
|
||||
// 否则会变成跨设备输入注入。
|
||||
if (watch.size() == 1) return *watch.begin();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
|
||||
// Parse JSON
|
||||
Json::Value root;
|
||||
@@ -817,16 +943,7 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
|
||||
}
|
||||
|
||||
// Get device being watched
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
it->second.last_activity = (uint64_t)time(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, ParseDeviceIdField(root, "id"));
|
||||
if (device_id == 0) return;
|
||||
|
||||
// Get screen context (not main context!)
|
||||
@@ -904,16 +1021,7 @@ void CWebService::HandleKey(void* ws_ptr, const std::string& msg) {
|
||||
}
|
||||
|
||||
// Get device being watched
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
it->second.last_activity = (uint64_t)time(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, ParseDeviceIdField(root, "id"));
|
||||
if (device_id == 0) return;
|
||||
|
||||
// Get screen context
|
||||
@@ -953,16 +1061,7 @@ void CWebService::HandleClipboardSet(void* ws_ptr, const std::string& msg) {
|
||||
}
|
||||
|
||||
// Get device being watched
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
it->second.last_activity = (uint64_t)time(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, ParseDeviceIdField(root, "id"));
|
||||
if (device_id == 0) return;
|
||||
|
||||
// Get screen context
|
||||
@@ -989,24 +1088,15 @@ void CWebService::HandleClipboardSet(void* ws_ptr, const std::string& msg) {
|
||||
ctx->Send2Client(szData.data(), (ULONG)szData.size());
|
||||
}
|
||||
|
||||
void CWebService::HandleClipboardGet(void* ws_ptr, const std::string& token) {
|
||||
void CWebService::HandleClipboardGet(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
|
||||
// Validate token
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get device being watched
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
it->second.last_activity = (uint64_t)time(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the device being watched
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, requested_device_id);
|
||||
if (device_id == 0) return;
|
||||
|
||||
// Get screen context
|
||||
@@ -1020,7 +1110,7 @@ void CWebService::HandleClipboardGet(void* ws_ptr, const std::string& token) {
|
||||
ctx->Send2Client(&bToken, sizeof(BYTE));
|
||||
}
|
||||
|
||||
void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
|
||||
void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "Invalid token"));
|
||||
@@ -1028,15 +1118,7 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
|
||||
}
|
||||
|
||||
// Get the device being watched by this client
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, requested_device_id);
|
||||
if (device_id == 0) {
|
||||
SendText(ws_ptr, BuildJsonResponse("rdp_reset_result", false, "No device connected"));
|
||||
return;
|
||||
@@ -1060,19 +1142,14 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
|
||||
}
|
||||
}
|
||||
|
||||
void CWebService::HandleAudioToggle(void* ws_ptr, const std::string& token) {
|
||||
void CWebService::HandleAudioToggle(void* ws_ptr, const std::string& token, uint64_t requested_device_id) {
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "Invalid token"));
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t device_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) device_id = it->second.watch_device_id;
|
||||
}
|
||||
uint64_t device_id = ResolveWatchDevice(ws_ptr, requested_device_id);
|
||||
if (device_id == 0) {
|
||||
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "No device connected"));
|
||||
return;
|
||||
@@ -1087,6 +1164,78 @@ void CWebService::HandleAudioToggle(void* ws_ptr, const std::string& token) {
|
||||
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", true));
|
||||
}
|
||||
|
||||
void CWebService::HandleKeyframe(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "keyframe_result";
|
||||
res["id"] = std::to_string(device_id);
|
||||
if (!req.empty()) res["req"] = req;
|
||||
|
||||
auto send = [&](bool ok, const std::string& err) {
|
||||
res["ok"] = ok;
|
||||
if (!err.empty()) res["msg"] = err;
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
};
|
||||
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
send(false, "Invalid token");
|
||||
return;
|
||||
}
|
||||
if (device_id == 0) {
|
||||
send(false, "No device");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验该 ws 确实订阅了这台设备;同一设备最多 1 次/秒,前端重试不会打爆编码器。
|
||||
bool allowed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end() && it->second.watch_device_ids.count(device_id)) {
|
||||
uint64_t now = (uint64_t)time(nullptr);
|
||||
uint64_t& last = it->second.keyframe_req_at[device_id];
|
||||
if (last == 0 || now > last) {
|
||||
last = now;
|
||||
allowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allowed) {
|
||||
send(false, "Not watching device or rate limited");
|
||||
return;
|
||||
}
|
||||
|
||||
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
|
||||
if (!ctx) {
|
||||
send(false, "No active screen session");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先把缓存里的 raw IDR 单独发给请求方,让它立刻有帧可画;随后再让客户端
|
||||
// 强制生成新的 IDR 重置参考链(缓存 IDR 无法保证后续 P 帧的引用正确)。
|
||||
std::vector<uint8_t> cached;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||
auto it = m_DeviceCache.find(device_id);
|
||||
if (it != m_DeviceCache.end()) {
|
||||
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
|
||||
cached = it->second->keyframe_cache;
|
||||
}
|
||||
}
|
||||
if (!cached.empty()) {
|
||||
auto packet = BuildFramePacket(device_id, true, cached.data(), cached.size());
|
||||
SendBinary(ws_ptr, packet.data(), packet.size());
|
||||
}
|
||||
|
||||
BYTE bToken = COMMAND_SCREEN_KEYFRAME_REQ;
|
||||
ctx->Send2Client(&bToken, sizeof(BYTE));
|
||||
|
||||
send(true, "");
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// User Management Handlers
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1280,20 +1429,21 @@ void CWebService::RegisterClient(void* ws_ptr, const std::string& client_ip) {
|
||||
}
|
||||
|
||||
void CWebService::UnregisterClient(void* ws_ptr) {
|
||||
uint64_t device_id = 0;
|
||||
std::set<uint64_t> watched;
|
||||
|
||||
// Get device_id and remove client
|
||||
// Copy the watch set and remove the client under the lock.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
auto it = m_Clients.find(ws_ptr);
|
||||
if (it != m_Clients.end()) {
|
||||
device_id = it->second.watch_device_id;
|
||||
watched = std::move(it->second.watch_device_ids);
|
||||
m_Clients.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Close remote desktop if this was the last viewer
|
||||
if (device_id > 0) {
|
||||
// Close remote desktops whose last viewer just disconnected.
|
||||
// StopRemoteDesktop re-locks m_ClientsMutex, so this must run outside the lock.
|
||||
for (uint64_t device_id : watched) {
|
||||
StopRemoteDesktop(device_id);
|
||||
}
|
||||
|
||||
@@ -1317,7 +1467,7 @@ int CWebService::GetWebClientCount(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
int count = 0;
|
||||
for (const auto& [ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -1664,7 +1814,7 @@ void CWebService::BroadcastFrame(uint64_t device_id, const uint8_t* data, size_t
|
||||
// Broadcast to all watching clients
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendBinary(ws_ptr, packet.data(), packet.size());
|
||||
}
|
||||
}
|
||||
@@ -1689,15 +1839,19 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
|
||||
if (!data || len < 9 || m_bStopping) return;
|
||||
|
||||
// Broadcast to all watching clients
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
SendBinary(ws_ptr, data, len);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendBinary(ws_ptr, data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cache keyframe (check FrameType byte at offset 4)
|
||||
// Cache keyframe (check FrameType byte at offset 4). 只缓存 raw payload:
|
||||
// HandleConnect/HandleKeyframe 发送时会统一用 BuildFramePacket 封装,
|
||||
// 缓存完整包会导致双重封装(历史 bug)。
|
||||
if (data[4] == 1) { // IDR frame
|
||||
CacheKeyframe(device_id, data, len);
|
||||
CacheKeyframe(device_id, data + 9, len - 9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1716,12 +1870,17 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
||||
it->second->screen_height = height;
|
||||
it->second->top_down = top_down;
|
||||
if (!client_type.empty()) it->second->client_type = client_type;
|
||||
// 旧 IDR 对应旧分辨率/编码器,不能再发给新加入者。
|
||||
{
|
||||
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
|
||||
it->second->keyframe_cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Notify watching clients
|
||||
Json::Value res;
|
||||
res["cmd"] = "resolution_changed";
|
||||
res["id"] = device_id;
|
||||
res["id"] = std::to_string(device_id);
|
||||
res["width"] = width;
|
||||
res["height"] = height;
|
||||
res["top_down"] = top_down;
|
||||
@@ -1733,7 +1892,7 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
}
|
||||
@@ -1766,7 +1925,7 @@ void CWebService::NotifyAudioState(uint64_t device_id, bool enabled) {
|
||||
|
||||
Json::Value res;
|
||||
res["cmd"] = "audio_state";
|
||||
res["id"] = device_id;
|
||||
res["id"] = std::to_string(device_id);
|
||||
res["enabled"] = enabled;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
@@ -1775,7 +1934,7 @@ void CWebService::NotifyAudioState(uint64_t device_id, bool enabled) {
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
}
|
||||
@@ -1787,6 +1946,7 @@ void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
||||
// Build JSON message
|
||||
Json::Value res;
|
||||
res["cmd"] = "cursor";
|
||||
res["id"] = std::to_string(device_id);
|
||||
res["index"] = cursor_index;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
@@ -1796,7 +1956,7 @@ void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
||||
// Send to all watching clients
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
}
|
||||
@@ -1807,7 +1967,7 @@ void CWebService::NotifyClipboardText(uint64_t device_id, const std::string& tex
|
||||
|
||||
Json::Value res;
|
||||
res["cmd"] = "clipboard_text";
|
||||
res["id"] = device_id;
|
||||
res["id"] = std::to_string(device_id);
|
||||
res["text"] = text;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
@@ -1816,7 +1976,7 @@ void CWebService::NotifyClipboardText(uint64_t device_id, const std::string& tex
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
}
|
||||
@@ -1855,6 +2015,18 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
// Send COMMAND_SCREEN_SPY with H264 algorithm
|
||||
// If client is already capturing (MFC opened first), it will re-send TOKEN_BITMAPINFO
|
||||
// This creates a new hidden Web dialog while MFC dialog remains visible
|
||||
|
||||
// Cold start: drop any stale IDR cached from a previous capture session, it
|
||||
// may have a different resolution / encoder instance.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||
auto it = m_DeviceCache.find(device_id);
|
||||
if (it != m_DeviceCache.end()) {
|
||||
std::lock_guard<std::mutex> cache_lock(it->second->cache_mutex);
|
||||
it->second->keyframe_cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
BYTE bToken[32] = { 0 };
|
||||
bToken[0] = COMMAND_SCREEN_SPY;
|
||||
bToken[1] = 0; // DXGI mode: 0=GDI
|
||||
@@ -1872,7 +2044,7 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (const auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
if (client.watch_device_ids.count(device_id)) {
|
||||
watchingCount++;
|
||||
}
|
||||
}
|
||||
@@ -1901,7 +2073,7 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (const auto& [ws, client] : m_Clients)
|
||||
if (client.watch_device_id == device_id) count++;
|
||||
if (client.watch_device_ids.count(device_id)) count++;
|
||||
}
|
||||
if (count == 0 && m_pParentDlg) {
|
||||
ClearWebTriggered(device_id);
|
||||
@@ -2223,14 +2395,20 @@ void CWebService::FlushDeviceChanges() {
|
||||
if (!offline.empty()) {
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (offline.count(client.watch_device_id)) {
|
||||
// 一个 ws 可能同时看多台设备:只移除真正离线的那些,保留其它。
|
||||
std::vector<uint64_t> gone;
|
||||
for (uint64_t id : offline) {
|
||||
if (client.watch_device_ids.count(id)) gone.push_back(id);
|
||||
}
|
||||
for (uint64_t id : gone) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "device_offline";
|
||||
res["id"] = std::to_string(client.watch_device_id);
|
||||
res["id"] = std::to_string(id);
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
SendText(ws_ptr, Json::writeString(builder, res));
|
||||
client.watch_device_id = 0;
|
||||
client.watch_device_ids.erase(id);
|
||||
client.keyframe_req_at.erase(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user