From c7494e68cafe62cfdc718dd38eac7ef1fc7a184a Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sun, 13 Sep 2026 01:22:20 -0400 Subject: [PATCH] fix(lotus): revert to the default output sink when the selected device disappears Fixes #27 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/useAudioContext.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index c2e3a941..5f22ff70 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ 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 { @@ -172,13 +172,22 @@ export function useAudioContext( useMediaDevices().audioOutput.selected$, )?.id; 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. useEffect(() => { + if (typeof audioOutputId === "string") outputHasResolved.current = true; + if ( + !audioContext || + !("setSinkId" in audioContext) || + controlledAudioDevices + ) + return; if ( - audioContext && - "setSinkId" in audioContext && - !controlledAudioDevices && // Skip until a device is actually selected. audioOutputId is undefined // before MediaDevices resolves (e.g. on the Tauri desktop webview, where // the selected$ observable emits undefined first); setSinkId(undefined) @@ -191,6 +200,15 @@ export function useAudioContext( audioContext.setSinkId(audioOutputId).catch((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]); const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig();