Calls: screen share, mic/cam toggles, status badges, pin + fullscreen, call timer

Screen sharing swaps the outgoing video track on every peer's sender with
replaceTrack (same m-line, no renegotiation); the camera returns when the
share stops, including via the browser's own "Stop sharing" bar. Mic and
camera toggles flip track.enabled. A new Status signal kind broadcasts
mic/cam/screen state so tiles can show badges; newcomers get it directed
when their Join arrives.

Tiles carry a name label, click to pin as a stage (a peer's screen share
pins itself), double-click for fullscreen. The call bar has an elapsed
timer and a participant count. Controls are icon-only inline SVGs on
currentColor: plain with a strike when off, a soft glow and ticking LED
when live. Red is reserved for leave.

Also fixed along the way: the peer map was a non-reactive StoredValue, so
the tile list never re-rendered when someone joined; ICE candidates that
arrive before the remote description are now buffered instead of
rejected; a peer whose connection fails or closes is dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3sD29ozDvqA7jozeZXZoB
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-02 17:26:00 +02:00
co-authored by Claude Fable 5.1
parent c1c337c2a2
commit 78e9c9ff94
6 changed files with 847 additions and 125 deletions
+26
View File
@@ -110,6 +110,8 @@ src/
app.rs Leptos UI (login gate + chat console)
auth.rs shared User type + current_user server fn
chat.rs shared ChatMessage/rooms + send_message server fn (publishes to NATS)
call.rs call signaling types + send_signal server fn (call.room.*, never archived)
webrtc.rs browser-only mesh WebRTC: peers, mic/cam toggles, screen share
server/
oidc.rs Kanidm OIDC login/callback/logout handlers
sse.rs NATS → browser SSE bridge (one subscription per client)
@@ -117,6 +119,30 @@ src/
style/main.css the console theme
```
## Calls
The `lobby` room has a mesh WebRTC call (every pair of browsers connects
directly; fine for a handful of people, not more). Signaling rides NATS on
`call.room.<room>` via `/call-sse/<room>` and the `send_signal` server fn, so
it is never archived. Only a public STUN server is configured: calls across
symmetric NATs will not connect without a TURN relay, which this app does not
run.
In a call:
- **MIC / CAM** toggle your mic and camera (the track stays attached and sends
silence/black, so toggling is instant and needs no renegotiation).
- **SCR ▶ share** shares a screen, window, or tab. It swaps the outgoing video
track on every peer with `replaceTrack`; your camera comes back when you
stop, or when the browser's own "Stop sharing" bar is used.
- Tiles show the participant's name and MIC ✕ / CAM ✕ / SCR badges, kept in
sync by a `Status` signal on the same subject.
- **Click** a tile to pin it as the stage (a shared screen pins itself);
**double-click** for fullscreen.
- A peer whose tab closes without leaving is dropped once their connection
fails, and ICE candidates that arrive before the offer are buffered rather
than lost.
## Notes & production hardening
- Sessions are in-memory (`tower-sessions` `MemoryStore`): restart logs