Feature: Multi-device screen wall on the Go server

Bring the Go web backend up to parity with the MFC server's screen wall.
A browser tab can now stream several devices over one WebSocket: the
single `watching` field becomes a per-connection watch set guarded by the
hub mutex, capped at 32 devices per socket. The challenge advertises
`caps.multiWatch` and `caps.keyframeRequest`, binary frames stay keyed by
the low 32 bits of the device ID, and connect/disconnect replies echo the
device id and request id so the wall can route concurrent responses.

Input commands (mouse, key, clipboard, rdp_reset) resolve their target
from the watch set: an explicit id must be a current subscription, and an
absent id is accepted only while exactly one device is watched. Legacy
frontends that do not send `multiWatch` keep replace-all semantics, so a
repeated device switch cannot grow the set without bound. Offline
devices are removed from every watch set, and teardown is computed from a
snapshot taken outside the hub lock to avoid re-entrant locking.

Add the `keyframe` command: it replays the cached IDR, then asks the
device to emit a fresh one, rate-limited to one request per device per
second. This exposed a Go-only corruption bug: the client answers the
request with TOKEN_KEYFRAME (134), which the dispatcher treated as a
no-op. That token only appears on forced IDRs because the client's GOP is
effectively infinite, so the dropped IDR left the browser decoding
P-frames against a reference chain it never received, producing
persistent garbling. Relay it as a keyframe instead. Also drop the
cached IDR when the resolution changes, mirroring the C++ server, so a
late joiner cannot receive an old-geometry keyframe.

Add tests for the watch-set transitions and for relaying TOKEN_KEYFRAME.

Co-Authored-By: deepseek-flash
This commit is contained in:
yuanyuanxianganddeepseek-flash committed 2026-10-18 11:51:28 +02:00
1 parent 8eab26c1f6
commit 1922f87dd2
7 files changed
+502 -80

No files matched your search

+10 -5
View File
@@ -138,11 +138,16 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) {
case protocol.TokenClipboardText:
h.handleClipboardText(ctx, data)
case protocol.TokenKeyframe:
// Sent by the client only when frameID % m_GOP == 0; the client's
// DEFAULT_GOP is 0x7FFFFFFF (effectively infinite), so this token
// is essentially unused in practice. Treat as a no-op for now —
// IDRs always arrive in-band via TOKEN_NEXTSCREEN and we catch
// them via the H264 NAL scan in handleScreenFrame.
// The client switches to this token when it emits an IDR on request
// (COMMAND_SCREEN_KEYFRAME_REQ) or on a GOP boundary. The header
// layout is identical to TOKEN_NEXTSCREEN, but the payload is always
// an IDR, so relay it as frameType=1 without sniffing. Dropping it
// (the old behaviour, harmless while the client's DEFAULT_GOP of
// 0x7FFFFFFF meant this token never appeared) breaks keyframe
// recovery: the browser keeps decoding P-frames against a reference
// chain it never received, so the picture stays corrupt until the
// screen session restarts.
h.handleScreenFrame(ctx, data, true)
case protocol.CmdCursorImage:
// Custom cursor bitmaps — relayed in Phase 5+ when the web cursor
// overlay learns to render arbitrary BGRA images. Drop silently for
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"testing"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/hub"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
)
type frameRecorder struct {
frames []recordedFrame
}
type recordedFrame struct {
deviceID string
packet []byte
isKey bool
}
func (r *frameRecorder) OnScreenFrame(deviceID string, packet []byte, isKeyframe bool) {
r.frames = append(r.frames, recordedFrame{deviceID, packet, isKeyframe})
}
func (r *frameRecorder) OnDeviceOnline(hub.DeviceInfo) {}
func (r *frameRecorder) OnDeviceOffline(string) {}
func (r *frameRecorder) OnDeviceUpdate(string, int, string) {}
func (r *frameRecorder) OnResolutionChange(string, int, int) {}
func (r *frameRecorder) OnCursorChange(string, byte) {}
func (r *frameRecorder) OnClipboardText(string, string) {}
func (r *frameRecorder) OnTerminalReady(string, bool) {}
func (r *frameRecorder) OnTerminalData(string, []byte) {}
func (r *frameRecorder) OnTerminalClosed(string, string) {}
// TestOnReceiveRelaysKeyframeToken guards the forced-IDR recovery path: when
// the browser asks for a keyframe, the client replies with TOKEN_KEYFRAME.
// Dropping that token (the old no-op) leaves the browser decoding P-frames
// against a reference chain it never received, i.e. persistent corruption.
func TestOnReceiveRelaysKeyframeToken(t *testing.T) {
h := hub.New()
rec := &frameRecorder{}
h.Subscribe(rec)
h.Register(&hub.Device{ID: "dev", PeerIP: "10.0.0.1"},
&connection.Context{RemoteAddr: "10.0.0.1:1000"})
screenCtx := &connection.Context{RemoteAddr: "10.0.0.1:1001"}
if !h.BindScreenConn("dev", screenCtx) {
t.Fatal("screen bind failed")
}
// [TOKEN_KEYFRAME][algo=H264][cursorPos:8][cursorIdx:1][IDR NAL]
data := []byte{
protocol.TokenKeyframe, 2,
0, 0, 0, 0, 0, 0, 0, 0,
0,
0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84,
}
(&MyHandler{hub: h}).OnReceive(screenCtx, data)
if len(rec.frames) != 1 {
t.Fatalf("frames relayed = %d, want 1 (TOKEN_KEYFRAME must not be dropped)", len(rec.frames))
}
f := rec.frames[0]
if f.deviceID != "dev" {
t.Fatalf("deviceID = %q, want dev", f.deviceID)
}
if !f.isKey || f.packet[4] != 1 {
t.Fatalf("isKeyframe=%v frameType=%d, want true/1", f.isKey, f.packet[4])
}
if want := 9 + len(data) - (1 + protocol.ScreenFrameHeaderLen); len(f.packet) != want {
t.Fatalf("packet len = %d, want %d", len(f.packet), want)
}
}