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. Anyone with a Deepgram API key can also transcribe
20the meeting from their own browser (see below).
22## How it works
24There is no backend and no room registry. The techniques come from the sibling
25projects [commonview](https://github.com/concept-collection/commonview) (the
26auto-connecting mesh) and
27[commoncall](https://github.com/concept-collection/commoncall) (WebRTC media,
28quality presets, screen share):
30- **Identity** — each browser generates a secp256k1 (BIP340 schnorr) keypair,
31 persisted in localStorage. The x-only public key is the peer ID, and every
32 nostr event is signed with it, so peers can't be impersonated.
33- **Rooms** — the room name is hashed into a nostr topic; knowing the name IS
34 the key. Everyone in the room announces `{peerId, name}` on that topic every
35 few seconds via ephemeral events on public relays; entries expire when
36 announcements stop.
37- **Mesh** — being in the room is the consent: every participant automatically
38 brings up a WebRTC connection with every other participant (deterministic
39 initiator = smaller peer ID; offer/answer/ICE ride per-peer nostr topics).
40 Audio/video flows directly between browsers, with public STUN servers and
41 an optional TURN relay as fallback (see below). Rooms are softly capped at
42 8 — peers already at capacity turn newcomers away.
43- **Muted by default** — camera/mic are requested on entry so unmuting is
44 instant, but tracks start disabled. If you deny access you still join,
45 sending silent/black placeholder tracks; unmuting retries the device and
46 upgrades the track in place (`replaceTrack`, no renegotiation).
47- **Shared settings** — one settings object for the whole room, synced over
48 the per-peer control data channels with per-key last-writer-wins (revision
49 counters; ties resolved by the setter's peer ID). The video-quality presets
50 map to `RTCRtpSender.setParameters` caps that each participant applies to
51 its own outgoing senders.
53## Transcribing a meeting
55Anyone in the room who has a [Deepgram](https://deepgram.com) API key can turn
56on a live transcript from the transcript panel in the control bar. Because a
57mesh call already delivers everyone's audio to every participant, that one
58browser can transcribe the whole room: it opens a separate streaming
59connection per speaker, so the transcript says who said what without relying on
60speaker diarization to guess.
62Only speech is sent. A voice-activity gate on each participant's audio holds
63the connection open through silence with unbilled keep-alive messages and
64streams audio only while someone is actually talking, which is what keeps an
65hour-long call from being billed as eight hours of room tone. The panel shows
66how many seconds have actually been sent, so the cost is visible while it
67accrues. A short pre-roll buffer means the gate opening does not clip the
68first word.
70The key is stored in this browser and used only to connect to Deepgram; it is
71never shared with the other participants, and the person who enters it is the
72one billed. The transcript is likewise local: it is **not** sent to the other
73participants, for the same reason the chat has no history replay — text
74attributed to someone but relayed by someone else is text they cannot vouch
75for. Everyone does see that transcription is running, as a badge on the
76transcriber's tile and a line in the chat.
78Unlike the chat, the transcript is kept: it is stored in this browser under the
79room's name and comes back the next time you enter that room, with buttons to
80copy it, save it as a text file, or clear it (which asks first). Bear in mind
81that this leaves meeting transcripts in the browser's local storage.
83## The relay, and how one token covers a room
85Most pairs of browsers can reach each other directly once STUN has told them
86their public addresses. Some cannot: symmetric NAT at both ends, or a firewall
87that only permits outbound 443. Those pairs need a TURN relay, which costs
88bandwidth and therefore cannot simply be handed to everyone who loads the page.
90The arrangement here is that relay credentials are bought with a token, but
91only one person in the room needs to have one. Whoever has it types it into the
92landing form; their browser exchanges it at the credential Worker in
93[`worker/`](worker/) for a short-lived ICE configuration, and that
94configuration is then shared with the rest of the room over the same control
95data channels that carry chat and settings. Everyone else adopts it and gains
96relay candidates of their own, so the relay is available to every pair in the
97room rather than only to the token holder.
99What travels between peers is the credential, never the token. The token stays
100in the browser it was typed into; the credential expires on its own and can be
101revoked at the Worker. Credentials arriving from a peer are treated as
102untrusted input and validated down to a bounded list of well-formed ICE URLs
103before they go anywhere near an `RTCPeerConnection`.
105Note that a credential cannot help the connection that carried it: a peer
106learns it from the first peer it manages to reach, and uses it for every
107connection after that. In practice the token holder's own relay candidates are
108what make that first connection work for a peer that has none. A pair that
109stalls in the meantime is rebuilt by the ordinary connection retry, which picks
110up whatever credentials have arrived since, so recovery takes up to about
111twenty seconds.
113The control bar shows where the room's relay came from — your token, another
114participant's, or none at all. All of this is optional: with no
115`VITE_TURN_ENDPOINT` configured at build time the token field is hidden
116entirely, and calls fall back to STUN plus a free public relay exactly as
117before. See [`worker/README.md`](worker/README.md) for deployment and costs.
119## Development
121```sh
122npm install
123npm run dev
124```
126Identity is per-browser-profile (localStorage), so two tabs in the same
127profile are the *same* peer — to try a room with multiple participants, use a
128second browser or a private window.
130`npm run build` type-checks and bundles to `dist/`. Pushes to `main` deploy to
131GitHub Pages via `.github/workflows/deploy.yml`.
133To work on the relay as well, copy `.env.example` to `.env.local` and point
134`VITE_TURN_ENDPOINT` at a Worker (`cd worker && npm install && npm run dev`
135serves one locally on port 8787). The Worker is deployed separately from the
136page, with `npx wrangler deploy`.