From 38530facba4dd57bca47730e709a4eeaa86dd46e Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 9 Aug 2026 15:19:43 +0200 Subject: [PATCH] wip soundtester --- .../controls/Device/OutputSoundTester.tsx | 90 +++++++++++++++++++ .../controls/Device/SelectDevice.tsx | 7 ++ .../rooms/utils/canTestAudioOutput.ts | 3 + src/frontend/src/locales/de/rooms.json | 5 +- src/frontend/src/locales/en/rooms.json | 4 +- src/frontend/src/locales/fr/rooms.json | 4 +- src/frontend/src/locales/nl/rooms.json | 4 +- 7 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Device/OutputSoundTester.tsx create mode 100644 src/frontend/src/features/rooms/utils/canTestAudioOutput.ts diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/OutputSoundTester.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/OutputSoundTester.tsx new file mode 100644 index 00000000..088a9876 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/OutputSoundTester.tsx @@ -0,0 +1,90 @@ +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiVolumeUpLine } from '@remixicon/react' +import { css } from '@/styled-system/css' +import { reportError } from '@/features/analytics/telemetry' +import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput' + +/** + * Speaker test rendered as the audiooutput menu footer — sibling of + * AudioLevelGauge for audioinput (Google Meet UX: the test lives inside + * the device dropdown, the popover stays open while it plays). + * + * No track exists for outputs: the test IS playing a bundled file through + * the selected sink via setSinkId (same mechanism as the settings + * SoundTester). Needs no permission, so the never-auto-prompt policy + * holds. Following `sinkId` mid-playback re-routes the sound live, which + * makes A/B-ing speakers work. + */ + +type OutputSoundTesterProps = { + /** The device the test should play through (the select's current key). */ + sinkId?: string + variant?: 'light' | 'dark' +} + +export const OutputSoundTester = ({ + sinkId, + variant = 'light', +}: OutputSoundTesterProps) => { + const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) + const audioRef = useRef(null) + const [isPlaying, setIsPlaying] = useState(false) + + useEffect(() => { + if (!sinkId || !canTestAudioOutput()) return + audioRef.current?.setSinkId(sinkId).catch((error) => { + reportError('device_switch_failure', error, { + kind: 'audiooutput', + context: 'test sound setSinkId', + }) + }) + }, [sinkId]) + + return ( +
+ + {/* eslint-disable-next-line jsx-a11y/media-has-caption */} +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx index b1b2a672..9e31217e 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx @@ -7,6 +7,8 @@ import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice' import { useDeviceIcons } from '@/features/rooms/livekit/hooks/useDeviceIcons' import type { LocalAudioTrack } from 'livekit-client' import { AudioLevelGauge } from './AudioLevelGauge' +import { OutputSoundTester } from './OutputSoundTester' +import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput' type DeviceItems = Array<{ value: string; label: string }> @@ -81,6 +83,11 @@ const SelectDevicePermissions = ({ menuFooter={ kind === 'audioinput' ? ( + ) : kind === 'audiooutput' && canTestAudioOutput() ? ( + ) : undefined } {...props} diff --git a/src/frontend/src/features/rooms/utils/canTestAudioOutput.ts b/src/frontend/src/features/rooms/utils/canTestAudioOutput.ts new file mode 100644 index 00000000..f44efa2d --- /dev/null +++ b/src/frontend/src/features/rooms/utils/canTestAudioOutput.ts @@ -0,0 +1,3 @@ +export const canTestAudioOutput = () => + typeof HTMLMediaElement !== 'undefined' && + 'setSinkId' in HTMLMediaElement.prototype // Safari: no output routing diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 2d27256f..23184cb5 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -40,7 +40,9 @@ }, "audiooutput": { "choose": "Audioausgabe auswählen", - "permissionsNeeded": "Audioausgabe auswählen – Berechtigung erforderlich" + "permissionsNeeded": "Audioausgabe auswählen – Berechtigung erforderlich", + "test": "Lautsprecher testen", + "testing": "Testton wird abgespielt…" } }, "join": { @@ -328,7 +330,6 @@ } } }, - "faceLandmarks": { "title": "Visuelle Effekte", "glasses": { diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 21a13865..a798fff0 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -40,7 +40,9 @@ }, "audiooutput": { "choose": "Select speaker", - "permissionsNeeded": "Select speaker - permission needed" + "permissionsNeeded": "Select speaker - permission needed", + "test": "Test speakers", + "testing": "Playing test sound…" } }, "join": { diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 00519708..173feff2 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -40,7 +40,9 @@ }, "audiooutput": { "choose": "Choisir le haut-parleur", - "permissionsNeeded": "Choisir le haut-parleur - autorisations nécessaires" + "permissionsNeeded": "Choisir le haut-parleur - autorisations nécessaires", + "test": "Tester les haut-parleurs", + "testing": "Lecture du son de test…" } }, "join": { diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index 744d7384..cf519a5c 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -40,7 +40,9 @@ }, "audiooutput": { "choose": "Selecteer luidspreker", - "permissionsNeeded": "Selecteer luidspreker - Toestemming vereist" + "permissionsNeeded": "Selecteer luidspreker - Toestemming vereist", + "test": "Luidsprekers testen", + "testing": "Testgeluid wordt afgespeeld…" } }, "join": {