diff --git a/Cargo.toml b/Cargo.toml index eaffbed..ab685fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,8 @@ web-sys = { version = "0.3", features = [ "RtcIceCandidateInit", "RtcPeerConnectionIceEvent", "RtcRtpSender", + "RtcPeerConnectionState", + "DisplayMediaStreamConstraints", "RtcTrackEvent", "RtcRtpTransceiver", "RtcOfferOptions", diff --git a/README.md b/README.md index 39703d7..b4abeb2 100644 --- a/README.md +++ b/README.md @@ -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.` via `/call-sse/` 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 diff --git a/src/app.rs b/src/app.rs index 23783e4..fba2ba7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -422,7 +422,6 @@ fn CallPanel(room: Memo, me: String) -> impl IntoView { let call_state = StoredValue::new_local(CallState::new(room.get_untracked(), me.clone())); let in_call = call_state.get_value().in_call; - let local_video_ref = NodeRef::::new(); let es_handle = StoredValue::new_local(None::); Effect::new(move |_| { @@ -448,54 +447,22 @@ fn CallPanel(room: Memo, me: String) -> impl IntoView { } }); - // Mirror the local MediaStream into the preview