Call-bar audio output switch can't select a specific device: the host's deviceIds mean nothing inside the call iframe #310

Closed
opened 2026-10-05 00:17:39 -04:00 by jared · 1 comment
Owner

The speaker button in the call bar (#119, 98c80f36, src/app/features/call-status/AudioOutputButton.tsx) works like this:

  1. It enumerates audio outputs in the chat page.
  2. It sends the chosen deviceId to the Element Call fork as io.lotus.set_audio_output.
  3. The fork calls mediaDevices.audioOutput.select(deviceId) (element-call src/lotus/lotusAudioOutput.ts).

Browsers hash deviceId per frame/origin, so an id from the host is unknown inside the call iframe. The call page now has its own origin (7d7a379c), so this is guaranteed on prod. Only the special default id is shared.

Measured (Chromium, fake devices, in a voice room). In this local run the iframe gets different ids even on the same origin:

host outputs:  Fake Default Audio Output=default, Fake Audio Output 1=5842acf8dfca, Fake Audio Output 2=91fc192055a3
frame outputs: Fake Default Audio Output=default, Fake Audio Output 1=b8337718aab7, Fake Audio Output 2=03ad4b4234a0

Then I picked Fake Audio Output 2 in the call-bar menu:

  • Element Call saved the host's id: localStorage["matrix-setting-audio-output"] = "91fc192055a36c4a60a…". That id isn't in its device list, so selectDevice$ falls back to the default output. Audio doesn't move.
  • The host menu now shows "Fake Audio Output 2 ✓", so the UI claims a switch that didn't happen.
  • The fork replies with io.lotus.audio_output_state. The host doesn't handle that action, and the fork logs [lotus] host did not ack io.lotus.audio_output_state … Unknown or unsupported from-widget action on every change.
  • The junk id stays in Element Call's storage for later calls.

Choosing Default works. Picking a headset or speakers by name doesn't, which is the whole point of the button. It affects Chrome, Edge and the Windows desktop app (WebView2). Firefox, Safari, Android and the Linux desktop hide the button because they lack setSinkId.

Fix:

  • Let the fork own the list. io.lotus.audio_output_state already exists, and its doc comment promises available: [{id, label}], but the code only sends deviceId. Send the iframe's own ids and labels.
  • On the host, handle and ack that action, build the menu from it, and send back the fork's id.
  • Alternatively, as a smaller fix, send the label and have the fork match by label: labels are the same in every frame once mic permission is granted.
  • Show the ✓ from the fork's reported selected state, not from local React state.
  • Add a test: pick a non-default output → the fork's selected$ id is the frame's id for that label.

(Checked at the same time: the mic level meter on the mute button animates with input: heights 3,3,5,7 → 3 px.)


Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

The speaker button in the call bar (#119, 98c80f36, `src/app/features/call-status/AudioOutputButton.tsx`) works like this: 1. It enumerates audio outputs **in the chat page**. 2. It sends the chosen `deviceId` to the Element Call fork as `io.lotus.set_audio_output`. 3. The fork calls `mediaDevices.audioOutput.select(deviceId)` (element-call `src/lotus/lotusAudioOutput.ts`). Browsers hash `deviceId` per frame/origin, so an id from the host is unknown inside the call iframe. The call page now has its own origin (7d7a379c), so this is guaranteed on prod. Only the special `default` id is shared. **Measured** (Chromium, fake devices, in a voice room). In this local run the iframe gets different ids even on the same origin: ``` host outputs: Fake Default Audio Output=default, Fake Audio Output 1=5842acf8dfca, Fake Audio Output 2=91fc192055a3 frame outputs: Fake Default Audio Output=default, Fake Audio Output 1=b8337718aab7, Fake Audio Output 2=03ad4b4234a0 ``` Then I picked **Fake Audio Output 2** in the call-bar menu: - Element Call saved the host's id: `localStorage["matrix-setting-audio-output"] = "91fc192055a36c4a60a…"`. That id isn't in its device list, so `selectDevice$` falls back to the default output. Audio doesn't move. - The host menu now shows **"Fake Audio Output 2 ✓"**, so the UI claims a switch that didn't happen. - The fork replies with `io.lotus.audio_output_state`. The host doesn't handle that action, and the fork logs `[lotus] host did not ack io.lotus.audio_output_state … Unknown or unsupported from-widget action` on every change. - The junk id stays in Element Call's storage for later calls. Choosing **Default** works. Picking a headset or speakers by name doesn't, which is the whole point of the button. It affects Chrome, Edge and the Windows desktop app (WebView2). Firefox, Safari, Android and the Linux desktop hide the button because they lack `setSinkId`. **Fix:** - Let the fork own the list. `io.lotus.audio_output_state` already exists, and its doc comment promises `available: [{id, label}]`, but the code only sends `deviceId`. Send the iframe's own ids and labels. - On the host, handle and ack that action, build the menu from it, and send back the fork's id. - Alternatively, as a smaller fix, send the label and have the fork match by label: labels are the same in every frame once mic permission is granted. - Show the ✓ from the fork's reported `selected` state, not from local React state. - Add a test: pick a non-default output → the fork's `selected$` id is the frame's id for that label. (Checked at the same time: the mic level meter on the mute button animates with input: heights 3,3,5,7 → 3 px.) --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the bugpriority: mediumarea: calls labels 2026-10-05 00:17:39 -04:00
Author
Owner

The fork-side half is tracked in LotusGuild/element-call#44 (device list in audio_output_state; don't persist unknown ids).

The fork-side half is tracked in LotusGuild/element-call#44 (device list in `audio_output_state`; don't persist unknown ids).
jared closed this issue 2026-10-19 01:13:20 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#310