feat(lotus): io.lotus.set_audio_output — host-driven output device switch (#119)
The embed hides Element Call's footer, so its own output picker is out of
reach; the host's call bar now sends io.lotus.set_audio_output { deviceId }
and the fork applies it via mediaDevices.audioOutput.select(), answering
with io.lotus.audio_output_state { deviceId } on every change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
b9d0ac7cac
commit
4a48ec98d0
@@ -26,6 +26,7 @@ describe("LotusWidgetActions", () => {
|
||||
LotusWidgetActions.SetQuality,
|
||||
LotusWidgetActions.Decorations,
|
||||
LotusWidgetActions.SetDeafen,
|
||||
LotusWidgetActions.SetAudioOutput,
|
||||
];
|
||||
|
||||
expect(new Set(LOTUS_TO_WIDGET_ACTIONS)).toEqual(new Set(expectedToWidget));
|
||||
|
||||
@@ -52,6 +52,10 @@ export enum LotusWidgetActions {
|
||||
* disconnect or in-call teardown, whichever comes first.
|
||||
*/
|
||||
CallSummary = "io.lotus.call_summary",
|
||||
/** toWidget: select the audio output device `{ deviceId }` (#119). */
|
||||
SetAudioOutput = "io.lotus.set_audio_output",
|
||||
/** fromWidget: the currently selected output `{ deviceId }` (#119). */
|
||||
AudioOutputState = "io.lotus.audio_output_state",
|
||||
}
|
||||
|
||||
/** toWidget Lotus actions that must be allow-listed in `initializeWidget`. */
|
||||
@@ -61,4 +65,5 @@ export const LOTUS_TO_WIDGET_ACTIONS: LotusWidgetActions[] = [
|
||||
LotusWidgetActions.SetQuality,
|
||||
LotusWidgetActions.Decorations,
|
||||
LotusWidgetActions.SetDeafen,
|
||||
LotusWidgetActions.SetAudioOutput,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2026 Lotus Guild
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { type MediaDevices } from "../state/MediaDevices";
|
||||
import { widget } from "../widget";
|
||||
import { LotusWidgetActions } from "./lotusActions";
|
||||
import { lotusSendToHost } from "./lotusWidget";
|
||||
|
||||
/**
|
||||
* [Gitea #119] Let the host switch the audio output device (headset ↔
|
||||
* speakers) from its own call bar: `io.lotus.set_audio_output { deviceId }`
|
||||
* selects the output; the fork answers each change (and the initial state)
|
||||
* with `io.lotus.audio_output_state { deviceId, available: [{id,label}] }`.
|
||||
* No effect unless the host sends the action. Returns a teardown function.
|
||||
*/
|
||||
export function startLotusAudioOutput(mediaDevices: MediaDevices): () => void {
|
||||
const w = widget;
|
||||
if (!w) return () => undefined;
|
||||
|
||||
const handler = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
w.api.transport.reply(ev.detail, {});
|
||||
const data = ev.detail.data as { deviceId?: string } | undefined;
|
||||
if (typeof data?.deviceId !== "string") return;
|
||||
logger.debug(`[lotus] set_audio_output: ${data.deviceId}`);
|
||||
mediaDevices.audioOutput.select(data.deviceId);
|
||||
};
|
||||
w.lazyActions.on(LotusWidgetActions.SetAudioOutput, handler);
|
||||
|
||||
const sub = mediaDevices.audioOutput.selected$.subscribe((selected) => {
|
||||
lotusSendToHost(LotusWidgetActions.AudioOutputState, {
|
||||
deviceId: selected?.id ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
w.lazyActions.off(LotusWidgetActions.SetAudioOutput, handler);
|
||||
};
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { startLotusQuality } from "../lotus/lotusQuality";
|
||||
import { startLotusDecorations } from "../lotus/lotusDecorations";
|
||||
import { startLotusDenoise } from "../lotus/lotusDenoise";
|
||||
import { startLotusCallSummary } from "../lotus/lotusCallSummary";
|
||||
import { startLotusAudioOutput } from "../lotus/lotusAudioOutput";
|
||||
import { startLotusDeafen } from "../lotus/lotusDeafen";
|
||||
import styles from "./InCallView.module.css";
|
||||
import { GridTile } from "../tile/GridTile";
|
||||
@@ -315,6 +316,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
// audio (and optionally screenshare audio) at the LiveKit source. No-op
|
||||
// unless the host sends the action.
|
||||
useEffect(() => startLotusDeafen(), []);
|
||||
// [lotus #119] Let the host pick the audio output device from its call bar.
|
||||
const lotusMediaDevices = useMediaDevices();
|
||||
useEffect(
|
||||
() => startLotusAudioOutput(lotusMediaDevices),
|
||||
[lotusMediaDevices],
|
||||
);
|
||||
|
||||
const fatalCallError = useBehavior(vm.fatalError$);
|
||||
// Stop the rendering and throw for the error boundary
|
||||
|
||||
Reference in New Issue
Block a user