2023-05-30 20:56:25 +02:00
|
|
|
import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
|
2023-05-26 20:41:32 +02:00
|
|
|
import React from "react";
|
2023-05-30 20:56:25 +02:00
|
|
|
import { useMediaDevices, usePreviewDevice } from "@livekit/components-react";
|
|
|
|
|
|
|
|
import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices";
|
2023-05-26 20:41:32 +02:00
|
|
|
import { LocalMediaInfo, MediaInfo } from "./VideoPreview";
|
|
|
|
|
|
|
|
type LiveKitState = {
|
2023-06-02 14:49:11 +02:00
|
|
|
// The state of the media devices (changing the devices will also change them in the room).
|
2023-05-26 20:41:32 +02:00
|
|
|
mediaDevices: MediaDevicesState;
|
2023-06-02 14:49:11 +02:00
|
|
|
// The local media (audio and video) that can be referenced in an e.g. lobby view.
|
2023-05-26 20:41:32 +02:00
|
|
|
localMedia: LocalMediaInfo;
|
2023-06-02 14:49:11 +02:00
|
|
|
// A reference to the newly constructed (but not yet entered) room for future use with the LiveKit hooks.
|
|
|
|
// TODO: Abstract this away, so that the user doesn't have to deal with the LiveKit room directly.
|
|
|
|
room: Room;
|
2023-05-26 20:41:32 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Returns the React state for the LiveKit's Room class.
|
|
|
|
// The actual return type should be `LiveKitState`, but since this is a React hook, the initialisation is
|
|
|
|
// delayed (done after the rendering, not during the rendering), because of that this function may return `undefined`.
|
|
|
|
// But soon this state is changed to the actual `LiveKitState` value.
|
2023-06-02 14:49:11 +02:00
|
|
|
export function useLiveKit(): LiveKitState | undefined {
|
2023-05-26 20:41:32 +02:00
|
|
|
// TODO: Pass the proper paramters to configure the room (supported codecs, simulcast, adaptive streaming, etc).
|
|
|
|
const [room] = React.useState<Room>(() => {
|
|
|
|
return new Room();
|
|
|
|
});
|
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// Create a React state to store the available devices and the selected device for each kind.
|
|
|
|
const mediaDevices = useMediaDevicesState(room);
|
2023-05-26 20:41:32 +02:00
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// Create local video track.
|
|
|
|
const [videoEnabled, setVideoEnabled] = React.useState<boolean>(true);
|
|
|
|
const selectedVideoId = mediaDevices.state.get("videoinput")?.selectedId;
|
|
|
|
const video = usePreviewDevice(
|
|
|
|
videoEnabled,
|
|
|
|
selectedVideoId ?? "",
|
|
|
|
"videoinput"
|
|
|
|
);
|
2023-05-26 20:41:32 +02:00
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// Create local audio track.
|
|
|
|
const [audioEnabled, setAudioEnabled] = React.useState<boolean>(true);
|
|
|
|
const selectedAudioId = mediaDevices.state.get("audioinput")?.selectedId;
|
|
|
|
const audio = usePreviewDevice(
|
|
|
|
audioEnabled,
|
|
|
|
selectedAudioId ?? "",
|
|
|
|
"audioinput"
|
|
|
|
);
|
2023-05-26 20:41:32 +02:00
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// Create final LiveKit state.
|
2023-05-26 20:41:32 +02:00
|
|
|
const [state, setState] = React.useState<LiveKitState | undefined>(undefined);
|
|
|
|
React.useEffect(() => {
|
2023-05-30 20:56:25 +02:00
|
|
|
// Helper to create local media without the copy-paste.
|
2023-05-26 20:41:32 +02:00
|
|
|
const createLocalMedia = (
|
2023-05-30 20:56:25 +02:00
|
|
|
track: LocalVideoTrack | LocalAudioTrack | undefined,
|
2023-05-26 20:41:32 +02:00
|
|
|
enabled: boolean,
|
2023-05-30 20:56:25 +02:00
|
|
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
2023-05-26 20:41:32 +02:00
|
|
|
): MediaInfo | undefined => {
|
|
|
|
if (!track) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
track,
|
|
|
|
muted: !enabled,
|
2023-05-30 20:56:25 +02:00
|
|
|
toggle: async () => {
|
|
|
|
setEnabled(!enabled);
|
2023-05-26 20:41:32 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const state: LiveKitState = {
|
2023-05-30 20:56:25 +02:00
|
|
|
mediaDevices: mediaDevices,
|
2023-05-26 20:41:32 +02:00
|
|
|
localMedia: {
|
|
|
|
audio: createLocalMedia(
|
2023-05-30 20:56:25 +02:00
|
|
|
audio.localTrack,
|
|
|
|
audioEnabled,
|
|
|
|
setAudioEnabled
|
2023-05-26 20:41:32 +02:00
|
|
|
),
|
|
|
|
video: createLocalMedia(
|
2023-05-30 20:56:25 +02:00
|
|
|
video.localTrack,
|
|
|
|
videoEnabled,
|
|
|
|
setVideoEnabled
|
2023-05-26 20:41:32 +02:00
|
|
|
),
|
|
|
|
},
|
2023-06-02 14:49:11 +02:00
|
|
|
room,
|
2023-05-26 20:41:32 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
setState(state);
|
|
|
|
}, [
|
2023-05-30 20:56:25 +02:00
|
|
|
mediaDevices,
|
|
|
|
audio.localTrack,
|
|
|
|
video.localTrack,
|
|
|
|
audioEnabled,
|
|
|
|
videoEnabled,
|
2023-05-26 20:41:32 +02:00
|
|
|
room,
|
|
|
|
]);
|
|
|
|
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
function useMediaDevicesState(room: Room): MediaDevicesState {
|
|
|
|
// Video input state.
|
|
|
|
const videoInputDevices = useMediaDevices({ kind: "videoinput" });
|
|
|
|
const [selectedVideoInput, setSelectedVideoInput] =
|
|
|
|
React.useState<string>("");
|
|
|
|
|
|
|
|
// Audio input state.
|
|
|
|
const audioInputDevices = useMediaDevices({ kind: "audioinput" });
|
|
|
|
const [selectedAudioInput, setSelectedAudioInput] =
|
|
|
|
React.useState<string>("");
|
2023-05-26 20:41:32 +02:00
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// Audio output state.
|
|
|
|
const audioOutputDevices = useMediaDevices({ kind: "audiooutput" });
|
|
|
|
const [selectedAudioOut, setSelectedAudioOut] = React.useState<string>("");
|
2023-05-26 20:41:32 +02:00
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
// 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,
|
2023-05-26 20:41:32 +02:00
|
|
|
});
|
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
if (devicesChanged(mediaDevicesState.state, mediaDevices)) {
|
|
|
|
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;
|
2023-05-26 20:41:32 +02:00
|
|
|
}
|
|
|
|
|
2023-05-30 20:56:25 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2023-05-26 20:41:32 +02:00
|
|
|
}
|
2023-05-30 20:56:25 +02:00
|
|
|
|
|
|
|
return false;
|
2023-05-26 20:41:32 +02:00
|
|
|
}
|