feat(calls): propagate voice mute state over WS so peers see it (LC-407) #420
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-402-remote-mute"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements LC-407 (subtask of the LC-402 call redesign): propagate mute state over the chat WebSocket so a peer's tile shows when they mute, not just your own.
Why
After the redesign each tile has a mute indicator + active-speaker ring, but only the LOCAL mute was wired - a remote mute was invisible. Active-speaker for remotes already works (the analyser taps each peer's received audio and is suppressed while
data-mutedis set), so only the discrete mute flag needs to travel over the wire.How
ClientFrame::voice_mute { room_id, muted }. The handler checksis_in_voice_room(conn_id, room_id)and broadcastsChatEvent::VoiceMuteChanged { room_id, user_id, muted }to the room (same fan-out asVoiceJoined). Rendered to#lc-voice-busviavoice_event.htmlaskind="mute"withpayload"1"/"0". The recipient-independentrender_eventreturnsNonefor it (handled per recipient in the WS send task, like the other voice events).toggleMutesetsselfMuted, appliesdata-mutedlocally, and sendsvoice_mute. Inboundkind === 'mute'appliesdata-mutedon the peer's tile. A muted participant re-announces when someone new joins (broadcast reaches the newcomer too), so a late joiner sees the correct mic state.selfMutedresets on join/leave.Scope
ws/events.rs(1 variant),routes/ws.rs(frame + inbound handler + render arm + dispatch),views/ws_fragments.rs(None arm),assets/voice.js. No template, CSS (the badge + ring already exist), or schema change.Note
Active-speaker is intentionally NOT broadcast - it is derived locally from received audio every 250ms and would be needless WS spam to propagate. Only the infrequent mute toggle goes over the wire.
Tests
just check(standalone + saas compile, clippy, fmt),just test,just test-saasall pass.voice.jssyntax-checked.Until now only the LOCAL mute was reflected on a tile; a participant could not tell when another peer muted their mic. Add a lightweight room broadcast for the discrete mute flag (active-speaker is already detected locally from each peer's received audio, so it does not need wiring). - New ClientFrame `voice_mute { room_id, muted }`; the handler verifies the connection is in that voice room and broadcasts ChatEvent::VoiceMuteChanged to the room (like VoiceJoined), rendered to the voice bus as kind="mute" with payload "1"/"0". - voice.js broadcasts on every local mute toggle and applies an incoming mute to the peer's tile via data-muted (CSS draws the existing mute badge; pollSpeaking already suppresses the active-speaker ring while data-muted is true). - A muted participant re-announces on a late join, so a newcomer learns the current mic state instead of seeing an out-of-date tile. selfMuted resets on join/leave. Server: one new event variant + frame + render arm; the recipient-independent render_event returns None for it (handled per recipient in the WS send task). Frontend: voice.js only. No schema change. just check, just test, just test-saas all pass; voice.js syntax-checked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>