Skip to content

Relay fallback so guests outside the local network can join - #1

Open
iamvinbas wants to merge 1 commit into
mainfrom
feat/guest-relay
Open

iamvinbas wants to merge 1 commit into
mainfrom
feat/guest-relay

Conversation

@iamvinbas

Copy link
Copy Markdown
Owner

Problem

A guest outside the host's local network could not take part in a call. With an Internet invite link, the guest page and its signaling travel through the Cloudflare tunnel, but audio and video needed a direct WebRTC path between the two computers, helped only by a STUN server. Behind strict NATs, carrier-grade NAT, mobile data or firewalls there is no such path: the guest sat on "Connecting…" forever, with no error or timeout, while the host saw them as connecting.

Reproduced with a guest limited to relay-only ICE candidates and no TURN server (no direct path possible), on localhost and through a real Cloudflare tunnel. The old internet test passed only because host and guest ran on the same machine.

Fix

Relay fallback (zero configuration). When a guest's peer connection is not connected about 10 s after the offer (or turns failed, or stays disconnected), the host closes it and switches that guest to the relay: media travels over dedicated WebSockets through the streamFree server, over the same link the guest page already uses.

  • /ws/relay/guest (authenticated with a per-guest key sent only in that guest's welcome) and /ws/relay/host; separate from signaling, frames dropped when a receiver's queue is full, 2 MB message cap, routes torn down when a guest leaves or is removed.
  • WebCodecs (web/js/relay.js): H.264 (Annex B) or VP8 video, at most 640 px on the long side (portrait kept, encoder reconfigured on rotation); Opus or 16-bit PCM audio. Each side picks what the other can decode.
  • Sequence numbers on packets; a gap makes the receiver ask for a keyframe (rate-limited).
  • Audio plays through a ~100 ms jitter buffer that skips ahead past 250 ms, so delay does not grow during long calls (web/js/relay-worklet.js).
  • A relayed guest enters the mixer as a normal guest with its own mix-minus bus; one return-feed encoder serves all relayed guests.
  • Works with Chrome hosts and the macOS app (WebKit: no MediaStreamTrackGenerator, so decoded frames go through a canvas track).
  • Settings → Guests → Connection: automatic (direct, relay if that fails) or always through streamFree.

Robustness

  • Keepalive pings every 20 s on signaling and relay sockets, so proxies such as Cloudflare do not close idle backstage connections.
  • Guests reconnect by themselves after a drop and go back on stage (per-tab client id). A removed guest gets close code 4001 and is told so instead of reconnecting.
  • The internet link is announced only once cloudflared reports a registered edge connection. A tunnel that fails to start or stops later is restarted (up to four attempts) and the studio shows a toast with the new address.
  • Second default STUN server (Cloudflare), also applied to existing settings that still hold only the old single default.
  • iOS: the guest's AudioContext is created on the join click, and a "Tap to hear the studio" button appears if Safari blocks playback.

Tests

  • Browser e2e (npm test, 20/20 passing):
    • a guest with no direct path joins through the relay: video and audio both ways (peak level checked), mix-minus routing, recording with the guest on stage passes checkMedia;
    • the same guest through a real Cloudflare tunnel;
    • a guest whose connection drops comes back on stage;
    • "Always through streamFree" connects in about 2 s with no peer connection.
  • npm run test:native: the macOS app (WebKit host) now also hosts a relayed guest; it decodes the guest's video (relay-ok) and sends the program. 10 of 11 runs recorded the full 40 s; one run, the first after a rebuild, recorded 26 s of continuous media with no crash report. Cause unknown, not reproduced since.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (unit + FFmpeg oracle) pass.

Not tested

  • A real remote network (only a simulated strict one).
  • Safari, iOS and Firefox guests, including the PCM/Opus fallbacks.
  • Tunnel auto-restart after a real drop.

Notes

  • Privacy: direct connections are end-to-end encrypted. Relayed media is encrypted in transit, but with an Internet link it passes through Cloudflare's edge, which terminates TLS. Both READMEs now say so.
  • Relayed calls have a little more delay than direct ones (not measured).

🤖 Generated with Claude Code

Guests on the internet could load the invite page and exchange signaling
through the Cloudflare tunnel, but audio and video needed a direct WebRTC
path with only a STUN server to help. Behind strict NATs, carrier-grade NAT,
mobile data or firewalls there is no such path, and the guest sat on
"Connecting…" forever while the host saw them as connecting.

- Relay: when a guest's peer connection is not connected about 10 s after the
  offer (or fails, or stays disconnected), the host closes it and both pages
  exchange media over dedicated WebSockets through the server
  (/ws/relay/guest, authenticated with a per-guest key, and /ws/relay/host).
  Media is encoded with WebCodecs: H.264 or VP8 video (max 640 px on the long
  side, portrait kept), Opus or PCM audio, negotiated per side. Packets carry
  sequence numbers and receivers request keyframes after gaps; audio plays
  through a jitter buffer with drift control. Relayed guests get their own
  mix-minus and one shared return-feed encoder. Works with Chrome and WebKit
  (macOS app) hosts. Settings → Guests → Connection can force the relay.
- Keepalive pings every 20 s on signaling and relay sockets, so proxies such
  as Cloudflare do not close idle connections.
- Guests reconnect by themselves after a drop and go back on stage (per-tab
  client id); a removed guest gets close code 4001 and is told so.
- The internet link is announced only once cloudflared has registered an edge
  connection; a tunnel that fails or stops is restarted (up to four attempts)
  and the studio is told about the new address.
- Second default STUN server (Cloudflare), also added to existing settings
  that still hold only the old default.
- iOS: the guest's audio context starts on the join click, and a "Tap to hear
  the studio" button appears if playback is blocked.

Tests: browser e2e for a guest with no direct path (relay-only ICE, no TURN)
on localhost and through a real Cloudflare tunnel, with media both ways,
mix-minus and a recording with the guest on stage; reconnect and back on
stage; forced relay; the native WebKit host test gains a relayed guest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant