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:
1 parent
8eab26c1f6
commit
1922f87dd2
7 files changed
+502
-80
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user