Fix LiveKit's device selection during the call
This commit is contained in:
parent
991129e470
commit
b1d7631994
3 changed files with 60 additions and 153 deletions
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in a new issue