From b1d76319940da81712981b3fa862b61d670c8722 Mon Sep 17 00:00:00 2001 From: Daniel Abramov Date: Fri, 2 Jun 2023 19:55:41 +0200 Subject: [PATCH] Fix LiveKit's device selection during the call --- src/room/useLiveKit.ts | 210 ++++++++------------------ src/settings/SettingsModal.tsx | 2 +- src/video-grid/VideoTileContainer.tsx | 1 - 3 files changed, 60 insertions(+), 153 deletions(-) diff --git a/src/room/useLiveKit.ts b/src/room/useLiveKit.ts index a078c87..55289c9 100644 --- a/src/room/useLiveKit.ts +++ b/src/room/useLiveKit.ts @@ -1,6 +1,9 @@ import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client"; 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 { LocalMediaInfo, MediaInfo } from "./VideoPreview"; @@ -99,101 +102,44 @@ export function useLiveKit(): LiveKitState | undefined { } function useMediaDevicesState(room: Room): MediaDevicesState { - // Video input state. - const videoInputDevices = useMediaDevices({ kind: "videoinput" }); - const [selectedVideoInput, setSelectedVideoInput] = - React.useState(""); - - // Audio input state. - const audioInputDevices = useMediaDevices({ kind: "audioinput" }); - const [selectedAudioInput, setSelectedAudioInput] = - React.useState(""); - - // Audio output state. - const audioOutputDevices = useMediaDevices({ kind: "audiooutput" }); - const [selectedAudioOut, setSelectedAudioOut] = React.useState(""); - - // 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>; - }; - - 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, + const { + devices: videoDevices, + activeDeviceId: activeVideoDevice, + setActiveMediaDevice: setActiveVideoDevice, + } = useMediaDeviceSelect({ kind: "videoinput", room }); + const { + devices: audioDevices, + activeDeviceId: activeAudioDevice, + setActiveMediaDevice: setActiveAudioDevice, + } = useMediaDeviceSelect({ + kind: "audioinput", room, - ]); + }); + const { + devices: audioOutputDevices, + activeDeviceId: activeAudioOutputDevice, + setActiveMediaDevice: setActiveAudioOutputDevice, + } = useMediaDeviceSelect({ + kind: "audiooutput", + room, + }); - const selectActiveDevice = async (kind: MediaDeviceKind, id: string) => { - switch (kind) { - case "audioinput": - setSelectedAudioInput(id); - break; - case "videoinput": - setSelectedVideoInput(id); - break; - case "audiooutput": - setSelectedAudioOut(id); - break; - } - }; + const selectActiveDevice = React.useCallback( + async (kind: MediaDeviceKind, id: string) => { + switch (kind) { + case "audioinput": + setActiveAudioDevice(id); + break; + case "videoinput": + setActiveVideoDevice(id); + break; + case "audiooutput": + setActiveAudioOutputDevice(id); + break; + } + }, + [setActiveAudioDevice, setActiveVideoDevice, setActiveAudioOutputDevice] + ); const [mediaDevicesState, setMediaDevicesState] = React.useState(() => { @@ -205,70 +151,32 @@ function useMediaDevicesState(room: Room): MediaDevicesState { }); React.useEffect(() => { - // Fill the map of the devices with the current state. - const mediaDevices = new Map(); - mediaDevices.set("audioinput", { - available: audioInputDevices, - selectedId: selectedAudioInput, + const state = new Map(); + state.set("videoinput", { + available: videoDevices, + selectedId: activeVideoDevice, }); - mediaDevices.set("videoinput", { - available: videoInputDevices, - selectedId: selectedVideoInput, + state.set("audioinput", { + available: audioDevices, + selectedId: activeAudioDevice, }); - mediaDevices.set("audiooutput", { + state.set("audiooutput", { available: audioOutputDevices, - selectedId: selectedAudioOut, + selectedId: activeAudioOutputDevice, + }); + setMediaDevicesState({ + state, + selectActiveDevice, }); - - if (devicesChanged(mediaDevicesState.state, mediaDevices)) { - const newState: MediaDevicesState = { - state: mediaDevices, - selectActiveDevice, - }; - setMediaDevicesState(newState); - } }, [ - audioInputDevices, - selectedAudioInput, - videoInputDevices, - selectedVideoInput, + videoDevices, + activeVideoDevice, + audioDevices, + activeAudioDevice, audioOutputDevices, - selectedAudioOut, - mediaDevicesState.state, + activeAudioOutputDevice, + selectActiveDevice, ]); return mediaDevicesState; } - -// Determine if any devices changed between the old and new state. -function devicesChanged( - map1: Map, - map2: Map -): 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; -} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 1dc9524..72ae6e4 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -58,7 +58,7 @@ export const SettingsModal = (props: Props) => { // Generate a `SelectInput` with a list of devices for a given device kind. const generateDeviceSelection = (kind: MediaDeviceKind, caption: string) => { const devices = props.mediaDevices.state.get(kind); - if (!devices) return null; + if (!devices || devices.available.length == 0) return null; return ( )}