/ concept-collection / commonroom
Sign in
concept-collection / commonroom
commonroom
Go to fileHistoryFork
.githubOptional TURN relay, gated by a token and shared across the room
srcOptional TURN relay, gated by a token and shared across the room
workerOptional TURN relay, gated by a token and shared across the room
.env.exampleOptional TURN relay, gated by a token and shared across the room
.gitignoreOptional TURN relay, gated by a token and shared across the room
CLAUDE.mdOptional TURN relay, gated by a token and shared across the room
index.htmlServerless group video calls: rooms, WebRTC mesh, shared settings
package-lock.jsonServerless group video calls: rooms, WebRTC mesh, shared settings
package.jsonServerless group video calls: rooms, WebRTC mesh, shared settings
README.mdOptional TURN relay, gated by a token and shared across the room
tsconfig.jsonServerless group video calls: rooms, WebRTC mesh, shared settings
tsconfig.tsbuildinfoOptional TURN relay, gated by a token and shared across the room
vite.config.tsServerless group video calls: rooms, WebRTC mesh, shared settings

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.setParameters caps 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.

moveopenescclose