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
97 lines
3.1 KiB
Go
97 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|