mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-12 19:56:53 +00:00
✨(frontend) add a sound tester to the output select menu
Add a sound tester next to the selected output device in the speaker select menu, so users can play a test sound and confirm they picked the right speaker. Inspired by the microphone gauge added previously, and requested by users.
This commit is contained in:
committed by
aleb_the_flash
parent
b780d2845a
commit
8f27b89d21
@@ -12,6 +12,7 @@ and this project adheres to
|
||||
|
||||
- 📈(frontend) capture media diagnostics on media errors
|
||||
- ✨(frontend) add an audio gauge to the microphone select menu
|
||||
- ✨(frontend) add a sound tester to the output select menu
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiVolumeUpLine } from '@remixicon/react'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { Button } from '@/primitives'
|
||||
import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput'
|
||||
|
||||
// Speaker test in the audiooutput menu footer (Meet-style UX). Outputs have
|
||||
// no track: the test plays a bundled file through the selected sink, and
|
||||
// following `sinkId` mid-playback re-routes it live. No permission involved.
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
|
||||
const BUTTON_VARIANT = {
|
||||
light: 'quaternaryText',
|
||||
dark: 'primaryTextDark',
|
||||
} as const
|
||||
|
||||
const StyledContainer = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
marginTop: '0.5rem',
|
||||
borderTop: '1px solid',
|
||||
},
|
||||
variants: {
|
||||
theme: {
|
||||
light: {
|
||||
borderColor: 'gray.200',
|
||||
},
|
||||
dark: {
|
||||
borderColor: 'primaryDark.300',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledButtonContent = styled('span', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 'full',
|
||||
paddingX: '1.625rem',
|
||||
'& > svg': {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type OutputSoundTesterProps = {
|
||||
/** The device the test should play through (the select's current key). */
|
||||
sinkId?: string
|
||||
variant?: Theme
|
||||
}
|
||||
|
||||
export const OutputSoundTester = ({
|
||||
sinkId,
|
||||
variant = 'light',
|
||||
}: OutputSoundTesterProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||
const audioRef = useRef<HTMLAudioElement>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
|
||||
const latestSinkIdRef = useRef(sinkId)
|
||||
latestSinkIdRef.current = sinkId
|
||||
|
||||
const stopPlayback = useCallback(() => {
|
||||
const audio = audioRef.current
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
audio.currentTime = 0
|
||||
}
|
||||
setIsPlaying(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sinkId || !canTestAudioOutput()) return
|
||||
audioRef.current?.setSinkId(sinkId).catch(() => {
|
||||
// Re-routing failed (stale or unplugged device): stop the test rather
|
||||
// than keep playing through the previous sink.
|
||||
if (latestSinkIdRef.current === sinkId) {
|
||||
stopPlayback()
|
||||
}
|
||||
})
|
||||
}, [sinkId, stopPlayback])
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
return () => audio?.pause()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StyledContainer theme={variant}>
|
||||
<Button
|
||||
variant={BUTTON_VARIANT[variant]}
|
||||
size="sm"
|
||||
fullWidth
|
||||
isDisabled={isPlaying}
|
||||
onPress={async () => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
try {
|
||||
// Confirm routing before starting: a no-op when already routed,
|
||||
// but rejects on a stale device id, so the test never plays
|
||||
// through the wrong sink.
|
||||
if (sinkId && canTestAudioOutput()) {
|
||||
await audio.setSinkId(sinkId)
|
||||
}
|
||||
await audio.play()
|
||||
setIsPlaying(true)
|
||||
} catch {
|
||||
stopPlayback()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledButtonContent>
|
||||
<RiVolumeUpLine size={18} aria-hidden />
|
||||
{isPlaying ? t('audiooutput.testing') : t('audiooutput.test')}
|
||||
</StyledButtonContent>
|
||||
</Button>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src="sounds/uprise.mp3"
|
||||
onEnded={() => setIsPlaying(false)}
|
||||
/>
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
@@ -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 = <T extends string | number>({
|
||||
menuFooter={
|
||||
kind === 'audioinput' ? (
|
||||
<AudioLevelGauge track={track} variant={props.variant} />
|
||||
) : kind === 'audiooutput' && canTestAudioOutput() ? (
|
||||
<OutputSoundTester
|
||||
sinkId={selectedKey as string}
|
||||
variant={props.variant}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const canTestAudioOutput = () =>
|
||||
typeof HTMLMediaElement !== 'undefined' &&
|
||||
'setSinkId' in HTMLMediaElement.prototype // Safari: no output routing
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user