Merge pull request #2802 from robintown/device-fall-back

Don't fall back as eagerly to unselected devices
This commit is contained in:
Robin 2024-11-21 11:39:10 -05:00 committed by GitHub
commit 720c400e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 35 additions and 47 deletions

View File

@ -24,6 +24,7 @@ import {
audioInput as audioInputSetting, audioInput as audioInputSetting,
audioOutput as audioOutputSetting, audioOutput as audioOutputSetting,
videoInput as videoInputSetting, videoInput as videoInputSetting,
Setting,
} from "../settings/settings"; } from "../settings/settings";
import { isFirefox } from "../Platform"; import { isFirefox } from "../Platform";
@ -58,7 +59,7 @@ function useObservableState<T>(
function useMediaDevice( function useMediaDevice(
kind: MediaDeviceKind, kind: MediaDeviceKind,
fallbackDevice: string | undefined, setting: Setting<string | undefined>,
usingNames: boolean, usingNames: boolean,
alwaysDefault: boolean = false, alwaysDefault: boolean = false,
): MediaDevice { ): MediaDevice {
@ -84,15 +85,21 @@ function useMediaDevice(
[kind, requestPermissions], [kind, requestPermissions],
); );
const available = useObservableState(deviceObserver, []); const available = useObservableState(deviceObserver, []);
const [selectedId, select] = useState(fallbackDevice); const [preferredId, select] = useSetting(setting);
return useMemo(() => { return useMemo(() => {
let devId; let selectedId: string | undefined = undefined;
if (available) { if (!alwaysDefault && available) {
devId = available.some((d) => d.deviceId === selectedId) // If the preferred device is available, use it. Or if every available
? selectedId // device ID is falsy, the browser is probably just being paranoid about
: available.some((d) => d.deviceId === fallbackDevice) // fingerprinting and we should still try using the preferred device.
? fallbackDevice // 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; : available.at(0)?.deviceId;
} }
@ -102,10 +109,10 @@ function useMediaDevice(
// device entries for the exact same device ID; deduplicate them // device entries for the exact same device ID; deduplicate them
[...new Map(available.map((d) => [d.deviceId, d])).values()] [...new Map(available.map((d) => [d.deviceId, d])).values()]
: [], : [],
selectedId: alwaysDefault ? undefined : devId, selectedId,
select, select,
}; };
}, [available, selectedId, fallbackDevice, select, alwaysDefault]); }, [available, preferredId, select, alwaysDefault]);
} }
const deviceStub: MediaDevice = { const deviceStub: MediaDevice = {
@ -141,36 +148,22 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
// for ouput devices because the selector wont be shown on FF. // for ouput devices because the selector wont be shown on FF.
const useOutputNames = usingNames && !isFirefox(); const useOutputNames = usingNames && !isFirefox();
const [storedAudioInput, setStoredAudioInput] = useSetting(audioInputSetting); const audioInput = useMediaDevice(
const [storedAudioOutput, setStoredAudioOutput] = "audioinput",
useSetting(audioOutputSetting); audioInputSetting,
const [storedVideoInput, setStoredVideoInput] = useSetting(videoInputSetting); usingNames,
);
const audioInput = useMediaDevice("audioinput", storedAudioInput, usingNames);
const audioOutput = useMediaDevice( const audioOutput = useMediaDevice(
"audiooutput", "audiooutput",
storedAudioOutput, audioOutputSetting,
useOutputNames, useOutputNames,
alwaysUseDefaultAudio, alwaysUseDefaultAudio,
); );
const videoInput = useMediaDevice("videoinput", storedVideoInput, usingNames); const videoInput = useMediaDevice(
"videoinput",
useEffect(() => { videoInputSetting,
if (audioInput.selectedId !== undefined) usingNames,
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 startUsingDeviceNames = useCallback( const startUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n + 1), () => setNumCallersUsingNames((n) => n + 1),

View File

@ -20,7 +20,6 @@ import {
TrackEvent, TrackEvent,
} from "livekit-client"; } from "livekit-client";
import { useObservable, useObservableEagerState } from "observable-hooks"; import { useObservable, useObservableEagerState } from "observable-hooks";
import { useEffect } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { useMediaDevices } from "../livekit/MediaDevicesContext"; import { useMediaDevices } from "../livekit/MediaDevicesContext";
@ -35,6 +34,7 @@ export function useSwitchCamera(
video: Observable<LocalVideoTrack | null>, video: Observable<LocalVideoTrack | null>,
): (() => void) | null { ): (() => void) | null {
const mediaDevices = useMediaDevices(); const mediaDevices = useMediaDevices();
const setVideoInput = useLatest(mediaDevices.videoInput.select);
// Produce an observable like the input 'video' observable, except make it // Produce an observable like the input 'video' observable, except make it
// emit whenever the track is muted or the device changes // emit whenever the track is muted or the device changes
@ -75,6 +75,12 @@ export function useSwitchCamera(
.restartTrack({ .restartTrack({
facingMode: facingMode === "user" ? "environment" : "user", 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) => .catch((e) =>
logger.error("Failed to switch camera", facingMode, e), logger.error("Failed to switch camera", facingMode, e),
); );
@ -83,16 +89,5 @@ export function useSwitchCamera(
[videoTrack], [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); return useObservableEagerState(switchCamera);
} }