diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 3d85b165..d405eec0 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,21 @@ function useMediaDevice( [kind, requestPermissions], ); const available = useObservableState(deviceObserver, []); - const [selectedId, select] = useState(fallbackDevice); + const [preferredId, 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 selectedId: string | undefined = undefined; + if (!alwaysDefault && available) { + // If the preferred 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 preferred device. + // Worst case it is not available and the browser will gracefully fall + // back to some other device for us when requesting the media stream. + // Otherwise, select the first available device. + selectedId = + available.some((d) => d.deviceId === preferredId) || + available.every((d) => d.deviceId === "") + ? preferredId : available.at(0)?.deviceId; } @@ -102,10 +109,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, select, }; - }, [available, selectedId, fallbackDevice, select, alwaysDefault]); + }, [available, preferredId, select, alwaysDefault]); } const deviceStub: MediaDevice = { @@ -141,36 +148,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), diff --git a/src/room/useSwitchCamera.ts b/src/room/useSwitchCamera.ts index e0434566..c1b4f092 100644 --- a/src/room/useSwitchCamera.ts +++ b/src/room/useSwitchCamera.ts @@ -20,7 +20,6 @@ import { TrackEvent, } from "livekit-client"; import { useObservable, useObservableEagerState } from "observable-hooks"; -import { useEffect } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { useMediaDevices } from "../livekit/MediaDevicesContext"; @@ -35,6 +34,7 @@ export function useSwitchCamera( video: Observable, ): (() => void) | null { const mediaDevices = useMediaDevices(); + const setVideoInput = useLatest(mediaDevices.videoInput.select); // Produce an observable like the input 'video' observable, except make it // emit whenever the track is muted or the device changes @@ -75,6 +75,12 @@ export function useSwitchCamera( .restartTrack({ facingMode: facingMode === "user" ? "environment" : "user", }) + .then(() => { + // Inform the MediaDeviceContext which camera was chosen + const deviceId = + track.mediaStreamTrack.getSettings().deviceId; + if (deviceId !== undefined) setVideoInput.current(deviceId); + }) .catch((e) => logger.error("Failed to switch camera", facingMode, e), ); @@ -83,16 +89,5 @@ export function useSwitchCamera( [videoTrack], ); - const setVideoInput = useLatest(mediaDevices.videoInput.select); - useEffect(() => { - // Watch for device changes due to switching the camera and feed them back - // into the MediaDeviceContext - const subscription = videoTrack.subscribe((track) => { - const deviceId = track?.mediaStreamTrack.getSettings().deviceId; - if (deviceId !== undefined) setVideoInput.current(deviceId); - }); - return (): void => subscription.unsubscribe(); - }, [videoTrack, setVideoInput]); - return useObservableEagerState(switchCamera); }