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 { Audio } from '@custom/shared/components/Audio';
import { BasicTray } from '@custom/shared/components/Tray'; import { BasicTray } from '@custom/shared/components/Tray';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; 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 PropTypes from 'prop-types';
import { WaitingRoom } from './WaitingRoom'; import { WaitingRoom } from './WaitingRoom';

View File

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

View File

@ -1,36 +1,10 @@
import React, { useMemo, forwardRef, memo, useEffect, useState } from 'react'; import React, { useMemo, forwardRef, memo, useEffect } from 'react';
import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { isScreenId } from '@custom/shared/contexts/participantsState';
import Bowser from 'bowser'; import Bowser from 'bowser';
import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { shallowEqualObjects } from 'shallow-equal'; import { shallowEqualObjects } from 'shallow-equal';
import { useDeepCompareMemo } from 'use-deep-compare';
export const Video = memo( export const Video = memo(
forwardRef(({ fit = 'contain', participantId, videoTrack, ...rest }, videoEl) => { forwardRef(({ 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)]
);
/** /**
* Memo: Chrome >= 92? * Memo: Chrome >= 92?
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=1232649 * 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. * Effect: Umount
*/ * Note: nullify src to ensure media object is not counted
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.
*/ */
useEffect(() => { useEffect(() => {
const video = videoEl.current; const video = videoEl.current;
if (!video) return; if (!video) return false;
const handleCanPlay = () => { // clean up when video renders for different participant
if (!video.paused) return; video.srcObject = null;
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 (isChrome92) video.load(); if (isChrome92) video.load();
return () => { return () => {
// clean up when unmounted // clean up when unmounted
video.srcObject = null; video.srcObject = null;
if (isChrome92) video.load(); if (isChrome92) video.load();
}; };
}, [isChrome92, participantId, videoEl, videoTrack, videoTrack?.id]); }, [videoEl, isChrome92, participantId]);
return ( /**
<> * Effect: mount source (and force load on Chrome)
<video */
className={classNames(fit, { useEffect(() => {
isMirrored, const video = videoEl.current;
isMobile, if (!video || !videoTrack) return;
playable: isPlayable && videoTrack?.enabled, video.srcObject = new MediaStream([videoTrack]);
})} if (isChrome92) video.load();
autoPlay }, [videoEl, isChrome92, videoTrack]);
muted
playsInline return <video autoPlay muted playsInline ref={videoEl} {...rest} />;
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>
</>
);
}), }),
(p, n) => shallowEqualObjects(p, n) (p, n) => shallowEqualObjects(p, n)
); );

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 PropTypes from 'prop-types';
import { useCallState } from './CallProvider'; import { useCallState } from './CallProvider';
@ -12,33 +12,72 @@ export const MediaDeviceProvider = ({ children }) => {
const { localParticipant } = useParticipants(); const { localParticipant } = useParticipants();
const { const {
cams,
mics,
speakers,
camError, camError,
micError, cams,
currentDevices, currentCam,
currentMic,
currentSpeaker,
deviceState, deviceState,
setMicDevice, micError,
setCamDevice, mics,
setSpeakersDevice, refreshDevices,
setCurrentCam,
setCurrentMic,
setCurrentSpeaker,
speakers,
} = useDevices(callObject); } = 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 ( return (
<MediaDeviceContext.Provider <MediaDeviceContext.Provider
value={{ value={{
cams,
mics,
speakers,
camError, camError,
micError, cams,
currentDevices, currentCam,
currentMic,
currentSpeaker,
deviceState, deviceState,
isCamMuted: localParticipant.isCamMuted, isCamMuted: localParticipant.isCamMuted,
isMicMuted: localParticipant.isMicMuted, isMicMuted: localParticipant.isMicMuted,
setMicDevice, micError,
setCamDevice, mics,
setSpeakersDevice, refreshDevices,
setCurrentCam: selectCamera,
setCurrentMic: selectMic,
setCurrentSpeaker: selectSpeaker,
speakers,
}} }}
> >
{children} {children}

View File

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

View File

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

View File

@ -20,8 +20,8 @@ export const UIStateProvider = ({
customTrayComponent, customTrayComponent,
children, children,
}) => { }) => {
const [isMobile, setIsMobile] = useState(false);
const [pinnedId, setPinnedId] = useState(null); const [pinnedId, setPinnedId] = useState(null);
const [isMobile, setIsMobile] = useState(false);
const [preferredViewMode, setPreferredViewMode] = useState(VIEW_MODE_SPEAKER); const [preferredViewMode, setPreferredViewMode] = useState(VIEW_MODE_SPEAKER);
const [viewMode, setViewMode] = useState(preferredViewMode); const [viewMode, setViewMode] = useState(preferredViewMode);
const [isShowingScreenshare, setIsShowingScreenshare] = useState(false); const [isShowingScreenshare, setIsShowingScreenshare] = useState(false);
@ -31,6 +31,20 @@ export const UIStateProvider = ({
const [customCapsule, setCustomCapsule] = useState(); const [customCapsule, setCustomCapsule] = useState();
const [showAutoplayFailedModal, setShowAutoplayFailedModal] = useState(false); 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) => { const openModal = useCallback((modalName) => {
setActiveModals((prevState) => ({ setActiveModals((prevState) => ({
...prevState, ...prevState,

View File

@ -22,7 +22,6 @@ const initialParticipantsState = {
camMutedByHost: false, camMutedByHost: false,
hasNameSet: false, hasNameSet: false,
id: 'local', id: 'local',
user_id: '',
isActiveSpeaker: false, isActiveSpeaker: false,
isCamMuted: false, isCamMuted: false,
isLoading: true, isLoading: true,
@ -34,6 +33,7 @@ const initialParticipantsState = {
lastActiveDate: null, lastActiveDate: null,
micMutedByHost: false, micMutedByHost: false,
name: '', name: '',
sessionId: '',
}, },
], ],
screens: [], screens: [],
@ -42,7 +42,7 @@ const initialParticipantsState = {
// --- Derived data --- // --- Derived data ---
function getId(participant) { function getId(participant) {
return participant.local ? 'local' : participant.user_id; return participant.local ? 'local' : participant.session_id;
} }
function getScreenId(id) { function getScreenId(id) {
@ -69,28 +69,25 @@ function getNewParticipant(participant) {
camMutedByHost: video?.off?.byRemoteRequest, camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name, hasNameSet: !!participant.user_name,
id, id,
user_id: participant.user_id,
isActiveSpeaker: false, isActiveSpeaker: false,
isCamMuted: isCamMuted: video?.state === 'off' || video?.state === 'blocked',
video?.state === DEVICE_STATE_OFF || isLoading: audio?.state === 'loading' || video?.state === 'loading',
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isLocal: local, isLocal: local,
isMicMuted: isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isOwner: !!participant.owner, isOwner: !!participant.owner,
isRecording: !!participant.record, isRecording: !!participant.record,
isScreenshare: false, isScreenshare: false,
lastActiveDate: null, lastActiveDate: null,
micMutedByHost: audio?.off?.byRemoteRequest, micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name, name: participant.user_name,
sessionId: participant.session_id,
}; };
} }
function getUpdatedParticipant(participant, participants) { function getUpdatedParticipant(
participant,
participants
) {
const id = getId(participant); const id = getId(participant);
const prevItem = participants.find((p) => p.id === id); const prevItem = participants.find((p) => p.id === id);
@ -99,26 +96,21 @@ function getUpdatedParticipant(participant, participants) {
const { local } = participant; const { local } = participant;
const { audio, video } = participant.tracks; const { audio, video } = participant.tracks;
return { return {
...prevItem, ...prevItem,
camMutedByHost: video?.off?.byRemoteRequest, camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name, hasNameSet: !!participant.user_name,
id, id,
user_id: participant.user_id, isCamMuted: video?.state === 'off' || video?.state === 'blocked',
isCamMuted: isLoading: audio?.state === 'loading' || video?.state === 'loading',
video?.state === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isLocal: local, isLocal: local,
isMicMuted: isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isOwner: !!participant.owner, isOwner: !!participant.owner,
isRecording: !!participant.record, isRecording: !!participant.record,
micMutedByHost: audio?.off?.byRemoteRequest, micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name, name: participant.user_name,
sessionId: participant.session_id,
}; };
} }
@ -132,6 +124,7 @@ function getScreenItem(participant) {
isScreenshare: true, isScreenshare: true,
lastActiveDate: null, lastActiveDate: null,
name: participant.user_name, 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_FOUND = 'not-found';
export const CALL_STATE_NOT_ALLOWED = 'not-allowed'; export const CALL_STATE_NOT_ALLOWED = 'not-allowed';
export const CALL_STATE_AWAITING_ARGS = 'awaiting-args'; export const CALL_STATE_AWAITING_ARGS = 'awaiting-args';
export const CALL_STATE_NOT_SECURE = 'not-secure';
export const useCallMachine = ({ export const useCallMachine = ({
domain, domain,
@ -78,9 +79,10 @@ export const useCallMachine = ({
const join = useCallback( const join = useCallback(
async (callObject) => { async (callObject) => {
setState(CALL_STATE_JOINING); setState(CALL_STATE_JOINING);
const dailyRoomInfo = await callObject.room();
// Force mute clients when joining a call with experimental_optimize_large_calls enabled. // 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); callObject.setLocalAudio(false);
} }
@ -188,6 +190,15 @@ export const useCallMachine = ({
useEffect(() => { useEffect(() => {
if (daily || !url || state !== CALL_STATE_READY) return; 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'); console.log('🚀 Creating call object');
const co = DailyIframe.createCallObject({ const co = DailyIframe.createCallObject({
@ -206,7 +217,7 @@ export const useCallMachine = ({
* Listen for changes in the participant's access state * Listen for changes in the participant's access state
*/ */
useEffect(() => { useEffect(() => {
if (!daily) return false; if (!daily) return;
daily.on('access-state-updated', handleAccessStateUpdated); daily.on('access-state-updated', handleAccessStateUpdated);
return () => daily.off('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) => { export const useDevices = (callObject) => {
const [deviceState, setDeviceState] = useState(DEVICE_STATE_LOADING); 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 [cams, setCams] = useState([]);
const [mics, setMics] = useState([]); const [mics, setMics] = useState([]);
@ -38,6 +40,10 @@ export const useDevices = (callObject) => {
const { camera, mic, speaker } = await callObject.getInputDevices(); const { camera, mic, speaker } = await callObject.getInputDevices();
setCurrentCam(camera ?? null);
setCurrentMic(mic ?? null);
setCurrentSpeaker(speaker ?? null);
const [defaultCam, ...videoDevices] = devices.filter( const [defaultCam, ...videoDevices] = devices.filter(
(d) => d.kind === 'videoinput' && d.deviceId !== '' (d) => d.kind === 'videoinput' && d.deviceId !== ''
); );
@ -66,12 +72,6 @@ export const useDevices = (callObject) => {
].filter(Boolean) ].filter(Boolean)
); );
setCurrentDevices({
camera,
mic,
speaker,
});
console.log(`Current cam: ${camera.label}`); console.log(`Current cam: ${camera.label}`);
console.log(`Current mic: ${mic.label}`); console.log(`Current mic: ${mic.label}`);
console.log(`Current speakers: ${speaker.label}`); console.log(`Current speakers: ${speaker.label}`);
@ -125,31 +125,26 @@ export const useDevices = (callObject) => {
const handleParticipantUpdated = useCallback( const handleParticipantUpdated = useCallback(
({ participant }) => { ({ participant }) => {
if (!callObject || !participant.local) return; if (!callObject || deviceState === 'not-supported' || !participant.local) return;
setDeviceState((prevState) => { switch (participant?.tracks.video.state) {
if (prevState === DEVICE_STATE_NOT_SUPPORTED) return prevState; case DEVICE_STATE_BLOCKED:
switch (participant?.tracks.video.state) { setDeviceState(DEVICE_STATE_ERROR);
case DEVICE_STATE_BLOCKED: break;
updateDeviceErrors(); case DEVICE_STATE_OFF:
return DEVICE_STATE_ERROR; case DEVICE_STATE_PLAYABLE:
case DEVICE_STATE_OFF: updateDeviceState();
case DEVICE_STATE_PLAYABLE: setDeviceState(DEVICE_STATE_GRANTED);
if (prevState === DEVICE_STATE_GRANTED) { break;
return prevState; }
}
updateDeviceState(); updateDeviceErrors();
return DEVICE_STATE_GRANTED;
default:
return prevState;
}
});
}, },
[callObject, updateDeviceState, updateDeviceErrors] [callObject, deviceState, updateDeviceErrors, updateDeviceState]
); );
useEffect(() => { useEffect(() => {
if (!callObject) return false; if (!callObject) return;
/** /**
If the user is slow to allow access, we'll update the device state If the user is slow to allow access, we'll update the device state
@ -169,6 +164,7 @@ export const useDevices = (callObject) => {
updateDeviceState(); updateDeviceState();
}; };
updateDeviceState();
callObject.on('joining-meeting', handleJoiningMeeting); callObject.on('joining-meeting', handleJoiningMeeting);
callObject.on('joined-meeting', handleJoinedMeeting); callObject.on('joined-meeting', handleJoinedMeeting);
callObject.on('participant-updated', handleParticipantUpdated); callObject.on('participant-updated', handleParticipantUpdated);
@ -180,74 +176,8 @@ export const useDevices = (callObject) => {
}; };
}, [callObject, handleParticipantUpdated, updateDeviceState]); }, [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(() => { useEffect(() => {
if (!callObject) return false; if (!callObject) return;
console.log('💻 Device provider events bound'); console.log('💻 Device provider events bound');
@ -313,16 +243,19 @@ export const useDevices = (callObject) => {
}, [callObject, updateDeviceErrors]); }, [callObject, updateDeviceErrors]);
return { return {
cams,
mics,
speakers,
camError, camError,
micError, cams,
currentDevices, currentCam,
currentMic,
currentSpeaker,
deviceState, deviceState,
setCamDevice, micError,
setMicDevice, mics,
setSpeakersDevice, refreshDevices: updateDeviceState,
setCurrentCam,
setCurrentMic,
setCurrentSpeaker,
speakers,
}; };
}; };

View File

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