Deafen via per-participant setVolume is undone by EC volume controls and clobbers tile volumes on undeafen #1

Closed
opened 2026-09-12 02:12:56 -04:00 by jared · 1 comment
Owner

Severity: high · Type: bug · Confidence: high

Location: src/lotus/lotusDeafen.ts:51-56, src/state/media/RemoteUserMediaViewModel.ts:53, src/state/VolumeControls.ts:80-90
src/lotus/lotusDeafen.ts:51-56
src/lotus/lotusDeafen.ts:75-95, src/state/CallViewModel/CallViewModel.ts:643-684

Problem

lotusDeafen implements deafen by calling RemoteParticipant.setVolume(0, Track.Source.Microphone). Upstream EC uses the same API for its per-participant volume slider: createVolumeControls subscribes combineLatest([sink$, playbackVolume$]) and calls p.setVolume(volume) (default source = Microphone) every time either input emits. sink$ is derived from participant$, so it re-emits whenever a member's RemoteParticipant object appears or is replaced — i.e. on every join, and on participant re-resolution after a reconnect. The default playbackVolume$ is 1, so EC writes setVolume(1) after lotusDeafen wrote 0, and the LiveKit volumeMap keeps the last writer. Deafen therefore does not hold for anyone who joins (or reconnects) while you are deafened — exactly the case the module's docstring claims to cover via ParticipantConnected.

Second symptom (undeafen): applyToParticipant unconditionally writes 1 when not deafened, for both Microphone and ScreenShareAudio. EC exposes a per-participant volume slider and a per-tile "mute this participant" toggle backed by VolumeControls (playbackVolume$, togglePlaybackMuted). A deafen/undeafen cycle silently resets all of those to full volume, while playbackVolume$ — and therefore the slider position and the muted icon in GridTile/SpotlightTile — still shows the old value. The UI and the actual audio diverge with no way to resync short of nudging the slider.

Third symptom (first joiner audible while deafened): The module keys its RoomEvent.ParticipantConnected listeners off vm.livekitRoomItems$, which only surfaces a room once at least one matrix-validated, non-local member has a resolved LiveKit participant (CallViewModel.ts:648-654 returns null for local/unresolved). While you are alone in the call, rooms is empty and no listener is attached, so the very first joiner is covered only by the applyToRoom(room) call that runs on the subsequent stream emit — which lands after matrix membership resolution, i.e. potentially after their audio track has already subscribed and started playing through MatrixAudioRenderer. The sibling lotus modules (lotusDenoise.ts:116, lotusQuality.ts:87, lotusAudioInject.ts:46) all explicitly avoid livekitRoomItems$ for this reason; deafen is the one that still uses it.

How to trigger

Deafen in cinny, then have a second user join the call (or force a LiveKit reconnect for an existing remote). Their voice becomes audible while the cinny UI still shows deafened.

Also: Turn a participant down to 20% (or mute them) on their tile, then deafen and undeafen. They come back at 100% with the tile still showing 20% / muted.

Also: Deafen while alone in a call, then have someone join and speak immediately. A short burst of their audio is audible.

Suggested fix

Stop fighting setVolume; drive EC's existing global mute instead — setAudioEnabled$.next(false) feeds muteAllAudio$ (src/state/MuteAllAudioModel.ts:17), which InCallView already passes as muted to every LivekitRoomAudioRenderer plus the event/reaction renderers. That also covers Track.Source.Unknown soundboard audio (the known P6-2 divergence) without any setVolume conflict.

Also: Same as above — implement deafen as a global output mute rather than per-participant volume writes, so EC's per-participant state is left untouched.

Also: Switch to vm.allConnections$ like the sibling modules so the room (and its ParticipantConnected listener) exists from connect time; the global-mute fix above removes the problem entirely.


Filed from the September 2026 audit (branch lotus).

