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
@@ -68,12 +68,16 @@ struct WebClient {
|
||||
std::string token;
|
||||
std::string username;
|
||||
std::string role; // "admin" | "viewer"
|
||||
uint64_t watch_device_id; // 0 = not watching any device
|
||||
// 当前这个 ws 订阅的设备集合。屏幕墙允许多路复用;旧前端仍按单设备语义使用
|
||||
// (connect 替换整个集合)。
|
||||
std::set<uint64_t> watch_device_ids;
|
||||
// 每台设备的 keyframe 请求节流时间戳(秒),防止前端重试打爆服务端/客户端。
|
||||
std::map<uint64_t, uint64_t> keyframe_req_at;
|
||||
uint64_t connected_at;
|
||||
uint64_t last_activity; // Last message/pong received (for heartbeat timeout)
|
||||
std::string client_ip;
|
||||
|
||||
WebClient() : watch_device_id(0), connected_at(0), last_activity(0) {}
|
||||
WebClient() : connected_at(0), last_activity(0) {}
|
||||
};
|
||||
|
||||
// Login attempt tracking for rate limiting
|
||||
@@ -185,15 +189,24 @@ private:
|
||||
void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip);
|
||||
void HandleGetSalt(void* ws_ptr, const std::string& msg);
|
||||
void HandleGetDevices(void* ws_ptr, const std::string& token);
|
||||
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
|
||||
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
|
||||
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req = "", bool multi_watch = false);
|
||||
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0,
|
||||
const std::string& req = "", bool multi_watch = false);
|
||||
void HandleKeyframe(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req);
|
||||
void HandlePing(void* ws_ptr, const std::string& token);
|
||||
void HandleMouse(void* ws_ptr, const std::string& msg);
|
||||
void HandleKey(void* ws_ptr, const std::string& msg);
|
||||
void HandleClipboardSet(void* ws_ptr, const std::string& msg);
|
||||
void HandleClipboardGet(void* ws_ptr, const std::string& token);
|
||||
void HandleRdpReset(void* ws_ptr, const std::string& token);
|
||||
void HandleAudioToggle(void* ws_ptr, const std::string& token);
|
||||
void HandleClipboardGet(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
void HandleRdpReset(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
void HandleAudioToggle(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
|
||||
// 输入类命令的设备解析:优先用显式 id(必须在 watch 集合内);否则当集合恰好
|
||||
// 只有一台设备时回退到它(兼容不带 id 的旧前端)。多设备且无 id 时返回 0(丢弃),
|
||||
// 绝不能回退到任意集合成员,否则会变成跨设备输入注入。
|
||||
uint64_t ResolveWatchDevice(void* ws_ptr, uint64_t requested_device_id);
|
||||
|
||||
// Token management
|
||||
std::string GenerateToken(const std::string& username, const std::string& role);
|
||||
@@ -246,6 +259,10 @@ private:
|
||||
static const int HEARTBEAT_INTERVAL_SEC = 30; // Send ping every 30 seconds
|
||||
static const int HEARTBEAT_TIMEOUT_SEC = 90; // Disconnect if no activity for 90 seconds
|
||||
|
||||
// 每个 ws 最多订阅的设备数(屏幕墙最多 25 个 tile,留余量)。防止异常客户端
|
||||
// 订阅数百台设备拖慢每一帧的广播循环。
|
||||
static constexpr size_t kMaxWatchPerClient = 32;
|
||||
|
||||
// Heartbeat thread function
|
||||
void HeartbeatThread();
|
||||
|
||||
|
||||
Reference in new issue
Block a user