Use LiveKit's react hooks for devices
More reliable device management.
This commit is contained in:
parent
f4f5c1ed31
commit
fb9dd7ff71
9 changed files with 229 additions and 250 deletions
|
|
@ -63,7 +63,7 @@ import { ParticipantInfo } from "./useGroupCall";
|
||||||
import { TileDescriptor } from "../video-grid/TileDescriptor";
|
import { TileDescriptor } from "../video-grid/TileDescriptor";
|
||||||
import { AudioSink } from "../video-grid/AudioSink";
|
import { AudioSink } from "../video-grid/AudioSink";
|
||||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||||
import { MediaDevicesState } from "./devices/useMediaDevices";
|
import { MediaDevicesState } from "../settings/mediaDevices";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { LocalMediaInfo, MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { LocalMediaInfo, MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
import { MediaDevicesState } from "./devices/useMediaDevices";
|
import { MediaDevicesState } from "../settings/mediaDevices";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { InviteModal } from "./InviteModal";
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
import { FeedbackModal } from "./FeedbackModal";
|
import { FeedbackModal } from "./FeedbackModal";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { MediaDevicesState } from "./devices/useMediaDevices";
|
import { MediaDevicesState } from "../settings/mediaDevices";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import styles from "./VideoPreview.module.css";
|
import styles from "./VideoPreview.module.css";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { MediaDevicesState } from "./devices/useMediaDevices";
|
import { MediaDevicesState } from "../settings/mediaDevices";
|
||||||
|
|
||||||
export type MatrixInfo = {
|
export type MatrixInfo = {
|
||||||
userName: string;
|
userName: string;
|
||||||
|
|
@ -36,7 +36,7 @@ export type MatrixInfo = {
|
||||||
export type MediaInfo = {
|
export type MediaInfo = {
|
||||||
track: Track; // TODO: Replace it by a more generic `CallFeed` type from JS SDK once we generalise the types.
|
track: Track; // TODO: Replace it by a more generic `CallFeed` type from JS SDK once we generalise the types.
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
setMuted: (muted: boolean) => void;
|
toggle: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocalMediaInfo = {
|
export type LocalMediaInfo = {
|
||||||
|
|
@ -67,7 +67,7 @@ export function VideoPreview({
|
||||||
return () => {
|
return () => {
|
||||||
localMediaInfo.video?.track.detach();
|
localMediaInfo.video?.track.detach();
|
||||||
};
|
};
|
||||||
}, [localMediaInfo]);
|
}, [localMediaInfo.video?.track, mediaElement]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.preview} ref={previewRef}>
|
<div className={styles.preview} ref={previewRef}>
|
||||||
|
|
@ -86,17 +86,13 @@ export function VideoPreview({
|
||||||
{localMediaInfo.audio && (
|
{localMediaInfo.audio && (
|
||||||
<MicButton
|
<MicButton
|
||||||
muted={localMediaInfo.audio?.muted}
|
muted={localMediaInfo.audio?.muted}
|
||||||
onPress={() =>
|
onPress={localMediaInfo.audio?.toggle}
|
||||||
localMediaInfo.audio?.setMuted(!localMediaInfo.audio?.muted)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{localMediaInfo.video && (
|
{localMediaInfo.video && (
|
||||||
<VideoButton
|
<VideoButton
|
||||||
muted={localMediaInfo.video?.muted}
|
muted={localMediaInfo.video?.muted}
|
||||||
onPress={() =>
|
onPress={localMediaInfo.video?.toggle}
|
||||||
localMediaInfo.video?.setMuted(!localMediaInfo.video?.muted)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import type TypedEmitter from "typed-emitter";
|
|
||||||
|
|
||||||
/* This file should become a part of LiveKit JS SDK. */
|
|
||||||
|
|
||||||
// Generic interface for all types that are capable of providing and managing media devices.
|
|
||||||
export interface MediaDevicesManager
|
|
||||||
extends TypedEmitter<MediaDeviceHandlerCallbacks> {
|
|
||||||
getDevices(kind: MediaDeviceKind): Promise<MediaDeviceInfo[]>;
|
|
||||||
setActiveDevice(kind: MediaDeviceKind, deviceId: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MediaDeviceHandlerCallbacks = {
|
|
||||||
devicesChanged: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum MediaDeviceHandlerEvents {
|
|
||||||
DevicesChanged = "devicesChanged",
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { MediaDevicesManager } from "./mediaDevices";
|
|
||||||
|
|
||||||
export type MediaDevices = {
|
|
||||||
available: MediaDeviceInfo[];
|
|
||||||
selected: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MediaDevicesState = {
|
|
||||||
state: Map<MediaDeviceKind, MediaDevices>;
|
|
||||||
selectActiveDevice: (
|
|
||||||
kind: MediaDeviceKind,
|
|
||||||
deviceId: string
|
|
||||||
) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useMediaDevices(
|
|
||||||
mediaDeviceHandler: MediaDevicesManager
|
|
||||||
): MediaDevicesState {
|
|
||||||
// Create a React state to store the available devices and the selected device for each kind.
|
|
||||||
const [state, setState] = useState<Map<MediaDeviceKind, MediaDevices>>(
|
|
||||||
new Map()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the React state when the available devices change.
|
|
||||||
useEffect(() => {
|
|
||||||
// Define a callback that is going to be called each time the available devices change.
|
|
||||||
const updateDevices = async () => {
|
|
||||||
const mediaDeviceKinds: MediaDeviceKind[] = [
|
|
||||||
"audioinput",
|
|
||||||
"audiooutput",
|
|
||||||
"videoinput",
|
|
||||||
];
|
|
||||||
|
|
||||||
const newState = new Map(state);
|
|
||||||
|
|
||||||
// Request all the available devices for each kind.
|
|
||||||
for (const kind of mediaDeviceKinds) {
|
|
||||||
const devices = await mediaDeviceHandler.getDevices(
|
|
||||||
kind as MediaDeviceKind
|
|
||||||
);
|
|
||||||
|
|
||||||
// If newly requested devices are empty, remove the kind from the React state.
|
|
||||||
if (devices.length === 0) {
|
|
||||||
newState.delete(kind);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, check if the current state contains any selected device and find this device in the new list of devices.
|
|
||||||
// If the device exists, update the React state with the new list of devices and the index of the selected device.
|
|
||||||
// If the device does not exist, select the first one (default device).
|
|
||||||
const selectedDevice = state.get(kind);
|
|
||||||
const newSelectedDeviceIndex = selectedDevice
|
|
||||||
? devices.findIndex(
|
|
||||||
(device) =>
|
|
||||||
device.deviceId ===
|
|
||||||
selectedDevice.available[selectedDevice.selected].deviceId
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
newState.set(kind, {
|
|
||||||
available: devices,
|
|
||||||
selected: newSelectedDeviceIndex !== -1 ? newSelectedDeviceIndex : 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesChanged(state, newState)) {
|
|
||||||
setState(newState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
mediaDeviceHandler.on("devicesChanged", updateDevices);
|
|
||||||
return () => {
|
|
||||||
mediaDeviceHandler.off("devicesChanged", updateDevices);
|
|
||||||
};
|
|
||||||
}, [mediaDeviceHandler, state]);
|
|
||||||
|
|
||||||
const selectActiveDeviceFunc = async (
|
|
||||||
kind: MediaDeviceKind,
|
|
||||||
deviceId: string
|
|
||||||
) => {
|
|
||||||
await mediaDeviceHandler.setActiveDevice(kind, deviceId);
|
|
||||||
|
|
||||||
// Update react state as well.
|
|
||||||
setState((prevState) => {
|
|
||||||
const newState = new Map(prevState);
|
|
||||||
const devices = newState.get(kind);
|
|
||||||
if (!devices) {
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSelectedDeviceIndex = devices.available.findIndex(
|
|
||||||
(device) => device.deviceId === deviceId
|
|
||||||
);
|
|
||||||
|
|
||||||
newState.set(kind, {
|
|
||||||
available: devices.available,
|
|
||||||
selected: newSelectedDeviceIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const [selectActiveDevice] = useState<
|
|
||||||
(kind: MediaDeviceKind, deviceId: string) => Promise<void>
|
|
||||||
>(selectActiveDeviceFunc);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
selectActiveDevice,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.selected !== newValue.selected) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import { EventEmitter } from "events";
|
import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
|
||||||
import { Room, RoomEvent, Track } from "livekit-client";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLocalParticipant } from "@livekit/components-react";
|
import { useMediaDevices, usePreviewDevice } from "@livekit/components-react";
|
||||||
|
|
||||||
import {
|
import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices";
|
||||||
MediaDeviceHandlerCallbacks,
|
|
||||||
MediaDeviceHandlerEvents,
|
|
||||||
MediaDevicesManager,
|
|
||||||
} from "./devices/mediaDevices";
|
|
||||||
import { MediaDevicesState, useMediaDevices } from "./devices/useMediaDevices";
|
|
||||||
import { LocalMediaInfo, MediaInfo } from "./VideoPreview";
|
import { LocalMediaInfo, MediaInfo } from "./VideoPreview";
|
||||||
import type TypedEmitter from "typed-emitter";
|
|
||||||
|
|
||||||
type LiveKitState = {
|
type LiveKitState = {
|
||||||
mediaDevices: MediaDevicesState;
|
mediaDevices: MediaDevicesState;
|
||||||
|
|
@ -33,32 +26,35 @@ export function useLiveKit(
|
||||||
return new Room();
|
return new Room();
|
||||||
});
|
});
|
||||||
|
|
||||||
const [mediaDevicesManager] = React.useState<MediaDevicesManager>(() => {
|
// Create a React state to store the available devices and the selected device for each kind.
|
||||||
return new LkMediaDevicesManager(room);
|
const mediaDevices = useMediaDevicesState(room);
|
||||||
});
|
|
||||||
|
|
||||||
const { state: mediaDevicesState, selectActiveDevice: selectDeviceFn } =
|
// Create local video track.
|
||||||
useMediaDevices(mediaDevicesManager);
|
const [videoEnabled, setVideoEnabled] = React.useState<boolean>(true);
|
||||||
|
const selectedVideoId = mediaDevices.state.get("videoinput")?.selectedId;
|
||||||
|
const video = usePreviewDevice(
|
||||||
|
videoEnabled,
|
||||||
|
selectedVideoId ?? "",
|
||||||
|
"videoinput"
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Create local audio track.
|
||||||
console.log("media devices changed, mediaDevices:", mediaDevicesState);
|
const [audioEnabled, setAudioEnabled] = React.useState<boolean>(true);
|
||||||
}, [mediaDevicesState]);
|
const selectedAudioId = mediaDevices.state.get("audioinput")?.selectedId;
|
||||||
|
const audio = usePreviewDevice(
|
||||||
const {
|
audioEnabled,
|
||||||
microphoneTrack,
|
selectedAudioId ?? "",
|
||||||
isMicrophoneEnabled,
|
"audioinput"
|
||||||
cameraTrack,
|
);
|
||||||
isCameraEnabled,
|
|
||||||
localParticipant,
|
|
||||||
} = useLocalParticipant({ room });
|
|
||||||
|
|
||||||
|
// Create final LiveKit state.
|
||||||
const [state, setState] = React.useState<LiveKitState | undefined>(undefined);
|
const [state, setState] = React.useState<LiveKitState | undefined>(undefined);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Helper to create local media without the
|
// Helper to create local media without the copy-paste.
|
||||||
const createLocalMedia = (
|
const createLocalMedia = (
|
||||||
|
track: LocalVideoTrack | LocalAudioTrack | undefined,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
track: Track | undefined,
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
setEnabled
|
|
||||||
): MediaInfo | undefined => {
|
): MediaInfo | undefined => {
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -67,29 +63,24 @@ export function useLiveKit(
|
||||||
return {
|
return {
|
||||||
track,
|
track,
|
||||||
muted: !enabled,
|
muted: !enabled,
|
||||||
setMuted: async (newState: boolean) => {
|
toggle: async () => {
|
||||||
if (enabled != newState) {
|
setEnabled(!enabled);
|
||||||
await setEnabled(newState);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const state: LiveKitState = {
|
const state: LiveKitState = {
|
||||||
mediaDevices: {
|
mediaDevices: mediaDevices,
|
||||||
state: mediaDevicesState,
|
|
||||||
selectActiveDevice: selectDeviceFn,
|
|
||||||
},
|
|
||||||
localMedia: {
|
localMedia: {
|
||||||
audio: createLocalMedia(
|
audio: createLocalMedia(
|
||||||
isMicrophoneEnabled,
|
audio.localTrack,
|
||||||
microphoneTrack?.track,
|
audioEnabled,
|
||||||
localParticipant.setMicrophoneEnabled
|
setAudioEnabled
|
||||||
),
|
),
|
||||||
video: createLocalMedia(
|
video: createLocalMedia(
|
||||||
isCameraEnabled,
|
video.localTrack,
|
||||||
cameraTrack?.track,
|
videoEnabled,
|
||||||
localParticipant.setCameraEnabled
|
setVideoEnabled
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
enterRoom: async () => {
|
enterRoom: async () => {
|
||||||
|
|
@ -105,41 +96,188 @@ export function useLiveKit(
|
||||||
}, [
|
}, [
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
|
mediaDevices,
|
||||||
|
audio.localTrack,
|
||||||
|
video.localTrack,
|
||||||
|
audioEnabled,
|
||||||
|
videoEnabled,
|
||||||
room,
|
room,
|
||||||
mediaDevicesState,
|
|
||||||
selectDeviceFn,
|
|
||||||
localParticipant,
|
|
||||||
microphoneTrack,
|
|
||||||
cameraTrack,
|
|
||||||
isMicrophoneEnabled,
|
|
||||||
isCameraEnabled,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the MediaDevicesHandler interface for the LiveKit's Room class by wrapping it, so that
|
function useMediaDevicesState(room: Room): MediaDevicesState {
|
||||||
// we can pass the confined version of the `Room` to the `MediaDevicesHandler` consumers.
|
// Video input state.
|
||||||
export class LkMediaDevicesManager
|
const videoInputDevices = useMediaDevices({ kind: "videoinput" });
|
||||||
extends (EventEmitter as new () => TypedEmitter<MediaDeviceHandlerCallbacks>)
|
const [selectedVideoInput, setSelectedVideoInput] =
|
||||||
implements MediaDevicesManager
|
React.useState<string>("");
|
||||||
{
|
|
||||||
private room: Room;
|
|
||||||
|
|
||||||
constructor(room: Room) {
|
// Audio input state.
|
||||||
super();
|
const audioInputDevices = useMediaDevices({ kind: "audioinput" });
|
||||||
this.room = room;
|
const [selectedAudioInput, setSelectedAudioInput] =
|
||||||
|
React.useState<string>("");
|
||||||
|
|
||||||
this.room.on(RoomEvent.MediaDevicesChanged, () => {
|
// Audio output state.
|
||||||
this.emit(MediaDeviceHandlerEvents.DevicesChanged);
|
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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 [mediaDevicesState, setMediaDevicesState] =
|
||||||
|
React.useState<MediaDevicesState>(() => {
|
||||||
|
const state: MediaDevicesState = {
|
||||||
|
state: new Map(),
|
||||||
|
selectActiveDevice,
|
||||||
|
};
|
||||||
|
return state;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async getDevices(kind: MediaDeviceKind) {
|
React.useEffect(() => {
|
||||||
return await Room.getLocalDevices(kind);
|
// Fill the map of the devices with the current state.
|
||||||
}
|
const mediaDevices = new Map<MediaDeviceKind, MediaDevices>();
|
||||||
|
mediaDevices.set("audioinput", {
|
||||||
|
available: audioInputDevices,
|
||||||
|
selectedId: selectedAudioInput,
|
||||||
|
});
|
||||||
|
mediaDevices.set("videoinput", {
|
||||||
|
available: videoInputDevices,
|
||||||
|
selectedId: selectedVideoInput,
|
||||||
|
});
|
||||||
|
mediaDevices.set("audiooutput", {
|
||||||
|
available: audioOutputDevices,
|
||||||
|
selectedId: selectedAudioOut,
|
||||||
|
});
|
||||||
|
|
||||||
async setActiveDevice(kind: MediaDeviceKind, deviceId: string) {
|
if (devicesChanged(mediaDevicesState.state, mediaDevices)) {
|
||||||
await this.room.switchActiveDevice(kind, deviceId);
|
const newState: MediaDevicesState = {
|
||||||
}
|
state: mediaDevices,
|
||||||
|
selectActiveDevice,
|
||||||
|
};
|
||||||
|
setMediaDevicesState(newState);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
audioInputDevices,
|
||||||
|
selectedAudioInput,
|
||||||
|
videoInputDevices,
|
||||||
|
selectedVideoInput,
|
||||||
|
audioOutputDevices,
|
||||||
|
selectedAudioOut,
|
||||||
|
mediaDevicesState.state,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
||||||
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
|
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
|
||||||
import { SelectInput } from "../input/SelectInput";
|
import { SelectInput } from "../input/SelectInput";
|
||||||
import { MediaDevicesState } from "../room/devices/useMediaDevices";
|
import { MediaDevicesState } from "./mediaDevices";
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
useSpatialAudio,
|
useSpatialAudio,
|
||||||
|
|
@ -63,7 +63,7 @@ export const SettingsModal = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label={caption}
|
label={caption}
|
||||||
selectedKey={devices.available[devices.selected].deviceId}
|
selectedKey={devices.selectedId}
|
||||||
onSelectionChange={(id) =>
|
onSelectionChange={(id) =>
|
||||||
props.mediaDevices.selectActiveDevice(kind, id.toString())
|
props.mediaDevices.selectActiveDevice(kind, id.toString())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/settings/mediaDevices.ts
Normal file
12
src/settings/mediaDevices.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type MediaDevices = {
|
||||||
|
available: MediaDeviceInfo[];
|
||||||
|
selectedId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MediaDevicesState = {
|
||||||
|
state: Map<MediaDeviceKind, MediaDevices>;
|
||||||
|
selectActiveDevice: (
|
||||||
|
kind: MediaDeviceKind,
|
||||||
|
deviceId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue