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