Fix LiveKit's device selection during the call

This commit is contained in:
Daniel Abramov 2023-06-02 19:55:41 +02:00
parent 991129e470
commit b1d7631994
3 changed files with 60 additions and 153 deletions

View file

@ -1,6 +1,9 @@
import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client"; import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
import React from "react"; import React from "react";
import { useMediaDevices, usePreviewDevice } from "@livekit/components-react"; import {
useMediaDeviceSelect,
usePreviewDevice,
} from "@livekit/components-react";
import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices"; import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices";
import { LocalMediaInfo, MediaInfo } from "./VideoPreview"; import { LocalMediaInfo, MediaInfo } from "./VideoPreview";
@ -99,101 +102,44 @@ export function useLiveKit(): LiveKitState | undefined {
} }
function useMediaDevicesState(room: Room): MediaDevicesState { function useMediaDevicesState(room: Room): MediaDevicesState {
// Video input state. const {
const videoInputDevices = useMediaDevices({ kind: "videoinput" }); devices: videoDevices,
const [selectedVideoInput, setSelectedVideoInput] = activeDeviceId: activeVideoDevice,
React.useState<string>(""); setActiveMediaDevice: setActiveVideoDevice,
} = useMediaDeviceSelect({ kind: "videoinput", room });
// Audio input state. const {
const audioInputDevices = useMediaDevices({ kind: "audioinput" }); devices: audioDevices,
const [selectedAudioInput, setSelectedAudioInput] = activeDeviceId: activeAudioDevice,
React.useState<string>(""); setActiveMediaDevice: setActiveAudioDevice,
} = useMediaDeviceSelect({
// Audio output state. kind: "audioinput",
const audioOutputDevices = useMediaDevices({ kind: "audiooutput" });
const [selectedAudioOut, setSelectedAudioOut] = React.useState<string>("");
// Install hooks, so that we react to changes in the available devices.
React.useEffect(() => {
// Helper type to make the code more readable.
type DeviceHookData = {
kind: MediaDeviceKind;
available: MediaDeviceInfo[];
selected: string;
setSelected: React.Dispatch<React.SetStateAction<string>>;
};
const videoInputHook: DeviceHookData = {
kind: "videoinput",
available: videoInputDevices,
selected: selectedVideoInput,
setSelected: setSelectedVideoInput,
};
const audioInputHook: DeviceHookData = {
kind: "audioinput",
available: audioInputDevices,
selected: selectedAudioInput,
setSelected: setSelectedAudioInput,
};
const audioOutputHook: DeviceHookData = {
kind: "audiooutput",
available: audioOutputDevices,
selected: selectedAudioOut,
setSelected: setSelectedAudioOut,
};
const updateDevice = async (kind: MediaDeviceKind, id: string) => {
try {
await room.switchActiveDevice(kind, id);
} catch (e) {
console.error("Failed to switch device", e);
}
};
for (const hook of [videoInputHook, audioInputHook, audioOutputHook]) {
if (hook.available.length === 0) {
const newSelected = "";
hook.setSelected(newSelected);
updateDevice(hook.kind, newSelected);
continue;
}
const found = hook.available.find(
(device) => device.deviceId === hook.selected
);
if (!found) {
const newSelected = hook.available[0].deviceId;
hook.setSelected(newSelected);
updateDevice(hook.kind, newSelected);
continue;
}
}
}, [
videoInputDevices,
selectedVideoInput,
audioInputDevices,
selectedAudioInput,
audioOutputDevices,
selectedAudioOut,
room, room,
]); });
const {
devices: audioOutputDevices,
activeDeviceId: activeAudioOutputDevice,
setActiveMediaDevice: setActiveAudioOutputDevice,
} = useMediaDeviceSelect({
kind: "audiooutput",
room,
});
const selectActiveDevice = async (kind: MediaDeviceKind, id: string) => { const selectActiveDevice = React.useCallback(
switch (kind) { async (kind: MediaDeviceKind, id: string) => {
case "audioinput": switch (kind) {
setSelectedAudioInput(id); case "audioinput":
break; setActiveAudioDevice(id);
case "videoinput": break;
setSelectedVideoInput(id); case "videoinput":
break; setActiveVideoDevice(id);
case "audiooutput": break;
setSelectedAudioOut(id); case "audiooutput":
break; setActiveAudioOutputDevice(id);
} break;
}; }
},
[setActiveAudioDevice, setActiveVideoDevice, setActiveAudioOutputDevice]
);
const [mediaDevicesState, setMediaDevicesState] = const [mediaDevicesState, setMediaDevicesState] =
React.useState<MediaDevicesState>(() => { React.useState<MediaDevicesState>(() => {
@ -205,70 +151,32 @@ function useMediaDevicesState(room: Room): MediaDevicesState {
}); });
React.useEffect(() => { React.useEffect(() => {
// Fill the map of the devices with the current state. const state = new Map<MediaDeviceKind, MediaDevices>();
const mediaDevices = new Map<MediaDeviceKind, MediaDevices>(); state.set("videoinput", {
mediaDevices.set("audioinput", { available: videoDevices,
available: audioInputDevices, selectedId: activeVideoDevice,
selectedId: selectedAudioInput,
}); });
mediaDevices.set("videoinput", { state.set("audioinput", {
available: videoInputDevices, available: audioDevices,
selectedId: selectedVideoInput, selectedId: activeAudioDevice,
}); });
mediaDevices.set("audiooutput", { state.set("audiooutput", {
available: audioOutputDevices, available: audioOutputDevices,
selectedId: selectedAudioOut, selectedId: activeAudioOutputDevice,
});
setMediaDevicesState({
state,
selectActiveDevice,
}); });
if (devicesChanged(mediaDevicesState.state, mediaDevices)) {
const newState: MediaDevicesState = {
state: mediaDevices,
selectActiveDevice,
};
setMediaDevicesState(newState);
}
}, [ }, [
audioInputDevices, videoDevices,
selectedAudioInput, activeVideoDevice,
videoInputDevices, audioDevices,
selectedVideoInput, activeAudioDevice,
audioOutputDevices, audioOutputDevices,
selectedAudioOut, activeAudioOutputDevice,
mediaDevicesState.state, selectActiveDevice,
]); ]);
return mediaDevicesState; return mediaDevicesState;
} }
// Determine if any devices changed between the old and new state.
function devicesChanged(
map1: Map<MediaDeviceKind, MediaDevices>,
map2: Map<MediaDeviceKind, MediaDevices>
): boolean {
if (map1.size !== map2.size) {
return true;
}
for (const [key, value] of map1) {
const newValue = map2.get(key);
if (!newValue) {
return true;
}
if (value.selectedId !== newValue.selectedId) {
return true;
}
if (value.available.length !== newValue.available.length) {
return true;
}
for (let i = 0; i < value.available.length; i++) {
if (value.available[i].deviceId !== newValue.available[i].deviceId) {
return true;
}
}
}
return false;
}

View file

@ -58,7 +58,7 @@ export const SettingsModal = (props: Props) => {
// Generate a `SelectInput` with a list of devices for a given device kind. // Generate a `SelectInput` with a list of devices for a given device kind.
const generateDeviceSelection = (kind: MediaDeviceKind, caption: string) => { const generateDeviceSelection = (kind: MediaDeviceKind, caption: string) => {
const devices = props.mediaDevices.state.get(kind); const devices = props.mediaDevices.state.get(kind);
if (!devices) return null; if (!devices || devices.available.length == 0) return null;
return ( return (
<SelectInput <SelectInput

View file

@ -68,7 +68,6 @@ export function VideoTileContainer({
sfuParticipant={item.sfuParticipant} sfuParticipant={item.sfuParticipant}
name={rawDisplayName} name={rawDisplayName}
avatar={getAvatar && getAvatar(item.member, width, height)} avatar={getAvatar && getAvatar(item.member, width, height)}
maximised={maximised}
{...rest} {...rest}
/> />
)} )}