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;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (devicesChanged(mediaDevicesState.state, mediaDevices)) {
 | 
				
			||||||
 | 
					      const newState: MediaDevicesState = {
 | 
				
			||||||
 | 
					        state: mediaDevices,
 | 
				
			||||||
 | 
					        selectActiveDevice,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      setMediaDevicesState(newState);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [
 | 
				
			||||||
 | 
					    audioInputDevices,
 | 
				
			||||||
 | 
					    selectedAudioInput,
 | 
				
			||||||
 | 
					    videoInputDevices,
 | 
				
			||||||
 | 
					    selectedVideoInput,
 | 
				
			||||||
 | 
					    audioOutputDevices,
 | 
				
			||||||
 | 
					    selectedAudioOut,
 | 
				
			||||||
 | 
					    mediaDevicesState.state,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return mediaDevicesState;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getDevices(kind: MediaDeviceKind) {
 | 
					// Determine if any devices changed between the old and new state.
 | 
				
			||||||
    return await Room.getLocalDevices(kind);
 | 
					function devicesChanged(
 | 
				
			||||||
 | 
					  map1: Map<MediaDeviceKind, MediaDevices>,
 | 
				
			||||||
 | 
					  map2: Map<MediaDeviceKind, MediaDevices>
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
					  if (map1.size !== map2.size) {
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async setActiveDevice(kind: MediaDeviceKind, deviceId: string) {
 | 
					  for (const [key, value] of map1) {
 | 
				
			||||||
    await this.room.switchActiveDevice(kind, deviceId);
 | 
					    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