From 52f119db02ac429a1346861f2ae30b8b5e034e87 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 12 Aug 2026 18:45:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20harden=20speaker=20tes?= =?UTF-8?q?t=20against=20missing=20sinks=20and=20play=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Only call `setSinkId` when supported and the device is actually enumerated: LiveKit can fall back to a stale id on browsers (e.g. WebKit) that expose no such device, making `setSinkId` throw `NotFoundError`. - Await `audio.play()` and reset the playing state on failure, to avoid a stuck button and an unhandled rejection. - Use an absolute `/sounds/uprise.mp3` URL so the asset resolves regardless of the current SPA route. --- CHANGELOG.md | 1 + src/frontend/src/components/SoundTester.tsx | 39 +++++++++++-------- .../hooks/useSoundNotification.tsx | 2 +- .../controls/Device/OutputSoundTester.tsx | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f48fb87..5471cedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to - 📈(frontend) downgrade unreachable external home URL from error to event - 🐛(frontend) handle 401 responses when syncing user preferences +- 🐛(frontend) harden speaker test against missing sinks and play errors ## [1.26.0] - 2026-08-12 diff --git a/src/frontend/src/components/SoundTester.tsx b/src/frontend/src/components/SoundTester.tsx index 5080a1a4..7faa0845 100644 --- a/src/frontend/src/components/SoundTester.tsx +++ b/src/frontend/src/components/SoundTester.tsx @@ -3,27 +3,30 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useMediaDeviceSelect } from '@livekit/components-react' import { reportError } from '@/features/analytics/telemetry' +import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput' export const SoundTester = () => { const { t } = useTranslation('settings') const [isPlaying, setIsPlaying] = useState(false) const audioRef = useRef(null) - const { activeDeviceId } = useMediaDeviceSelect({ kind: 'audiooutput' }) + const { devices, activeDeviceId } = useMediaDeviceSelect({ + kind: 'audiooutput', + }) useEffect(() => { - const updateActiveId = async (deviceId: string) => { - try { - await audioRef?.current?.setSinkId(deviceId) - } catch (error) { - reportError( - 'device_switch_failure', - new Error(`Error setting sinkId: ${error}`) - ) + if (!canTestAudioOutput() || !activeDeviceId) return + if (!devices.some((device) => device.deviceId === activeDeviceId)) return + audioRef.current?.setSinkId(activeDeviceId).catch((error) => { + if (error instanceof DOMException && error.name === 'NotFoundError') { + return } - } - updateActiveId(activeDeviceId) - }, [activeDeviceId]) + reportError( + 'device_switch_failure', + new Error(`Error setting sinkId: ${error}`) + ) + }) + }, [devices, activeDeviceId]) // prevent pausing the sound navigator.mediaSession.setActionHandler('pause', function () {}) @@ -32,9 +35,13 @@ export const SoundTester = () => { <>