1# commonroom
3Serverless group video calls in the browser.
5**Live page:** https://concept-collection.github.io/commonroom/
7Enter your name and a room name — any string you like (no spaces) — and you're
8in. Share the room URL with anyone; everyone who joins the same room is
9connected to everyone else over a full WebRTC mesh, up to 8 people. You enter
10with your microphone and camera **off** and turn them on when you're ready.
11Click any tile to enlarge it (the rest shrink to a filmstrip); click it again
12or press Esc to return to the gallery.
14The room has shared settings that anyone can change and that apply to
15everyone — currently the video quality (low / medium / high / auto, medium by
16default). You can also share your screen in place of your camera, and there's
17a room chat (with join/left notices and clickable links) that is as ephemeral
18as the call itself: you only see what's said while you're in the room, and
19nothing is stored anywhere.
21## How it works
23There is no backend and no room registry. The techniques come from the sibling
24projects [commonview](https://github.com/concept-collection/commonview) (the
25auto-connecting mesh) and
26[commoncall](https://github.com/concept-collection/commoncall) (WebRTC media,
27quality presets, screen share):
29- **Identity** — each browser generates a secp256k1 (BIP340 schnorr) keypair,
30 persisted in localStorage. The x-only public key is the peer ID, and every
31 nostr event is signed with it, so peers can't be impersonated.
32- **Rooms** — the room name is hashed into a nostr topic; knowing the name IS
33 the key. Everyone in the room announces `{peerId, name}` on that topic every
34 few seconds via ephemeral events on public relays; entries expire when
35 announcements stop.
36- **Mesh** — being in the room is the consent: every participant automatically
37 brings up a WebRTC connection with every other participant (deterministic
38 initiator = smaller peer ID; offer/answer/ICE ride per-peer nostr topics).
39 Audio/video flows directly between browsers, with public STUN servers and
40 an optional TURN relay as fallback (see below). Rooms are softly capped at
41 8 — peers already at capacity turn newcomers away.
42- **Muted by default** — camera/mic are requested on entry so unmuting is
43 instant, but tracks start disabled. If you deny access you still join,
44 sending silent/black placeholder tracks; unmuting retries the device and
45 upgrades the track in place (`replaceTrack`, no renegotiation).
46- **Shared settings** — one settings object for the whole room, synced over
47 the per-peer control data channels with per-key last-writer-wins (revision
48 counters; ties resolved by the setter's peer ID). The video-quality presets
49 map to `RTCRtpSender.setParameters` caps that each participant applies to
50 its own outgoing senders.
52## The relay, and how one token covers a room
54Most pairs of browsers can reach each other directly once STUN has told them
55their public addresses. Some cannot: symmetric NAT at both ends, or a firewall
56that only permits outbound 443. Those pairs need a TURN relay, which costs
57bandwidth and therefore cannot simply be handed to everyone who loads the page.
59The arrangement here is that relay credentials are bought with a token, but
60only one person in the room needs to have one. Whoever has it types it into the
61landing form; their browser exchanges it at the credential Worker in
62[`worker/`](worker/) for a short-lived ICE configuration, and that
63configuration is then shared with the rest of the room over the same control
64data channels that carry chat and settings. Everyone else adopts it and gains
65relay candidates of their own, so the relay is available to every pair in the
66room rather than only to the token holder.
68What travels between peers is the credential, never the token. The token stays
69in the browser it was typed into; the credential expires on its own and can be
70revoked at the Worker. Credentials arriving from a peer are treated as
71untrusted input and validated down to a bounded list of well-formed ICE URLs
72before they go anywhere near an `RTCPeerConnection`.
74Note that a credential cannot help the connection that carried it: a peer
75learns it from the first peer it manages to reach, and uses it for every
76connection after that. In practice the token holder's own relay candidates are
77what make that first connection work for a peer that has none. A pair that
78stalls in the meantime is rebuilt by the ordinary connection retry, which picks
79up whatever credentials have arrived since, so recovery takes up to about
80twenty seconds.
82The control bar shows where the room's relay came from — your token, another
83participant's, or none at all. All of this is optional: with no
84`VITE_TURN_ENDPOINT` configured at build time the token field is hidden
85entirely, and calls fall back to STUN plus a free public relay exactly as
86before. See [`worker/README.md`](worker/README.md) for deployment and costs.
88## Development
90```sh
91npm install
92npm run dev
93```
95Identity is per-browser-profile (localStorage), so two tabs in the same
96profile are the *same* peer — to try a room with multiple participants, use a
97second browser or a private window.
99`npm run build` type-checks and bundles to `dist/`. Pushes to `main` deploy to
100GitHub Pages via `.github/workflows/deploy.yml`.
102To work on the relay as well, copy `.env.example` to `.env.local` and point
103`VITE_TURN_ENDPOINT` at a Worker (`cd worker && npm install && npm run dev`
104serves one locally on port 8787). The Worker is deployed separately from the
105page, with `npx wrangler deploy`.