From 770730ba6cbde04ba4f8667ae161ba9223555ec5 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 19 Nov 2024 13:17:24 -0500 Subject: [PATCH] Don't fall back as eagerly to unselected devices Somewhere around version 131 or 132, Firefox has started being more paranoid about media device fingerprinting, and will not even give you the IDs of available devices until you've requested a media stream. Instead you only get a single audio input and video input each with the empty string as their device ID, representing the system's default device. We can recognize this case and avoid resetting the device selection. --- src/livekit/MediaDevicesContext.tsx | 61 ++++++++++++----------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 3d85b165c..a26cf722d 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -24,6 +24,7 @@ import { audioInput as audioInputSetting, audioOutput as audioOutputSetting, videoInput as videoInputSetting, + Setting, } from "../settings/settings"; import { isFirefox } from "../Platform"; @@ -58,7 +59,7 @@ function useObservableState( function useMediaDevice( kind: MediaDeviceKind, - fallbackDevice: string | undefined, + setting: Setting, usingNames: boolean, alwaysDefault: boolean = false, ): MediaDevice { @@ -84,15 +85,19 @@ function useMediaDevice( [kind, requestPermissions], ); const available = useObservableState(deviceObserver, []); - const [selectedId, select] = useState(fallbackDevice); + const [selectedId, select] = useSetting(setting); return useMemo(() => { - let devId; - if (available) { - devId = available.some((d) => d.deviceId === selectedId) - ? selectedId - : available.some((d) => d.deviceId === fallbackDevice) - ? fallbackDevice + let devId: string | undefined = undefined; + if (!alwaysDefault && available) { + // If the selected device is available, use it. Or if every available + // device ID is falsy, the browser is probably just being paranoid about + // fingerprinting and we should still try using the selected device. + // Otherwise, fall back to the first available device. + devId = + available.some((d) => d.deviceId === selectedId) || + available.every((d) => d.deviceId === "") + ? selectedId : available.at(0)?.deviceId; } @@ -102,10 +107,10 @@ function useMediaDevice( // device entries for the exact same device ID; deduplicate them [...new Map(available.map((d) => [d.deviceId, d])).values()] : [], - selectedId: alwaysDefault ? undefined : devId, + selectedId: devId, select, }; - }, [available, selectedId, fallbackDevice, select, alwaysDefault]); + }, [available, selectedId, select, alwaysDefault]); } const deviceStub: MediaDevice = { @@ -141,36 +146,22 @@ export const MediaDevicesProvider: FC = ({ children }) => { // for ouput devices because the selector wont be shown on FF. const useOutputNames = usingNames && !isFirefox(); - const [storedAudioInput, setStoredAudioInput] = useSetting(audioInputSetting); - const [storedAudioOutput, setStoredAudioOutput] = - useSetting(audioOutputSetting); - const [storedVideoInput, setStoredVideoInput] = useSetting(videoInputSetting); - - const audioInput = useMediaDevice("audioinput", storedAudioInput, usingNames); + const audioInput = useMediaDevice( + "audioinput", + audioInputSetting, + usingNames, + ); const audioOutput = useMediaDevice( "audiooutput", - storedAudioOutput, + audioOutputSetting, useOutputNames, alwaysUseDefaultAudio, ); - const videoInput = useMediaDevice("videoinput", storedVideoInput, usingNames); - - useEffect(() => { - if (audioInput.selectedId !== undefined) - setStoredAudioInput(audioInput.selectedId); - }, [setStoredAudioInput, audioInput.selectedId]); - - useEffect(() => { - // Skip setting state for ff output. Redundent since it is set to always return 'undefined' - // but makes it clear while debugging that this is not happening on FF. + perf ;) - if (audioOutput.selectedId !== undefined && !isFirefox()) - setStoredAudioOutput(audioOutput.selectedId); - }, [setStoredAudioOutput, audioOutput.selectedId]); - - useEffect(() => { - if (videoInput.selectedId !== undefined) - setStoredVideoInput(videoInput.selectedId); - }, [setStoredVideoInput, videoInput.selectedId]); + const videoInput = useMediaDevice( + "videoinput", + videoInputSetting, + usingNames, + ); const startUsingDeviceNames = useCallback( () => setNumCallersUsingNames((n) => n + 1),