Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 28 additions & 35 deletions src/livekit/MediaDevicesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
audioInput as audioInputSetting,
audioOutput as audioOutputSetting,
videoInput as videoInputSetting,
Setting,
} from "../settings/settings";
import { isFirefox } from "../Platform";

Expand Down Expand Up @@ -58,7 +59,7 @@ function useObservableState<T>(

function useMediaDevice(
kind: MediaDeviceKind,
fallbackDevice: string | undefined,
setting: Setting<string | undefined>,
usingNames: boolean,
alwaysDefault: boolean = false,
): MediaDevice {
Expand All @@ -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.
Comment on lines +96 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

selectedId =
available.some((d) => d.deviceId === preferredId) ||
available.every((d) => d.deviceId === "")
? preferredId
: available.at(0)?.deviceId;
}

Expand All @@ -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 = {
Expand Down Expand Up @@ -141,36 +148,22 @@ export const MediaDevicesProvider: FC<Props> = ({ 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),
Expand Down
19 changes: 7 additions & 12 deletions src/room/useSwitchCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,6 +34,7 @@ export function useSwitchCamera(
video: Observable<LocalVideoTrack | null>,
): (() => 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
Expand Down Expand Up @@ -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),
);
Expand All @@ -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);
}