fix(lotus): revert to the default output sink when the selected device disappears

Fixes #27

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-13 01:22:20 -04:00
co-authored by Claude Opus 5
parent 68eafcb9a8
commit c7494e68ca
+22 -4
View File
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { import {
@@ -172,13 +172,22 @@ export function useAudioContext<S extends string>(
useMediaDevices().audioOutput.selected$, useMediaDevices().audioOutput.selected$,
)?.id; )?.id;
const { controlledAudioDevices } = useUrlParams(); const { controlledAudioDevices } = useUrlParams();
// [lotus] Tracks whether audioOutputId has ever resolved to a real device,
// so we can tell "MediaDevices hasn't resolved yet" (skip) apart from "the
// previously selected output device just disappeared" (revert to default,
// #27) — both present as audioOutputId === undefined.
const outputHasResolved = useRef(false);
// Update the sink ID whenever we change devices. // Update the sink ID whenever we change devices.
useEffect(() => { useEffect(() => {
if (typeof audioOutputId === "string") outputHasResolved.current = true;
if (
!audioContext ||
!("setSinkId" in audioContext) ||
controlledAudioDevices
)
return;
if ( if (
audioContext &&
"setSinkId" in audioContext &&
!controlledAudioDevices &&
// Skip until a device is actually selected. audioOutputId is undefined // Skip until a device is actually selected. audioOutputId is undefined
// before MediaDevices resolves (e.g. on the Tauri desktop webview, where // before MediaDevices resolves (e.g. on the Tauri desktop webview, where
// the selected$ observable emits undefined first); setSinkId(undefined) // the selected$ observable emits undefined first); setSinkId(undefined)
@@ -191,6 +200,15 @@ export function useAudioContext<S extends string>(
audioContext.setSinkId(audioOutputId).catch((ex) => { audioContext.setSinkId(audioOutputId).catch((ex) => {
logger.warn("Unable to change sink for audio context", ex); logger.warn("Unable to change sink for audio context", ex);
}); });
} else if (outputHasResolved.current) {
// [lotus] The previously selected output device disappeared (selected$
// re-emits undefined once devices have already resolved), not the
// pre-resolution case above. Revert to the default sink instead of
// leaving the context pinned to a now-nonexistent device (#27).
// @ts-expect-error - setSinkId doesn't exist yet in types, maybe because it's not supported everywhere.
audioContext.setSinkId("").catch((ex) => {
logger.warn("Unable to revert sink for audio context", ex);
});
} }
}, [audioContext, audioOutputId, controlledAudioDevices]); }, [audioContext, audioOutputId, controlledAudioDevices]);
const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig(); const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig();