diff --git a/src/frontend/src/features/devices/index.ts b/src/frontend/src/features/devices/index.ts index 3a4db185..a4ea34cb 100644 --- a/src/frontend/src/features/devices/index.ts +++ b/src/frontend/src/features/devices/index.ts @@ -1 +1,2 @@ +export { useAudioOutputs } from './utils/useAudioOutputs' export { usePersistedMediaDeviceSelect } from './utils/usePersistedMediaDeviceSelect' diff --git a/src/frontend/src/features/devices/utils/useAudioOutputs.ts b/src/frontend/src/features/devices/utils/useAudioOutputs.ts new file mode 100644 index 00000000..99319669 --- /dev/null +++ b/src/frontend/src/features/devices/utils/useAudioOutputs.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react' + +const getOutputDevices = () => { + return navigator.mediaDevices + .getUserMedia({ audio: true, video: false }) + .then(() => navigator.mediaDevices.enumerateDevices()) + .then((devices) => devices.filter(({ kind }) => kind === 'audiooutput')) +} + +/** + * custom hook to fetch audio outputs + * + * this is used instead of livekit's useMediaDevices because the livekit integrated one seems to request + * outputs in a weird order, resulting in empty results in firefox if we didn't ask for input before + */ +export const useAudioOutputs = () => { + const [audioOutputs, setAudioOutputs] = useState([]) + useEffect(() => { + const retrieveOutputDevices = () => { + getOutputDevices() + .then(setAudioOutputs) + .catch((error) => { + console.error('Audio outputs retrieval error :', error) + }) + } + retrieveOutputDevices() + const onDeviceChange = () => { + retrieveOutputDevices() + } + navigator?.mediaDevices?.addEventListener('devicechange', onDeviceChange) + return () => { + navigator.mediaDevices.removeEventListener('devicechange', onDeviceChange) + } + }, []) + return audioOutputs +} diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 126aa403..1110ff0c 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -73,9 +73,16 @@ export const Conference = ({ audioCaptureDefaults: { deviceId: userConfig.devices.micDeviceId ?? undefined, }, + audioOutput: { + deviceId: userConfig.devices.speakerDeviceId ?? undefined, + }, } // do not rely on the userConfig object directly as its reference may change on every render - }, [userConfig.devices.cameraDeviceId, userConfig.devices.micDeviceId]) + }, [ + userConfig.devices.cameraDeviceId, + userConfig.devices.micDeviceId, + userConfig.devices.speakerDeviceId, + ]) const room = useMemo(() => new Room(roomOptions), [roomOptions]) diff --git a/src/frontend/src/features/rooms/components/HomemadeJoin.tsx b/src/frontend/src/features/rooms/components/HomemadeJoin.tsx index db5e0e4f..cff67843 100644 --- a/src/frontend/src/features/rooms/components/HomemadeJoin.tsx +++ b/src/frontend/src/features/rooms/components/HomemadeJoin.tsx @@ -27,7 +27,10 @@ import { Track, LocalVideoTrack, LocalAudioTrack } from 'livekit-client' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSnapshot } from 'valtio' -import { usePersistedMediaDeviceSelect } from '@/features/devices' +import { + usePersistedMediaDeviceSelect, + useAudioOutputs, +} from '@/features/devices' import { settingsStore, type SettingsState } from '@/features/settings' import { css } from '@/styled-system/css' @@ -102,6 +105,7 @@ export const HomemadeJoin = ({ track: videoTrack, requestPermissions: true, }) + const speakerDevices = useAudioOutputs() useEffect(() => { if (settingsStore.devices.micDeviceId) { @@ -171,6 +175,27 @@ export const HomemadeJoin = ({ )} + {/* audio output dropdown */} + + + ({ + value: d.deviceId, + label: d.label, + }))} + selectedItem={settingsSnap.devices.speakerDeviceId} + onAction={(value) => { + settingsStore.devices.speakerDeviceId = value as string + }} + /> + + {/* audio input toggle + dropdown */} ( : { username: undefined, devices: { + speakerDeviceId: undefined, micDeviceId: undefined, cameraDeviceId: undefined, enableMic: false, diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 4bfe63d6..fe57f7aa 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -10,6 +10,7 @@ "camlabel": "", "chooseCamera": "", "chooseMic": "", + "chooseSpeaker": "", "heading": "", "joinLabel": "", "joinMeeting": "", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 9ca31e3e..eaa058e7 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -10,6 +10,7 @@ "camlabel": "", "chooseCamera": "Select camera", "chooseMic": "Select microphone", + "chooseSpeaker": "Select speakers", "heading": "Verify your settings", "joinLabel": "", "joinMeeting": "Join meeting", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 785c8175..a9a9abe7 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -10,6 +10,7 @@ "camlabel": "", "chooseCamera": "Choisir la webcam", "chooseMic": "Choisir le micro", + "chooseSpeaker": "Choisir la sortie audio", "heading": "Vérifiez vos paramètres", "joinLabel": "", "joinMeeting": "Rejoindre la réjoindre",