rtc-call

command
v0.85.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 20 Imported by: 0

README

rtc-call

One Go binary, anonymous users, rooms by name: the dogfood example for battery/rtc and the rtc runtime module. Two people who type the same room name end up in the same peer-to-peer video call, with chat over a negotiated data channel. No accounts, no invite links, and no media in the Go process.

This is the reference example for the packaged signaling surface:

  • battery/rtc (rtc.New + app.RegisterPlugin): rooms, the addressed offer/answer/ICE relay, ICE server config with optional per-peer TURN credentials. The example writes zero signaling code.
  • The rtc runtime module (__gofastr.connectRoom): one RTCPeerConnection per remote peer, perfect negotiation, trickle ICE, the negotiated chat channel, reconnect generations.

examples/webmcp-remote-assist is the contrast: that example hand-rolls its relay on core/stream.StateChannel to teach the underlying shape for one fixed pair of roles. When you want rooms of peers, this example is the shape: register the battery, derive the rtc.Join from your own auth, open the Permissions-Policy.

Run it

cd examples/rtc-call && gofastr dev   # or: go run .

Open http://localhost:8091 (the framework's dev isolation may remap the port; the startup log names the real one), pick a name and a room, and open the same room in a second browser window. Share the camera in one; the other sees it.

Environment:

  • CALL_STUN — STUN URL. Unset means stun:stun.l.google.com:19302; set to an empty string to disable (both peers on one machine). ICE URL schemes are validated at rtc.New, which panics on a bad one.
  • CALL_TURN_URL + CALL_TURN_SECRET — when both are set, every peer gets its own time-limited TURN credential in the coturn static-auth-secret convention. The secret never leaves the process.

The boundaries it exists to show

Boundary Where
Identity is server-derived The lobby's form sets one HttpOnly call_name cookie. The signaler's Authorize reads the cookie and ?room=; a client names nothing about itself on the wire.
The gate before the upgrade Authorize answers 401 without the cookie, 400 on a bad room, before any WebSocket upgrade is attempted. The room page answers the same failures with a redirect to the lobby.
The camera is open by header, not hope rtc.PermissionsPolicy(rtc.Camera, rtc.Microphone) keeps the framework's defaults and opens both features to this origin only.
Media bypasses the server getUserMedia feeds room.addTrack; SDP and ICE cross the signaler as opaque addressed frames; audio and video never do.
Every state server-rendered The room page ships the local tile, one remote-tile <template>, a chat-line <template>, and both pills of each status pair. app.js flips hidden, sets textContent/srcObject, and clones templates. No innerHTML, no element creation except template.content.cloneNode(true).
Mute is two moves, not one The audio track's enabled flips locally (instant, no renegotiation) and room.setStatus({muted}) tells the room, which renders the other side's pill.
Leaving is a real navigation Leave is a plain link to the lobby. app.js is registered with RegisterDocumentScript scoped to /room, so the runtime loads the lobby as a real document instead of a partial swap, and that teardown retires the script, the sockets, and the tracks together. No location.href anywhere.
Chat has no server Messages travel on the negotiated chat data channel. The form's GET action is only the no-script fallback; there is no chat endpoint to attack.

What is demo-grade on purpose

  • A name cookie is not authentication. Anyone can set any name; the room gate proves a form was filled, not a person. A real app puts its session check inside Authorize (handler.GetUser, a session cookie, whatever it already trusts) and the rest stays.
  • Rooms are names, not secrets. Anyone who guesses the room name joins it; MaxPeers (default 8) caps the mesh. Room access control is the host's Authorize, not the room string.
  • No room list, no presence UI. The roster lives in the signaler; this example renders only what the page itself can prove.

Deployment notes

  • HTTPS and WSS. getUserMedia is a secure-context API. The script builds its WebSocket URL from the page scheme (wss under HTTPS); terminate TLS at your proxy and forward the upgrade headers for /__gofastr/rtc.
  • STUN/TURN. The defaults suit the demo. Real networks need STUN at minimum, TURN behind symmetric NAT; see gofastr docs rtc for the coturn setup the TURN env vars expect.
  • Cookies. call_name is HttpOnly, SameSite=Lax, Path=/ (the signaler lives under /__gofastr/, outside the page tree). Secure is decided per request from TLS or X-Forwarded-Proto, so plain-HTTP localhost works and a TLS deployment never sends the cookie in clear. The join POST is same-origin checked.
  • Mesh limits. Everyone connects to everyone: O(n²) connections, each browser uploads one stream per peer. Past a handful of participants you want an SFU, which this battery is not.

Tests

go test ./examples/rtc-call/ -count=1           # HTTP level
go test ./examples/rtc-call/ -count=1 -run TestCallFlow -v  # one Chrome, two tabs

The browser suite launches Chromium with fake media flags (--use-fake-device-for-media-stream, --use-fake-ui-for-media-stream), plays Ann and Bob in two tabs of one browser, and covers hydration, the peer-to-peer camera, chat over the data channel, mute as a status document, and the leave that removes the tile. It writes /tmp/rtc-call-A.png and /tmp/rtc-call-B.png as evidence for the reviewer.

Documentation

Overview

Package main is the dogfood example for battery/rtc: anonymous users, rooms by name, camera and microphone peer to peer, chat over a negotiated data channel. One binary, no accounts.

The boundaries it exists to teach:

  • Identity is a cookie. The lobby's form sets one HttpOnly cookie (the display name); the room page and the signaler both read it and nothing else. rtc.Join carries only what the server derived.
  • The signaler is the battery. rtc.New + RegisterPlugin mounts the WebSocket endpoint; the example writes no signaling code, unlike webmcp-remote-assist, which hand-rolls its relay to teach the underlying StateChannel shape.
  • Every visible state is server-rendered. The room page ships the local tile, one remote-tile template, a chat-line template, and both pills of every status pair; static/app.js only flips hidden, sets textContent and srcObject, and clones templates.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL