Jitter buffer v2, per-minute rx logging, websocket keepalive

The cohost sounded jittery on the 2026-11-03 show. v1 skipped ahead on an
underrun and kept playing from an empty buffer, so after the first hiccup
every later wobble was another glitch. It also never corrected a slow guest
clock: ~12 glitches/min in simulation at 30 ppm slow.

- Underrun: hold the sequence position and refill to a larger target
- Drift correction both ways (drop/insert a frame in quiet moments),
  quiet judged relative to the guest's own noise floor
- 80 ms start, 300 ms max, relaxes after 2 calm minutes
- Log 'rx 1m:' stats, the selected route, and call totals
- Ping the guest websocket every 25 s (the proxy closed it every 90 s)
- Simulation kept in test/jb_sim_test.go.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
namandClaude Opus 5.5 committed 2026-11-04 11:21:50 -06:00
1 parent b0e3b805b1
commit de7ed62d96
5 files changed
+346 -64

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
# guestlink - MMO remote guest line
Self-hosted replacement for Cleanfeed: one remote guest, live audio both ways, plus an
anonymous text chat between the guest and the host. Built 2026-11-02.
| What | Where |
|---|---|
| Guest page | `https://guest.mmo.show/j/<token>` (NPM on .158 -> 192.168.50.204:8790) |
| Host console | `http://192.168.50.204:8791` (LAN/VPN only, never proxied) |
| Media | UDP **50090** - one port for all WebRTC traffic; forwarded on the router to .204 |
| Service | `systemctl --user {status,restart} guestlink` · logs: `journalctl --user -u guestlink -f` |
| Invite token | `~/.local/state/guestlink/token` (0600) - survives restarts |
## Audio path (same as `motu-cleanfeed`)
- guest voice -> `pacat` -> sink **`motu_guest`** -> From Computer 1-2 -> guest strip
- **`motu_mixminus.monitor`** (To Computer 3-4, show minus guest) -> `parec` -> guest
Both are ordinary PipeWire client streams that exist only during a call. They never reopen
the MOTU. They carry `node.dont-fallback/dont-reconnect/dont-move`, so if `motu_guest`
disappears playback **fails** instead of landing on the default output (tested).
Restarting guestlink is safe for the rest of the rig. It only drops the guest's call.
Opus 48 kHz stereo, 128 kb/s, in-band FEC. The guest page disables browser echo cancellation,
noise suppression and AGC. The "I'm wearing headphones" box is ticked by default; unticking it
turns echo cancellation back on for guests on speakers.
The receive side has an adaptive jitter buffer (v2, 2026-11-04): 80 ms start, max 300 ms.
On an underrun it holds its place and refills to a larger target, so delayed packets are
played rather than thrown away. It corrects guest/MOTU clock drift in both directions by
dropping (guest fast) or adding (guest slow) a frame during a quiet moment. "Quiet" is
relative to the guest's own noise floor. The target rises on underruns and late packets
and relaxes after two calm minutes. Host console: `buffer x/y ms`, lost/FEC/concealed,
late, drift-drops, inserted, underruns.
**Logs for diagnosing a bad night:** one `rx 1m:` line a minute per call (buffer, losses,
underruns, drift corrections), the network `route:` when it is chosen, and `call totals:`
when it ends. `journalctl --user -u guestlink --since ...`
Why v2: v1 skipped ahead on an underrun and kept playing from an empty buffer, so after the
first hiccup every later wobble was another glitch. It also never corrected for a slow guest
clock. In simulation that was ~12 glitches/min with a guest clock 30 ppm slow, against ~0
for v2. The 3 Nov 2026 cohost "jitter" was probably this, but that night was not measured.
The simulation is kept in `test/jb_sim_test.go.txt` (rename to `_test.go` in the package dir to run).
The guest websocket is pinged every 25 s. Without that, the proxy closed it every 90 s
(signalling only, audio unaffected, but the guest saw "reconnecting...").
## Host console
New link (ends the session, clears chat), Revoke, Copy, End audio, Kick, Clear chat. It shows
live meters for both directions and the selected network route. The page never plays sound
(it is on the rig): a new guest message flashes the tab title.
## Chat
Anonymous: no accounts, and the guest's name is optional (default "Guest"). The guest sees
the host as `-host-label` (default "Host"). Messages are kept **in memory only** (last 300),
are never written to disk or logs, and are cleared by New link / Clear / restart. Guest
messages are rate limited and rendered as plain text.
## Public IP
Learned per call via STUN (google, then cloudflare), falling back to the A record of
guest.mmo.show. Override with `-public-ip`.
## Build / test
cd ~/apps/guestlink && go build -tags nolibopusfile -o guestlink . && go build -tags nolibopusfile -o glprobe ./cmd/glprobe
systemctl --user restart guestlink # drops a live guest - not during a show
`glprobe` is a headless test guest. **It creates a new link (ending the current session)**
unless given `-token`. Safe offline test on spare ports, with no PipeWire playback:
./guestlink -public 127.0.0.1:18790 -host 127.0.0.1:18791 -udp 50091 \
-sink none -test-playout-file /tmp/rx.raw -state /tmp/glstate &
./glprobe -hostws ws://127.0.0.1:18791/ws -guest ws://127.0.0.1:18790 -tone -20
## Fallback
`motu-cleanfeed` is untouched and still works. Don't run both at once: both would feed `motu_guest`.
## Not done (v1)
One guest only. No TURN relay: a guest on a network that blocks all outbound UDP (some
corporate/hotel Wi-Fi) cannot connect; they can use a phone hotspot. No recording.