feat(calls): audio output quick-switch in the call bar (#119)

Verified first: the embed hides Element Call's footer, so its in-call
settings sheet (which has the output picker) is unreachable from Lotus;
the cinny call bar had no output control.

Now a speaker button next to Deafen (desktop bar only; hidden where
setSinkId is unavailable — Firefox, Safari, Android Chrome) opens a menu
of enumerateDevices() audio outputs with the current one checked; picking
one sends io.lotus.set_audio_output to the fork (≥ 0.25.0-lotus.11),
which selects it, and the choice is re-sent with the rest of the sticky
fork state after an EC remount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 15:31:47 -04:00
co-authored by Claude Opus 5
parent ad1cbcf792
commit 98c80f36cb
3 changed files with 173 additions and 0 deletions
+19
View File
@@ -191,6 +191,7 @@ export class CallControl extends EventEmitter implements CallControlState {
public resendForkState(): void {
this.sendDeafenState();
this.sendQuality();
if (this._audioOutputId !== undefined) this.sendAudioOutput(this._audioOutputId);
// [Gitea #17] The pin lives fork-side and is dropped on a handler remount.
if (this._focusedUserId !== null) this.sendFocus(this._focusedUserId, this._focusedMediaId);
}
@@ -277,6 +278,24 @@ export class CallControl extends EventEmitter implements CallControlState {
// P6-2: send deafen state to the fork (io.lotus.set_deafen). Join-gated: the
// fork's handler only exists once joined; onCallJoined() re-sends the current
// state so a pre-join deafen is not lost.
// [Gitea #119] Output device (headset ↔ speakers) chosen from the host's
// call bar; the fork applies it with mediaDevices.audioOutput.select().
private _audioOutputId: string | undefined;
public get audioOutputId(): string | undefined {
return this._audioOutputId;
}
public setAudioOutput(deviceId: string): void {
this._audioOutputId = deviceId;
this.sendAudioOutput(deviceId);
}
private sendAudioOutput(deviceId: string): void {
if (!this.joined) return;
this.call.transport.send('io.lotus.set_audio_output', { deviceId }).catch(() => undefined);
}
private sendDeafenState(): void {
if (!this.joined) return;
this.call.transport