Get context providers into parity with prebuilt

This commit is contained in:
harshithpabbati 2021-12-21 18:47:01 +05:30
parent 838948bf93
commit d72a200c96
11 changed files with 247 additions and 343 deletions

View File

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Audio } from '@custom/shared/components/Audio';
import { BasicTray } from '@custom/shared/components/Tray';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import useJoinSound from '@custom/shared/hooks/useJoinSound';
import { useJoinSound } from '@custom/shared/hooks/useJoinSound';
import PropTypes from 'prop-types';
import { WaitingRoom } from './WaitingRoom';

View File

@ -8,13 +8,15 @@ export const DeviceSelect = () => {
cams,
mics,
speakers,
currentDevices,
setCamDevice,
setMicDevice,
setSpeakersDevice,
currentCam,
setCurrentCam,
currentMic,
setCurrentMic,
currentSpeaker,
setCurrentSpeaker,
} = useMediaDevices();
if (!currentDevices) {
if (!currentCam && !currentMic && !currentSpeaker) {
return <div>Loading devices...</div>;
}
@ -22,9 +24,9 @@ export const DeviceSelect = () => {
<>
<Field label="Select camera:">
<SelectInput
onChange={(e) => setCamDevice(cams[e.target.value])}
onChange={(e) => setCurrentCam(cams[e.target.value])}
value={cams.findIndex(
(i) => i.deviceId === currentDevices.camera.deviceId
(i) => i.deviceId === currentCam.deviceId
)}
>
{cams.map(({ deviceId, label }, i) => (
@ -37,9 +39,9 @@ export const DeviceSelect = () => {
<Field label="Select microphone:">
<SelectInput
onChange={(e) => setMicDevice(mics[e.target.value])}
onChange={(e) => setCurrentMic(mics[e.target.value])}
value={mics.findIndex(
(i) => i.deviceId === currentDevices.mic.deviceId
(i) => i.deviceId === currentMic.deviceId
)}
>
{mics.map(({ deviceId, label }, i) => (
@ -56,9 +58,9 @@ export const DeviceSelect = () => {
{speakers.length > 0 && (
<Field label="Select speakers:">
<SelectInput
onChange={(e) => setSpeakersDevice(speakers[e.target.value])}
onChange={(e) => setCurrentSpeaker(speakers[e.target.value])}
value={speakers.findIndex(
(i) => i.deviceId === currentDevices.speaker.deviceId
(i) => i.deviceId === currentSpeaker.deviceId
)}
>
{speakers.map(({ deviceId, label }, i) => (

View File

@ -1,36 +1,10 @@
import React, { useMemo, forwardRef, memo, useEffect, useState } from 'react';
import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { isScreenId } from '@custom/shared/contexts/participantsState';
import React, { useMemo, forwardRef, memo, useEffect } from 'react';
import Bowser from 'bowser';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { shallowEqualObjects } from 'shallow-equal';
import { useDeepCompareMemo } from 'use-deep-compare';
export const Video = memo(
forwardRef(({ fit = 'contain', participantId, videoTrack, ...rest }, videoEl) => {
const { callObject } = useCallState();
const { isMobile } = useUIState();
const isLocalCam = useMemo(() => {
const localParticipant = callObject.participants()?.local;
return participantId === localParticipant.session_id && !isScreenId(participantId);
}, [callObject, participantId]);
const [isMirrored, setIsMirrored] = useState(isLocalCam);
/**
* Considered as playable video:
* - local cam feed
* - any screen share
* - remote cam feed that is subscribed and reported as playable
*/
const isPlayable = useDeepCompareMemo(
() => isLocalCam || isScreenId(participantId),
[isLocalCam, isScreenId(participantId)]
);
forwardRef(({ participantId, videoTrack, ...rest }, videoEl) => {
/**
* Memo: Chrome >= 92?
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=1232649
@ -45,114 +19,33 @@ export const Video = memo(
}, []);
/**
* Determine if video needs to be mirrored.
*/
useEffect(() => {
if (!videoTrack) return;
const videoTrackSettings = videoTrack.getSettings();
const isUsersFrontCamera =
'facingMode' in videoTrackSettings
? isLocalCam && videoTrackSettings.facingMode === 'user'
: isLocalCam;
// only apply mirror effect to user facing camera
if (isMirrored !== isUsersFrontCamera) {
setIsMirrored(isUsersFrontCamera);
}
}, [isMirrored, isLocalCam, videoTrack]);
/**
* Handle canplay & picture-in-picture events.
* Effect: Umount
* Note: nullify src to ensure media object is not counted
*/
useEffect(() => {
const video = videoEl.current;
if (!video) return;
const handleCanPlay = () => {
if (!video.paused) return;
video.play();
};
const handleEnterPIP = () => {
video.style.transform = 'scale(1)';
};
const handleLeavePIP = () => {
video.style.transform = '';
setTimeout(() => {
if (video.paused) video.play();
}, 100);
};
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('enterpictureinpicture', handleEnterPIP);
video.addEventListener('leavepictureinpicture', handleLeavePIP);
return () => {
video.removeEventListener('canplay', handleCanPlay);
video.removeEventListener('enterpictureinpicture', handleEnterPIP);
video.removeEventListener('leavepictureinpicture', handleLeavePIP);
};
}, [isChrome92, videoEl]);
/**
* Update srcObject.
*/
useEffect(() => {
const video = videoEl.current;
if (!video || !videoTrack) return;
video.srcObject = new MediaStream([videoTrack]);
if (!video) return false;
// clean up when video renders for different participant
video.srcObject = null;
if (isChrome92) video.load();
return () => {
// clean up when unmounted
video.srcObject = null;
if (isChrome92) video.load();
};
}, [isChrome92, participantId, videoEl, videoTrack, videoTrack?.id]);
}, [videoEl, isChrome92, participantId]);
return (
<>
<video
className={classNames(fit, {
isMirrored,
isMobile,
playable: isPlayable && videoTrack?.enabled,
})}
autoPlay
muted
playsInline
ref={videoEl}
{...props}
/>
<style jsx>{`
video {
opacity: 0;
}
video.playable {
opacity: 1;
}
video.isMirrored {
transform: scale(-1, 1);
}
video.isMobile {
border-radius: 4px;
display: block;
height: 100%;
position: relative;
width: 100%;
}
video:not(.isMobile) {
height: calc(100% + 4px);
left: -2px;
object-position: center;
position: absolute;
top: -2px;
width: calc(100% + 4px);
}
video.contain {
object-fit: contain;
}
video.cover {
object-fit: cover;
}
`}</style>
</>
);
/**
* Effect: mount source (and force load on Chrome)
*/
useEffect(() => {
const video = videoEl.current;
if (!video || !videoTrack) return;
video.srcObject = new MediaStream([videoTrack]);
if (isChrome92) video.load();
}, [videoEl, isChrome92, videoTrack]);
return <video autoPlay muted playsInline ref={videoEl} {...rest} />;
}),
(p, n) => shallowEqualObjects(p, n)
);
@ -165,4 +58,4 @@ Video.propTypes = {
participantId: PropTypes.string,
};
export default Video;
export default Video;

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext } from 'react';
import React, { createContext, useContext, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useCallState } from './CallProvider';
@ -12,33 +12,72 @@ export const MediaDeviceProvider = ({ children }) => {
const { localParticipant } = useParticipants();
const {
cams,
mics,
speakers,
camError,
micError,
currentDevices,
cams,
currentCam,
currentMic,
currentSpeaker,
deviceState,
setMicDevice,
setCamDevice,
setSpeakersDevice,
micError,
mics,
refreshDevices,
setCurrentCam,
setCurrentMic,
setCurrentSpeaker,
speakers,
} = useDevices(callObject);
const selectCamera = useCallback(
async (newCam) => {
if (!callObject || newCam.deviceId === currentCam?.deviceId) return;
const { camera } = await callObject.setInputDevicesAsync({
videoDeviceId: newCam.deviceId,
});
setCurrentCam(camera);
},
[callObject, currentCam, setCurrentCam]
);
const selectMic = useCallback(
async (newMic) => {
if (!callObject || newMic.deviceId === currentMic?.deviceId) return;
const { mic } = await callObject.setInputDevicesAsync({
audioDeviceId: newMic.deviceId,
});
setCurrentMic(mic);
},
[callObject, currentMic, setCurrentMic]
);
const selectSpeaker = useCallback(
(newSpeaker) => {
if (!callObject || newSpeaker.deviceId === currentSpeaker?.deviceId) return;
callObject.setOutputDevice({
outputDeviceId: newSpeaker.deviceId,
});
setCurrentSpeaker(newSpeaker);
},
[callObject, currentSpeaker, setCurrentSpeaker]
);
return (
<MediaDeviceContext.Provider
value={{
cams,
mics,
speakers,
camError,
micError,
currentDevices,
cams,
currentCam,
currentMic,
currentSpeaker,
deviceState,
isCamMuted: localParticipant.isCamMuted,
isMicMuted: localParticipant.isMicMuted,
setMicDevice,
setCamDevice,
setSpeakersDevice,
micError,
mics,
refreshDevices,
setCurrentCam: selectCamera,
setCurrentMic: selectMic,
setCurrentSpeaker: selectSpeaker,
speakers,
}}
>
{children}

View File

@ -37,12 +37,12 @@ import {
export const ParticipantsContext = createContext();
export const ParticipantsProvider = ({ children }) => {
const { callObject, videoQuality, networkState } = useCallState();
const { callObject, videoQuality, networkState, broadcast, broadcastRole, } = useCallState();
const [state, dispatch] = useReducer(
participantsReducer,
initialParticipantsState
);
const { viewMode } = useUIState();
const { isMobile, viewMode, pinnedId } = useUIState();
const [participantMarkedForRemoval, setParticipantMarkedForRemoval] =
useState(null);
@ -57,7 +57,12 @@ export const ParticipantsProvider = ({ children }) => {
/**
* Only return participants that should be visible in the call
*/
const participants = useMemo(() => state.participants, [state.participants]);
const participants = useMemo(() => {
if (broadcast) {
return state.participants.filter((p) => p?.isOwner);
}
return state.participants;
}, [broadcast, state.participants]);
/**
* Array of participant IDs
@ -95,11 +100,6 @@ export const ParticipantsProvider = ({ children }) => {
[allParticipants]
);
const isOwner = useMemo(
() => !!localParticipant?.isOwner,
[localParticipant]
);
/**
* The participant who should be rendered prominently right now
*/
@ -110,8 +110,13 @@ export const ParticipantsProvider = ({ children }) => {
* if everyone else is muted when AP leaves, the value will be stale.
*/
const isPresent = participants.some((p) => p?.id === activeParticipant?.id);
const pinned = participants.find((p) => p?.id === pinnedId);
const displayableParticipants = participants.filter((p) => !p?.isLocal);
if (pinned) return pinned;
const displayableParticipants = participants.filter((p) =>
isMobile ? !p?.isLocal && !p?.isScreenshare : !p?.isLocal
);
if (
!isPresent &&
@ -130,22 +135,42 @@ export const ParticipantsProvider = ({ children }) => {
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
.reverse();
return isPresent ? activeParticipant : sorted?.[0] ?? localParticipant;
}, [activeParticipant, localParticipant, participants]);
const fallback = broadcastRole === 'attendee' ? null : localParticipant;
return isPresent ? activeParticipant : sorted?.[0] ?? fallback;
}, [
activeParticipant,
broadcastRole,
isMobile,
localParticipant,
participants,
pinnedId,
]);
/**
* Screen shares
*/
const screens = useMemo(
() => allParticipants.filter(({ isScreenshare }) => isScreenshare),
[allParticipants]
);
const screens = useMemo(() => state?.screens, [state?.screens]);
/**
* The local participant's name
*/
const username = callObject?.participants()?.local?.user_name ?? '';
/**
* Sets the local participant's name in daily-js
* @param name The new username
*/
const setUsername = (name) => {
callObject.setUserName(name);
};
const isOwner = useMemo(
() => !!localParticipant?.isOwner,
[localParticipant]
);
const [muteNewParticipants, setMuteNewParticipants] = useState(false);
const muteAll = useCallback(
@ -165,22 +190,20 @@ export const ParticipantsProvider = ({ children }) => {
[callObject, localParticipant, participants]
);
/**
* Sets the local participant's name in daily-js
* @param name The new username
*/
const setUsername = (name) => {
callObject.setUserName(name);
};
const swapParticipantPosition = (id1, id2) => {
const swapParticipantPosition = useCallback((id1, id2) => {
/**
* Ignore in the following cases:
* - id1 and id2 are equal
* - one of both ids is not set
* - one of both ids is 'local'
*/
if (id1 === id2 || !id1 || !id2 || isLocalId(id1) || isLocalId(id2)) return;
dispatch({
type: SWAP_POSITION,
type: 'SWAP_POSITION',
id1,
id2,
});
};
}, []);
const handleNewParticipantsState = useCallback(
(event = null) => {
@ -241,10 +264,11 @@ export const ParticipantsProvider = ({ children }) => {
/**
* Change between the simulcast layers based on view / available bandwidth
*/
const setBandWidthControls = useCallback(() => {
if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
const ids = participantIds.split(',');
const ids = participantIds.split(',').filter(Boolean);
const receiveSettings = {};
ids.forEach((id) => {
@ -252,19 +276,16 @@ export const ParticipantsProvider = ({ children }) => {
if (
// weak or bad network
([VIDEO_QUALITY_LOW, VIDEO_QUALITY_VERY_LOW].includes(networkState) &&
videoQuality === VIDEO_QUALITY_AUTO) ||
(['low', 'very-low'].includes(networkState) && videoQuality === 'auto') ||
// Low quality or Bandwidth saver mode enabled
[VIDEO_QUALITY_BANDWIDTH_SAVER, VIDEO_QUALITY_LOW].includes(
videoQuality
)
['bandwidth-saver', 'low'].includes(videoQuality)
) {
receiveSettings[id] = { video: { layer: 0 } };
return;
}
// Speaker view settings based on speaker status or pinned user
if (viewMode === VIEW_MODE_SPEAKER) {
if (viewMode === 'speaker') {
if (currentSpeaker?.id === id) {
receiveSettings[id] = { video: { layer: 2 } };
} else {
@ -273,16 +294,11 @@ export const ParticipantsProvider = ({ children }) => {
}
// Grid view settings are handled separately in GridView
// Mobile view settings are handled separately in MobileCall
});
callObject.updateReceiveSettings(receiveSettings);
}, [
currentSpeaker?.id,
callObject,
networkState,
participantIds,
videoQuality,
viewMode,
]);
}, [callObject, participantIds, networkState, videoQuality, viewMode, currentSpeaker?.id]);
useEffect(() => {
setBandWidthControls();
@ -315,12 +331,12 @@ export const ParticipantsProvider = ({ children }) => {
allParticipants,
currentSpeaker,
localParticipant,
muteAll,
muteNewParticipants,
participantCount,
participantMarkedForRemoval,
participants,
screens,
muteNewParticipants,
muteAll,
setParticipantMarkedForRemoval,
setUsername,
swapParticipantPosition,

View File

@ -15,6 +15,7 @@ import { useDeepCompareEffect } from 'use-deep-compare';
import { sortByKey } from '../lib/sortByKey';
import { useCallState } from './CallProvider';
import { useParticipants } from './ParticipantsProvider';
import { useUIState } from './UIStateProvider';
import { isLocalId, isScreenId } from './participantsState';
import {
initialTracksState,
@ -42,6 +43,7 @@ const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => {
const { callObject, subscribeToTracksAutomatically } = useCallState();
const { participants } = useParticipants();
const { viewMode } = useUIState();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
const recentSpeakerIds = useMemo(
@ -83,13 +85,19 @@ export const TracksProvider = ({ children }) => {
// stage all remote cams that aren't already marked for subscription.
// Otherwise, honor the provided stagedIds, with recent speakers appended
// who aren't already marked for subscription.
const stagedIdsFiltered =
if (
remoteParticipantIds.length <= SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD
? remoteParticipantIds.filter((id) => !subscribedIds.includes(id))
: [
...stagedIds,
...recentSpeakerIds.filter((id) => !subscribedIds.includes(id)),
];
) {
stagedIds = remoteParticipantIds.filter(
(id) => !subscribedIds.includes(id)
);
} else {
if (viewMode !== 'grid') {
stagedIds.push(
...recentSpeakerIds.filter((id) => !subscribedIds.includes(id))
);
}
}
// Assemble updates to get to desired cam subscriptions
const updates = remoteParticipantIds.reduce((u, id) => {
@ -104,7 +112,7 @@ export const TracksProvider = ({ children }) => {
// subscribed, staged, or unsubscribed
if (subscribedIds.includes(id)) {
desiredSubscription = true;
} else if (stagedIdsFiltered.includes(id)) {
} else if (stagedIds.includes(id)) {
desiredSubscription = 'staged';
} else {
desiredSubscription = false;
@ -116,9 +124,6 @@ export const TracksProvider = ({ children }) => {
u[id] = {
setSubscribedTracks: {
audio: true,
screenAudio: true,
screenVideo: true,
video: desiredSubscription,
},
};
@ -128,7 +133,7 @@ export const TracksProvider = ({ children }) => {
if (Object.keys(updates).length === 0) return;
callObject.updateParticipants(updates);
},
[callObject, remoteParticipantIds, recentSpeakerIds]
[callObject, remoteParticipantIds, viewMode, recentSpeakerIds]
);
useEffect(() => {

View File

@ -20,8 +20,8 @@ export const UIStateProvider = ({
customTrayComponent,
children,
}) => {
const [isMobile, setIsMobile] = useState(false);
const [pinnedId, setPinnedId] = useState(null);
const [isMobile, setIsMobile] = useState(false);
const [preferredViewMode, setPreferredViewMode] = useState(VIEW_MODE_SPEAKER);
const [viewMode, setViewMode] = useState(preferredViewMode);
const [isShowingScreenshare, setIsShowingScreenshare] = useState(false);
@ -31,6 +31,20 @@ export const UIStateProvider = ({
const [customCapsule, setCustomCapsule] = useState();
const [showAutoplayFailedModal, setShowAutoplayFailedModal] = useState(false);
/**
* Decide on view mode based on input conditions.
*/
useEffect(() => {
if (isMobile) {
setViewMode(VIEW_MODE_MOBILE);
} else if (pinnedId || isShowingScreenshare) {
setViewMode(VIEW_MODE_SPEAKER);
} else {
setViewMode(preferredViewMode);
}
}, [pinnedId, isMobile, isShowingScreenshare, preferredViewMode]);
const openModal = useCallback((modalName) => {
setActiveModals((prevState) => ({
...prevState,

View File

@ -22,7 +22,6 @@ const initialParticipantsState = {
camMutedByHost: false,
hasNameSet: false,
id: 'local',
user_id: '',
isActiveSpeaker: false,
isCamMuted: false,
isLoading: true,
@ -34,6 +33,7 @@ const initialParticipantsState = {
lastActiveDate: null,
micMutedByHost: false,
name: '',
sessionId: '',
},
],
screens: [],
@ -42,7 +42,7 @@ const initialParticipantsState = {
// --- Derived data ---
function getId(participant) {
return participant.local ? 'local' : participant.user_id;
return participant.local ? 'local' : participant.session_id;
}
function getScreenId(id) {
@ -69,28 +69,25 @@ function getNewParticipant(participant) {
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
user_id: participant.user_id,
isActiveSpeaker: false,
isCamMuted:
video?.state === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isCamMuted: video?.state === 'off' || video?.state === 'blocked',
isLoading: audio?.state === 'loading' || video?.state === 'loading',
isLocal: local,
isMicMuted:
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
isOwner: !!participant.owner,
isRecording: !!participant.record,
isScreenshare: false,
lastActiveDate: null,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
sessionId: participant.session_id,
};
}
function getUpdatedParticipant(participant, participants) {
function getUpdatedParticipant(
participant,
participants
) {
const id = getId(participant);
const prevItem = participants.find((p) => p.id === id);
@ -99,26 +96,21 @@ function getUpdatedParticipant(participant, participants) {
const { local } = participant;
const { audio, video } = participant.tracks;
return {
...prevItem,
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
user_id: participant.user_id,
isCamMuted:
video?.state === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isCamMuted: video?.state === 'off' || video?.state === 'blocked',
isLoading: audio?.state === 'loading' || video?.state === 'loading',
isLocal: local,
isMicMuted:
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
isOwner: !!participant.owner,
isRecording: !!participant.record,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
sessionId: participant.session_id,
};
}
@ -132,6 +124,7 @@ function getScreenItem(participant) {
isScreenshare: true,
lastActiveDate: null,
name: participant.user_name,
sessionId: participant.session_id,
};
}

View File

@ -29,6 +29,7 @@ export const CALL_STATE_REDIRECTING = 'redirecting';
export const CALL_STATE_NOT_FOUND = 'not-found';
export const CALL_STATE_NOT_ALLOWED = 'not-allowed';
export const CALL_STATE_AWAITING_ARGS = 'awaiting-args';
export const CALL_STATE_NOT_SECURE = 'not-secure';
export const useCallMachine = ({
domain,
@ -78,9 +79,10 @@ export const useCallMachine = ({
const join = useCallback(
async (callObject) => {
setState(CALL_STATE_JOINING);
const dailyRoomInfo = await callObject.room();
// Force mute clients when joining a call with experimental_optimize_large_calls enabled.
if (room?.config?.experimental_optimize_large_calls) {
if (dailyRoomInfo?.config?.experimental_optimize_large_calls) {
callObject.setLocalAudio(false);
}
@ -188,6 +190,15 @@ export const useCallMachine = ({
useEffect(() => {
if (daily || !url || state !== CALL_STATE_READY) return;
if (
location.protocol !== 'https:' &&
// We want to still allow local development.
!['localhost'].includes(location.hostname)
) {
setState('not-secure');
return;
}
console.log('🚀 Creating call object');
const co = DailyIframe.createCallObject({
@ -206,7 +217,7 @@ export const useCallMachine = ({
* Listen for changes in the participant's access state
*/
useEffect(() => {
if (!daily) return false;
if (!daily) return;
daily.on('access-state-updated', handleAccessStateUpdated);
return () => daily.off('access-state-updated', handleAccessStateUpdated);

View File

@ -15,7 +15,9 @@ export const DEVICE_STATE_SENDABLE = 'sendable';
export const useDevices = (callObject) => {
const [deviceState, setDeviceState] = useState(DEVICE_STATE_LOADING);
const [currentDevices, setCurrentDevices] = useState(null);
const [currentCam, setCurrentCam] = useState(null);
const [currentMic, setCurrentMic] = useState(null);
const [currentSpeaker, setCurrentSpeaker] = useState(null);
const [cams, setCams] = useState([]);
const [mics, setMics] = useState([]);
@ -38,6 +40,10 @@ export const useDevices = (callObject) => {
const { camera, mic, speaker } = await callObject.getInputDevices();
setCurrentCam(camera ?? null);
setCurrentMic(mic ?? null);
setCurrentSpeaker(speaker ?? null);
const [defaultCam, ...videoDevices] = devices.filter(
(d) => d.kind === 'videoinput' && d.deviceId !== ''
);
@ -66,12 +72,6 @@ export const useDevices = (callObject) => {
].filter(Boolean)
);
setCurrentDevices({
camera,
mic,
speaker,
});
console.log(`Current cam: ${camera.label}`);
console.log(`Current mic: ${mic.label}`);
console.log(`Current speakers: ${speaker.label}`);
@ -125,31 +125,26 @@ export const useDevices = (callObject) => {
const handleParticipantUpdated = useCallback(
({ participant }) => {
if (!callObject || !participant.local) return;
if (!callObject || deviceState === 'not-supported' || !participant.local) return;
setDeviceState((prevState) => {
if (prevState === DEVICE_STATE_NOT_SUPPORTED) return prevState;
switch (participant?.tracks.video.state) {
case DEVICE_STATE_BLOCKED:
updateDeviceErrors();
return DEVICE_STATE_ERROR;
case DEVICE_STATE_OFF:
case DEVICE_STATE_PLAYABLE:
if (prevState === DEVICE_STATE_GRANTED) {
return prevState;
}
updateDeviceState();
return DEVICE_STATE_GRANTED;
default:
return prevState;
}
});
switch (participant?.tracks.video.state) {
case DEVICE_STATE_BLOCKED:
setDeviceState(DEVICE_STATE_ERROR);
break;
case DEVICE_STATE_OFF:
case DEVICE_STATE_PLAYABLE:
updateDeviceState();
setDeviceState(DEVICE_STATE_GRANTED);
break;
}
updateDeviceErrors();
},
[callObject, updateDeviceState, updateDeviceErrors]
[callObject, deviceState, updateDeviceErrors, updateDeviceState]
);
useEffect(() => {
if (!callObject) return false;
if (!callObject) return;
/**
If the user is slow to allow access, we'll update the device state
@ -169,6 +164,7 @@ export const useDevices = (callObject) => {
updateDeviceState();
};
updateDeviceState();
callObject.on('joining-meeting', handleJoiningMeeting);
callObject.on('joined-meeting', handleJoinedMeeting);
callObject.on('participant-updated', handleParticipantUpdated);
@ -180,74 +176,8 @@ export const useDevices = (callObject) => {
};
}, [callObject, handleParticipantUpdated, updateDeviceState]);
const setCamDevice = useCallback(
async (newCam, useLocalStorage = true) => {
if (!callObject || newCam.deviceId === currentDevices?.cam?.deviceId) {
return;
}
console.log(`🔛 Changing camera device to: ${newCam.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultCamId', newCam.deviceId);
}
await callObject.setInputDevicesAsync({
videoDeviceId: newCam.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, camera: newCam }));
},
[callObject, currentDevices]
);
const setMicDevice = useCallback(
async (newMic, useLocalStorage = true) => {
if (!callObject || newMic.deviceId === currentDevices?.mic?.deviceId) {
return;
}
console.log(`🔛 Changing mic device to: ${newMic.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultMicId', newMic.deviceId);
}
await callObject.setInputDevicesAsync({
audioDeviceId: newMic.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, mic: newMic }));
},
[callObject, currentDevices]
);
const setSpeakersDevice = useCallback(
async (newSpeakers, useLocalStorage = true) => {
if (
!callObject ||
newSpeakers.deviceId === currentDevices?.speaker?.deviceId
) {
return;
}
console.log(`Changing speakers device to: ${newSpeakers.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultSpeakersId', newSpeakers.deviceId);
}
callObject.setOutputDevice({
outputDeviceId: newSpeakers.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, speaker: newSpeakers }));
},
[callObject, currentDevices]
);
useEffect(() => {
if (!callObject) return false;
if (!callObject) return;
console.log('💻 Device provider events bound');
@ -313,16 +243,19 @@ export const useDevices = (callObject) => {
}, [callObject, updateDeviceErrors]);
return {
cams,
mics,
speakers,
camError,
micError,
currentDevices,
cams,
currentCam,
currentMic,
currentSpeaker,
deviceState,
setCamDevice,
setMicDevice,
setSpeakersDevice,
micError,
mics,
refreshDevices: updateDeviceState,
setCurrentCam,
setCurrentMic,
setCurrentSpeaker,
speakers,
};
};

View File

@ -1,15 +1,13 @@
export const sortByKey = (a, b, key, caseSensitive = true) => {
const aKey =
!caseSensitive && typeof a[key] === 'string'
? a[key]?.toLowerCase()
? String(a[key])?.toLowerCase()
: a[key];
const bKey =
!caseSensitive && typeof b[key] === 'string'
? b[key]?.toLowerCase()
? String(b[key])?.toLowerCase()
: b[key];
if (aKey > bKey) return 1;
if (aKey < bKey) return -1;
return 0;
};
export default sortByKey;
};