91 lines
5.4 KiB
Markdown
91 lines
5.4 KiB
Markdown
# guestlink - MMO remote guest line (branded "JohnFeed" on both pages since 2026-11-06)
|
|
|
|
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.
|
|
|
|
Guest page controls: **Mute my mic** (with a clear "the show cannot hear you" state),
|
|
**Hear yourself** (own mic in their headphones; refused when the headphones box is unticked),
|
|
and a **"What is happening"** panel. That panel logs connection events plus stats every 15 s
|
|
in both directions, with no IP addresses. It stays in the guest's browser until they press
|
|
Copy and send it to you (chat or email). Ask for it first when a guest sounds wrong.
|
|
|
|
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 (and a padding regression test, `test/pad_test.go.txt`) is kept in `test/jb_sim_test.go.txt` (rename to `_test.go` in the package dir to run).
|
|
|
|
Padding fix (v2b, same day): browsers send RTP padding/probe packets that use a sequence
|
|
number but carry no audio. v1/v2 treated each one as a lost packet and played an invented
|
|
frame. That was an audible glitch, plus a frame the drift correction later had to remove
|
|
(signature in the logs: lost ~= drift-drops). They are now skipped and counted as `padding`.
|
|
|
|
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.
|