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,96 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sortedWatchIDs(c *wsClient) []string {
|
||||
out := make([]string, 0, len(c.watchIDs))
|
||||
for id := range c.watchIDs {
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestApplyConnectWatchMulti(t *testing.T) {
|
||||
c := &wsClient{watchIDs: make(map[string]struct{})}
|
||||
|
||||
if stopped, tooMany := applyConnectWatch(c, "a", true); len(stopped) != 0 || tooMany {
|
||||
t.Fatalf("first connect = (%v, %v), want ([], false)", stopped, tooMany)
|
||||
}
|
||||
if stopped, tooMany := applyConnectWatch(c, "b", true); len(stopped) != 0 || tooMany {
|
||||
t.Fatalf("second connect = (%v, %v), want ([], false)", stopped, tooMany)
|
||||
}
|
||||
if got, want := sortedWatchIDs(c), []string{"a", "b"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("watch set = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// Repeated connect (focus switch) must be a no-op.
|
||||
if stopped, tooMany := applyConnectWatch(c, "a", true); len(stopped) != 0 || tooMany {
|
||||
t.Fatalf("repeat connect = (%v, %v), want ([], false)", stopped, tooMany)
|
||||
}
|
||||
if got, want := sortedWatchIDs(c), []string{"a", "b"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("watch set after repeat = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyConnectWatchLegacyReplaces(t *testing.T) {
|
||||
c := &wsClient{watchIDs: map[string]struct{}{"a": {}, "b": {}}}
|
||||
|
||||
stopped, tooMany := applyConnectWatch(c, "c", false)
|
||||
if tooMany {
|
||||
t.Fatal("legacy connect hit the cap, want not")
|
||||
}
|
||||
sort.Strings(stopped)
|
||||
if want := []string{"a", "b"}; !reflect.DeepEqual(stopped, want) {
|
||||
t.Fatalf("stopped = %v, want %v", stopped, want)
|
||||
}
|
||||
if got, want := sortedWatchIDs(c), []string{"c"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("watch set = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyConnectWatchCap(t *testing.T) {
|
||||
c := &wsClient{watchIDs: make(map[string]struct{})}
|
||||
for i := 0; i < maxWatchPerClient; i++ {
|
||||
applyConnectWatch(c, fmt.Sprintf("d%02d", i), true)
|
||||
}
|
||||
if _, tooMany := applyConnectWatch(c, "overflow", true); !tooMany {
|
||||
t.Fatal("expected the cap to reject a new device")
|
||||
}
|
||||
if len(c.watchIDs) != maxWatchPerClient {
|
||||
t.Fatalf("watch set size = %d, want %d", len(c.watchIDs), maxWatchPerClient)
|
||||
}
|
||||
// An already watched device must still be accepted at the cap.
|
||||
if _, tooMany := applyConnectWatch(c, "d00", true); tooMany {
|
||||
t.Fatal("repeat connect at the cap was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWatchDevice(t *testing.T) {
|
||||
h := &wsHub{}
|
||||
c := &wsClient{watchIDs: map[string]struct{}{"a": {}}}
|
||||
|
||||
if got := h.resolveWatchDevice(c, []byte(`{"id":"a"}`)); got != "a" {
|
||||
t.Fatalf("explicit watched id = %q, want a", got)
|
||||
}
|
||||
if got := h.resolveWatchDevice(c, []byte(`{"id":"nope"}`)); got != "" {
|
||||
t.Fatalf("explicit unwatched id = %q, want empty", got)
|
||||
}
|
||||
// Old frontend: no id, exactly one watched device.
|
||||
if got := h.resolveWatchDevice(c, []byte(`{}`)); got != "a" {
|
||||
t.Fatalf("no id, single device = %q, want a", got)
|
||||
}
|
||||
|
||||
c.watchIDs["b"] = struct{}{}
|
||||
if got := h.resolveWatchDevice(c, []byte(`{}`)); got != "" {
|
||||
t.Fatalf("no id, two devices = %q, want empty", got)
|
||||
}
|
||||
if got := h.resolveWatchDevice(c, []byte(`{"id":"a"}`)); got != "a" {
|
||||
t.Fatalf("explicit id, two devices = %q, want a", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user