**Severity:** high · **Type:** bug · **Confidence:** high **Location:** `src/lotus/lotusDeafen.ts:51-56`, `src/state/media/RemoteUserMediaViewModel.ts:53`, `src/state/VolumeControls.ts:80-90` `src/lotus/lotusDeafen.ts:51-56` `src/lotus/lotusDeafen.ts:75-95`, `src/state/CallViewModel/CallViewModel.ts:643-684` ### Problem `lotusDeafen` implements deafen by calling `RemoteParticipant.setVolume(0, Track.Source.Microphone)`. Upstream EC uses the *same* API for its per-participant volume slider: `createVolumeControls` subscribes `combineLatest([sink$, playbackVolume$])` and calls `p.setVolume(volume)` (default source = `Microphone`) every time either input emits. `sink$` is derived from `participant$`, so it re-emits whenever a member's `RemoteParticipant` object appears or is replaced — i.e. on every join, and on participant re-resolution after a reconnect. The default `playbackVolume$` is `1`, so EC writes `setVolume(1)` *after* lotusDeafen wrote `0`, and the LiveKit `volumeMap` keeps the last writer. Deafen therefore does not hold for anyone who joins (or reconnects) while you are deafened — exactly the case the module's docstring claims to cover via `ParticipantConnected`. **Second symptom (undeafen):** `applyToParticipant` unconditionally writes `1` when not deafened, for both `Microphone` and `ScreenShareAudio`. EC exposes a per-participant volume slider and a per-tile "mute this participant" toggle backed by `VolumeControls` (`playbackVolume$`, `togglePlaybackMuted`). A deafen/undeafen cycle silently resets all of those to full volume, while `playbackVolume$` — and therefore the slider position and the muted icon in `GridTile`/`SpotlightTile` — still shows the old value. The UI and the actual audio diverge with no way to resync short of nudging the slider. **Third symptom (first joiner audible while deafened):** The module keys its `RoomEvent.ParticipantConnected` listeners off `vm.livekitRoomItems$`, which only surfaces a room once at least one *matrix-validated, non-local* member has a resolved LiveKit participant (`CallViewModel.ts:648-654` returns `null` for local/unresolved). While you are alone in the call, `rooms` is empty and no listener is attached, so the very first joiner is covered only by the `applyToRoom(room)` call that runs on the subsequent stream emit — which lands after matrix membership resolution, i.e. potentially after their audio track has already subscribed and started playing through `MatrixAudioRenderer`. The sibling lotus modules (`lotusDenoise.ts:116`, `lotusQuality.ts:87`, `lotusAudioInject.ts:46`) all explicitly avoid `livekitRoomItems$` for this reason; deafen is the one that still uses it. ### How to trigger Deafen in cinny, then have a second user join the call (or force a LiveKit reconnect for an existing remote). Their voice becomes audible while the cinny UI still shows deafened. Also: Turn a participant down to 20% (or mute them) on their tile, then deafen and undeafen. They come back at 100% with the tile still showing 20% / muted. Also: Deafen while alone in a call, then have someone join and speak immediately. A short burst of their audio is audible. ### Suggested fix Stop fighting `setVolume`; drive EC's existing global mute instead — `setAudioEnabled$.next(false)` feeds `muteAllAudio$` (`src/state/MuteAllAudioModel.ts:17`), which `InCallView` already passes as `muted` to every `LivekitRoomAudioRenderer` plus the event/reaction renderers. That also covers `Track.Source.Unknown` soundboard audio (the known P6-2 divergence) without any `setVolume` conflict. Also: Same as above — implement deafen as a global output mute rather than per-participant volume writes, so EC's per-participant state is left untouched. Also: Switch to `vm.allConnections$` like the sibling modules so the room (and its `ParticipantConnected` listener) exists from connect time; the global-mute fix above removes the problem entirely. --- _Filed from the September 2026 audit (branch `lotus`)._
jared added this to the EC fork audit 2026-09 · High milestone 2026-09-12 02:12:56 -04:00
jared added the bugpriority: higharea: deafen labels 2026-09-12 02:12:56 -04:00
jared self-assigned this 2026-09-12 02:12:56 -04:00
jared closed this issue 2026-09-12 11:56:45 -04:00
Author
Owner

Fixed in f1cfcc73, shipped in 0.25.0-lotus.1 (published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.

Fixed in f1cfcc73, shipped in `0.25.0-lotus.1` (published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
2026-10-14
Dependencies

No dependencies set.

Reference: LotusGuild/element-call#1