feat(calls): WebRTC voice/video calls and enclave voice channels #96

Merged
nrupard merged 1 commit from feat/voice-and-video-calls into main 2026-05-14 21:55:31 +02:00
Owner

Summary

Real-time calling for lets-chat, signaled over the existing chat WebSocket hub (no media server).

  • 1:1 DM calls - one call type; voice vs video differ only in whether the camera starts on, and either peer can toggle their camera mid-call. Caller is the sole initial offerer (no setup glare); mid-call renegotiation uses perfect negotiation. Mutual-dial glare resolved deterministically by user id. Posts a "started a call" system message into the DM.
  • Enclave voice channels - new is_voice room flag, orthogonal to public/private visibility. Full-mesh multi-party (no SFU, so practically ~4-6 per channel). Newest joiner offers to everyone present; peers removed only by the explicit VoiceLeft event so one leaver does not cascade.
  • Shared - per-user ICE config via LETS_CHAT_ICE_SERVERS; avatar fallback when a participant has no camera on; /avatars/{id} now always resolves (generated default SVG for users with no custom avatar).
  • Schema - messages.is_system (0023) for system notices, rooms.is_voice (0024) for voice channels. Stale hardcoded migration lists in affected test files brought current.

Test plan

  • just test (full suite was green - 446 passing - before the final avatar/UI tweaks; cargo check passes after)
  • DM voice call connects; toggle camera both directions
  • DM video call - both sides start with camera on
  • Mutual dial (both call at once) connects instead of mutual-declining
  • Avatar shows when a participant has no camera on (custom and default avatars)
  • Create a public and a private voice channel in an enclave; join/leave with 3+ users
  • One person leaving an enclave voice channel does not drop the others

Note: WebRTC paths could not be exercised in a browser in the dev env (container OOM + multi-party needs several clients). Logic verified by code/spec review.

🤖 Generated with Claude Code

## Summary Real-time calling for lets-chat, signaled over the existing chat WebSocket hub (no media server). - **1:1 DM calls** - one call type; voice vs video differ only in whether the camera starts on, and either peer can toggle their camera mid-call. Caller is the sole initial offerer (no setup glare); mid-call renegotiation uses perfect negotiation. Mutual-dial glare resolved deterministically by user id. Posts a "started a call" system message into the DM. - **Enclave voice channels** - new `is_voice` room flag, orthogonal to public/private visibility. Full-mesh multi-party (no SFU, so practically ~4-6 per channel). Newest joiner offers to everyone present; peers removed only by the explicit `VoiceLeft` event so one leaver does not cascade. - **Shared** - per-user ICE config via `LETS_CHAT_ICE_SERVERS`; avatar fallback when a participant has no camera on; `/avatars/{id}` now always resolves (generated default SVG for users with no custom avatar). - **Schema** - `messages.is_system` (0023) for system notices, `rooms.is_voice` (0024) for voice channels. Stale hardcoded migration lists in affected test files brought current. ## Test plan - [ ] `just test` (full suite was green - 446 passing - before the final avatar/UI tweaks; `cargo check` passes after) - [ ] DM voice call connects; toggle camera both directions - [ ] DM video call - both sides start with camera on - [ ] Mutual dial (both call at once) connects instead of mutual-declining - [ ] Avatar shows when a participant has no camera on (custom and default avatars) - [ ] Create a public and a private voice channel in an enclave; join/leave with 3+ users - [ ] One person leaving an enclave voice channel does not drop the others Note: WebRTC paths could not be exercised in a browser in the dev env (container OOM + multi-party needs several clients). Logic verified by code/spec review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(calls): WebRTC voice/video calls and enclave voice channels
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 17s
c7b6da5f53
Adds real-time calling, signaled over the existing chat WebSocket hub (no media server).

1:1 DM calls: voice and video are one call type that differs only in whether the camera starts on; either peer can toggle their camera at any time. Setup has the caller as sole offerer (no glare); mid-call renegotiation uses perfect negotiation. Mutual-dial glare is resolved deterministically by user id. A "started a call" system message is posted into the DM.

Enclave voice channels: a new `is_voice` room flag (orthogonal to public/private visibility) plus a full-mesh multi-party page. The newest joiner offers to everyone already present; participants are removed only by the explicit VoiceLeft event so one person leaving does not cascade.

Shared: per-user ICE config via LETS_CHAT_ICE_SERVERS, avatar fallback when a participant has no camera on, and `/avatars/{id}` now always resolves by serving a generated default SVG.

Schema: `messages.is_system` (0023) for system notices, `rooms.is_voice` (0024) for voice channels. Stale hardcoded migration lists in the affected test files were brought current.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/voice-and-video-calls 2026-05-14 21:55:31 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!96
No description provided.