/ concept-collection / commonroom
Sign in
concept-collection / commonroom
commonroom / CLAUDE.md
88 lines · 5.4 KBCodeBlameHistory
3Tips for future agents working in this repo. It combines the p2p techniques of
4the sibling projects `commonview` (auto-connecting mesh) and `commoncall`
5(WebRTC media, settings, screen share) — read those first; this file only
6covers what is different here.
8## Architecture
10```
11src/p2p/
12 identity.ts schnorr keypair; pubkey hex = peer ID — ported from commoncall
13 nostr.ts minimal relay client + topic scheme — ported (roomTopic takes a room ID)
14 peer.ts WebRTC wrapper: media + control channel — ported (replaceTrack generalized to audio|video)
15 settings.ts shared ROOM settings, quality presets — default quality is 'medium', not 'auto'
16 network.ts the heart: rooms, presence, mesh, media, settings sync
1ebfe6fAdd spotlight view, room chat, and clearer media-failure messagesJeremy Magland 17src/App.tsx landing form (light) + in-room view (dark), video grid with
18 click-to-spotlight (gallery ↔ one big tile + filmstrip; Esc or
19 click again to return), control bar, chat panel (side panel on
20 wide screens, overlay ≤700px, unread badge)
23## Key design decisions
25- **Rooms, no registry.** The room ID is any string (whitespace stripped,
26 exact otherwise, case-sensitive); `roomTopic` hashes it into the nostr
27 presence topic. The URL hash holds the room (`#<encoded-room>`) so the
28 address bar is the invite link.
29- **Auto-mesh, no consent handshake.** Unlike commoncall, entering the room IS
30 the consent: on every presence announcement, `maybeConnect` brings up a
31 `Peer` (initiator = smaller peer ID, commonview's stalled-connection retry
32 at 15 s). All of commoncall's call-request/accept machinery is gone.
33- **Muted by default; placeholder tracks.** getUserMedia runs at entry but
34 tracks start `enabled = false`. Every participant ALWAYS carries exactly one
35 audio + one video track (denied/missing devices get a silent
36 AudioContext-destination track / black canvas-capture track), so
37 offer/answer stays symmetric and the one-offer, no-renegotiation design
38 holds. Unmuting without a real device retries getUserMedia and upgrades the
1ebfe6fAdd spotlight view, room chat, and clearer media-failure messagesJeremy Magland 39 placeholder via `replaceTrack` on every connection. getUserMedia failures
40 surface a cause-specific notice (`mediaErrorMessage`: permission vs
41 not-found vs device-busy, error name included) both at join and on retry —
42 on Linux, a camera held by another browser fails with NotReadableError,
43 which is NOT a permissions problem. The combined audio+video request fails
44 as a whole in that case, so `acquireMedia` retries each kind separately.
9813683Serverless group video calls: rooms, WebRTC mesh, shared settingsJeremy Magland 45- **Soft cap of 8** (`MAX_PARTICIPANTS`). A peer already holding 7 connections
46 answers an unknown peer's announcement/offer with `{t:'room-full'}` on the
47 newcomer's topic instead of connecting; a newcomer with zero connections
48 that receives room-full tears down and shows a notice. Two simultaneous
49 joiners racing for the last slot can briefly exceed the cap — accepted.
50- **Settings are room-wide, multi-party LWW.** One entry per key in
51 `settingsMeta` (`{rev, by}`); changes broadcast `{t:'set', key, value, rev,
52 by}` to all peers (complete graph — no relaying), late joiners get every
53 entry inside each peer's `hello`, and a same-rev tie is won by the SMALLER
54 setter ID. Default quality is `medium` — so quality caps are applied to each
55 sender on connect (`applyVideoParamsTo`, with one delayed retry because
56 encodings may not exist right at 'connected'), not only on change.
57- **Mute is per-participant, NOT a shared setting** — same as commoncall: own
58 flags, `{t:'mute'}` notices, `track.enabled` toggling, and the notice
59 carries the EFFECTIVE outgoing video state (screen share overrides camera
60 mute). Remote participants are assumed muted until told otherwise.
61- **Screen share = track swap on every connection.** `getDisplayMedia` +
62 `replaceTrack` per peer; a peer that joins mid-share gets the screen track
63 from `outgoingStream()`. Same-kind replacement avoids renegotiation — never
64 addTrack mid-connection.
1ebfe6fAdd spotlight view, room chat, and clearer media-failure messagesJeremy Magland 65- **Chat is ephemeral and never relayed.** `{t:'chat', text}` broadcasts on
66 the control channels; every message arrives directly from its author over a
67 channel established via signed signaling, so authorship needs no extra
68 crypto. There is deliberately NO history replay for late joiners — replay
69 would mean peers relaying others' messages, which a malicious peer could
70 fabricate; adding history requires signing each message. Log capped at 500,
71 messages at 2000 chars. Join/left lines are derived locally: hello carries a
72 self-reported `joinedAt`, and a peer whose join predates ours gets no
73 "joined" line on first sight (they were already here) — but `chatSeen`
74 ensures a blip-reconnect logs "joined" to match its "left". Links: only
75 http(s) URLs matched by `withLinks` become anchors (target=_blank,
76 rel=noopener noreferrer); never linkify other schemes.
9813683Serverless group video calls: rooms, WebRTC mesh, shared settingsJeremy Magland 77- **Cleanup is join-generation-guarded.** `joinSeq` is bumped on every
78 join/leave; async work (getUserMedia, topic hashing, display capture)
79 re-checks it after each await. `leave()` unsubscribes topics, stops all
80 tracks, closes the AudioContext, and resets settings to defaults.
82## Testing
84`npm run dev`, then open the room in two browsers (identity is
85per-browser-profile via localStorage, so two tabs in one profile are the SAME
86peer — use a private window or second browser). `npm run build` type-checks
87(`tsc -b`) and bundles. Let the user test multi-party media in real browsers;
88don't try to automate camera/mic flows.
moveopenescclose