From 41f27287242af5729479a0639ae1ac5255c6c9ab Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:20:53 +0200 Subject: [PATCH] Device from lobby to call (#1110) * respect mute state set in lobby for call Signed-off-by: Timo K * move device from lobby to call Signed-off-by: Timo K * save device in local storage Signed-off-by: Timo K * local storage + fixes Signed-off-by: Timo K * device permissions Signed-off-by: Timo K --------- Signed-off-by: Timo K --- src/livekit/useLiveKit.ts | 78 ++++++++++++++++++++++++++++++-------- src/room/InCallView.tsx | 7 ++++ src/settings/useSetting.ts | 7 ++++ 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 4bd68a6b..b888b055 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -1,5 +1,10 @@ -import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client"; -import React from "react"; +import { + ConnectionState, + LocalAudioTrack, + LocalVideoTrack, + Room, +} from "livekit-client"; +import React, { useEffect } from "react"; import { useMediaDeviceSelect, usePreviewDevice, @@ -8,6 +13,7 @@ import { import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices"; import { LocalMediaInfo, MediaInfo } from "../room/VideoPreview"; import { roomOptions } from "./options"; +import { useDefaultDevices } from "../settings/useSetting"; type LiveKitState = { // The state of the media devices (changing the devices will also change them in the room). @@ -19,6 +25,10 @@ type LiveKitState = { room: Room; }; +function emptyToUndef(str) { + return str === "" ? undefined : str; +} + // 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`. @@ -32,21 +42,35 @@ export function useLiveKit(): LiveKitState | undefined { // Create a React state to store the available devices and the selected device for each kind. const mediaDevices = useMediaDevicesState(room); - // Create local video track. + const [settingsDefaultDevices] = useDefaultDevices(); + const [videoEnabled, setVideoEnabled] = React.useState(true); const selectedVideoId = mediaDevices.state.get("videoinput")?.selectedId; + + const [audioEnabled, setAudioEnabled] = React.useState(true); + const selectedAudioId = mediaDevices.state.get("audioinput")?.selectedId; + + // trigger permission popup first, + useEffect(() => { + navigator.mediaDevices.getUserMedia({ + video: { deviceId: selectedVideoId ?? settingsDefaultDevices.videoinput }, + audio: { deviceId: selectedAudioId ?? settingsDefaultDevices.audioinput }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // then start the preview device (no permsssion should be triggered agian) + // Create local video track. const video = usePreviewDevice( videoEnabled, - selectedVideoId ?? "", + selectedVideoId ?? settingsDefaultDevices.videoinput, "videoinput" ); // Create local audio track. - const [audioEnabled, setAudioEnabled] = React.useState(true); - const selectedAudioId = mediaDevices.state.get("audioinput")?.selectedId; const audio = usePreviewDevice( audioEnabled, - selectedAudioId ?? "", + selectedAudioId ?? settingsDefaultDevices.audioinput, "audioinput" ); @@ -71,7 +95,6 @@ export function useLiveKit(): LiveKitState | undefined { }, }; }; - const state: LiveKitState = { mediaDevices: mediaDevices, localMedia: { @@ -102,19 +125,24 @@ export function useLiveKit(): LiveKitState | undefined { return state; } +// if a room is passed this only affects the device selection inside a call. Without room it changes what we see in the lobby function useMediaDevicesState(room: Room): MediaDevicesState { + let connectedRoom: Room; + if (room.state !== ConnectionState.Disconnected) { + connectedRoom = room; + } const { devices: videoDevices, activeDeviceId: activeVideoDevice, setActiveMediaDevice: setActiveVideoDevice, - } = useMediaDeviceSelect({ kind: "videoinput", room }); + } = useMediaDeviceSelect({ kind: "videoinput", room: connectedRoom }); const { devices: audioDevices, activeDeviceId: activeAudioDevice, setActiveMediaDevice: setActiveAudioDevice, } = useMediaDeviceSelect({ kind: "audioinput", - room, + room: connectedRoom, }); const { devices: audioOutputDevices, @@ -122,7 +150,7 @@ function useMediaDevicesState(room: Room): MediaDevicesState { setActiveMediaDevice: setActiveAudioOutputDevice, } = useMediaDeviceSelect({ kind: "audiooutput", - room, + room: connectedRoom, }); const selectActiveDevice = React.useCallback( @@ -139,7 +167,7 @@ function useMediaDevicesState(room: Room): MediaDevicesState { break; } }, - [setActiveAudioDevice, setActiveVideoDevice, setActiveAudioOutputDevice] + [setActiveVideoDevice, setActiveAudioOutputDevice, setActiveAudioDevice] ); const [mediaDevicesState, setMediaDevicesState] = @@ -151,19 +179,35 @@ function useMediaDevicesState(room: Room): MediaDevicesState { return state; }); + const [settingsDefaultDevices, setDefaultDevices] = useDefaultDevices(); + React.useEffect(() => { const state = new Map(); state.set("videoinput", { available: videoDevices, - selectedId: activeVideoDevice, + selectedId: + emptyToUndef(activeVideoDevice) ?? + emptyToUndef(settingsDefaultDevices.videoinput) ?? + videoDevices[0]?.deviceId, }); state.set("audioinput", { available: audioDevices, - selectedId: activeAudioDevice, + selectedId: + emptyToUndef(activeAudioDevice) ?? + emptyToUndef(settingsDefaultDevices.audioinput) ?? + audioDevices[0]?.deviceId, }); state.set("audiooutput", { available: audioOutputDevices, - selectedId: activeAudioOutputDevice, + selectedId: + emptyToUndef(activeAudioOutputDevice) ?? + emptyToUndef(settingsDefaultDevices.audiooutput) ?? + audioOutputDevices[0]?.deviceId, + }); + setDefaultDevices({ + audioinput: state.get("audioinput").selectedId, + videoinput: state.get("videoinput").selectedId, + audiooutput: state.get("audiooutput").selectedId, }); setMediaDevicesState({ state, @@ -177,6 +221,10 @@ function useMediaDevicesState(room: Room): MediaDevicesState { audioOutputDevices, activeAudioOutputDevice, selectActiveDevice, + setDefaultDevices, + settingsDefaultDevices.audioinput, + settingsDefaultDevices.videoinput, + settingsDefaultDevices.audiooutput, ]); return mediaDevicesState; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 92761feb..611f1301 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -165,6 +165,13 @@ export function InCallView({ options ); + // TODO: move the room creation into the useRoom hook and out of the useLiveKit hook. + // This would than allow to not have those 4 lines + livekitRoom.options.audioCaptureDefaults.deviceId = + mediaDevices.state.get("audioinput").selectedId; + livekitRoom.options.videoCaptureDefaults.deviceId = + mediaDevices.state.get("videoinput").selectedId; + // Uses a hook to connect to the LiveKit room (on unmount the room will be left) and publish local media tracks (default). useRoom({ token, serverUrl: Config.get().livekit.server_url, diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 8d0c5a0c..b862c211 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -100,3 +100,10 @@ export const useOptInAnalytics = (): DisableableSetting => { export const useDeveloperSettingsTab = () => useSetting("developer-settings-tab", false); + +export const useDefaultDevices = () => + useSetting("defaultDevices", { + audioinput: "", + videoinput: "", + audiooutput: "", + });