concept-collection / commoncall
commoncall / CLAUDE.md
60 lines · 3.2 KBPreviewCodeBlameHistoryRaw
1# CLAUDE.md
3Tips for future agents working in this repo. It borrows the p2p techniques of
4the sibling project `commonview` — read that first; this file only covers what
5is different here.
7## Architecture
9```
10src/p2p/
11 identity.ts schnorr keypair; pubkey hex = peer ID — ported from commonview
12 nostr.ts minimal relay client + topic scheme — ported (adds event-id dedup)
13 peer.ts WebRTC wrapper: media tracks + a control data channel
14 network.ts the heart: presence roster + the call state machine
15 settings.ts shared per-call settings: types, quality presets, validators
16src/App.tsx join form, roster, ring/accept UI, video views
17```
19## Key design decisions
21- **No auto-connect.** Unlike commonview (which meshes every peer), WebRTC is
22 only brought up after an explicit `call-request``call-accept` handshake;
23 `getUserMedia` is also deferred until then. All pre-call messaging rides on
24 nostr per-peer topics.
25- **One call at a time.** A second incoming ring is auto-declined with
26 `busy: true`. Glare (both users call each other) is treated as mutual
27 acceptance.
28- **Ephemeral events need retries.** The caller re-publishes its ring every 4 s
29 (the `Nostr` class dedupes by event id on the receiving side); a callee in
30 `connecting` answers a re-ring by re-sending `call-accept`. Ring and connect
31 phases both time out at 45 s.
32- **Deterministic initiator.** The smaller peer ID creates the offer, same as
33 commonview — no perfect-negotiation glare handling. Both sides add their
34 tracks before signaling starts so one offer/answer round covers all media.
35- **Screen share = track swap.** `getDisplayMedia` + `RTCRtpSender.replaceTrack`
36 replaces the camera track in place (screen instead of camera, not alongside).
37 Same-kind replacement avoids renegotiation, which the one-offer design cannot
38 do — never addTrack mid-call.
39- **Shared call settings ride the control channel.** One settings object per
40 call (reset each call), editable by either side; sync is per-key
41 last-writer-wins via `{t:'set', key, value, rev}` — a same-rev tie resolves
42 to the smaller peer ID's value on both sides. The video-quality presets map
43 to `RTCRtpSender.setParameters` caps (maxBitrate / scaleResolutionDownBy /
44 maxFramerate), which each side applies to its OWN sender — live, no
45 renegotiation. While screen sharing, resolution downscaling is skipped
46 (downscaled text is unreadable) and degradationPreference is
47 maintain-resolution; caps are re-derived on every share start/stop.
48- **Mute is per-party, NOT a shared setting.** Each side owns its own flags —
49 no revision counters, the ordered control channel makes last-sent win.
50 Toggling `track.enabled` sends silence/black without renegotiation; the
51 other side is told via `{t:'mute', audio, video}` and shows badges. The
52 notice carries the EFFECTIVE outgoing video state: while screen sharing the
53 screen is always live, so a muted camera is latent until the share ends
54 (share start/stop re-sends the notice).
56## Testing
58`npm run dev`, then open two browsers (identity is per-browser-profile via
59localStorage, so two tabs in one profile are the SAME peer — use a private
60window or second browser). `npm run build` type-checks (`tsc -b`) and bundles.