🐛(frontend) harden speaker test against missing sinks and play errors

- 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.
This commit is contained in:
lebaudantoine
2026-08-12 18:45:05 +02:00
parent fda2508a6e
commit 2c1a7e1a53
4 changed files with 26 additions and 18 deletions
+1
View File
@@ -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
+23 -16
View File
@@ -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<HTMLAudioElement>(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 = () => {
<>
<Button
variant="secondaryText"
onPress={() => {
audioRef?.current?.play()
setIsPlaying(true)
onPress={async () => {
try {
await audioRef?.current?.play()
setIsPlaying(true)
} catch {
setIsPlaying(false)
}
}}
size="sm"
isDisabled={isPlaying}
@@ -48,7 +55,7 @@ export const SoundTester = () => {
{/* eslint-disable jsx-a11y/media-has-caption */}
<audio
ref={audioRef}
src="sounds/uprise.mp3"
src="/sounds/uprise.mp3"
onEnded={() => setIsPlaying(false)}
/>
</>
@@ -6,7 +6,7 @@ import type { NotificationType } from '@/features/notifications/NotificationType
// fixme - handle dynamic audio output changes
export const useNotificationSound = () => {
const notificationsSnap = useSnapshot(notificationsStore)
const [play] = useSound('./sounds/notifications.mp3', {
const [play] = useSound('/sounds/notifications.mp3', {
sprite: {
participantJoined: [0, 1150],
handRaised: [1400, 180],
@@ -126,7 +126,7 @@ export const OutputSoundTester = ({
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio
ref={audioRef}
src="sounds/uprise.mp3"
src="/sounds/uprise.mp3"
onEnded={() => setIsPlaying(false)}
/>
</StyledContainer>