diff --git a/custom/shared/components/Tile/Tile.js b/custom/shared/components/Tile/Tile.js
index b6d28bd..621eb39 100644
--- a/custom/shared/components/Tile/Tile.js
+++ b/custom/shared/components/Tile/Tile.js
@@ -4,7 +4,7 @@ import { ReactComponent as IconMicMute } from '@custom/shared/icons/mic-off-sm.s
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { DEFAULT_ASPECT_RATIO } from '../../constants';
-import Video from './Video';
+import { Video } from './Video';
import { ReactComponent as Avatar } from './avatar.svg';
const SM_TILE_MAX_WIDTH = 300;
@@ -99,8 +99,9 @@ export const Tile = memo(
{videoTrack ? (
) : (
showAvatar && (
diff --git a/custom/shared/components/Tile/Video.js b/custom/shared/components/Tile/Video.js
index c9541cc..267057e 100644
--- a/custom/shared/components/Tile/Video.js
+++ b/custom/shared/components/Tile/Video.js
@@ -1,14 +1,30 @@
-import React, { useMemo, forwardRef, memo, useEffect } from 'react';
+import {
+ forwardRef,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
import Bowser from 'bowser';
-import PropTypes from 'prop-types';
-import { shallowEqualObjects } from 'shallow-equal';
+import classNames from 'classnames';
+
+import { useCallState } from '../../contexts/CallProvider';
+import { useUIState } from '../../contexts/UIStateProvider';
+import { useVideoTrack } from '../../hooks/useVideoTrack';
+
+export const Video = forwardRef(
+ (
+ { fit = 'contain', isScreen = false, participantId, ...props },
+ videoEl
+ ) => {
+ const { callObject: daily } = useCallState();
+ const { isMobile } = useUIState();
+ const isLocalCam = useMemo(() => {
+ const localParticipant = daily.participants()?.local;
+ return participantId === localParticipant.session_id && !isScreen;
+ }, [daily, isScreen, participantId]);
+ const [isMirrored, setIsMirrored] = useState(isLocalCam);
+ const videoTrack = useVideoTrack(participantId);
-export const Video = memo(
- forwardRef(({ participantId, videoTrack, ...rest }, videoEl) => {
- /**
- * Memo: Chrome >= 92?
- * See: https://bugs.chromium.org/p/chromium/issues/detail?id=1232649
- */
const isChrome92 = useMemo(() => {
const { browser, platform, os } = Bowser.parse(navigator.userAgent);
return (
@@ -19,43 +35,114 @@ export const Video = memo(
}, []);
/**
- * Effect: Umount
- * Note: nullify src to ensure media object is not counted
+ * 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.
*/
useEffect(() => {
const video = videoEl.current;
- 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();
+ if (!video) return;
+ const handleCanPlay = () => {
+ if (!video.paused) return;
+ video.play();
};
- }, [videoEl, isChrome92, participantId]);
+ 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]);
/**
- * Effect: mount source (and force load on Chrome)
+ * Update srcObject.
*/
useEffect(() => {
const video = videoEl.current;
if (!video || !videoTrack) return;
video.srcObject = new MediaStream([videoTrack]);
if (isChrome92) video.load();
- }, [videoEl, isChrome92, videoTrack]);
+ return () => {
+ // clean up when unmounted
+ video.srcObject = null;
+ if (isChrome92) video.load();
+ };
+ }, [isChrome92, participantId, videoEl, videoTrack, videoTrack?.id]);
- return ;
- }),
- (p, n) => shallowEqualObjects(p, n)
+ return (
+ <>
+
+
+ >
+ );
+ }
);
-
-Video.displayName = 'Video';
-
-Video.propTypes = {
- videoTrack: PropTypes.any,
- mirrored: PropTypes.bool,
- participantId: PropTypes.string,
-};
-
-export default Video;
\ No newline at end of file
+Video.displayName = 'Video';
\ No newline at end of file
diff --git a/custom/shared/contexts/ParticipantsProvider.js b/custom/shared/contexts/ParticipantsProvider.js
index 57a1156..5df6b7d 100644
--- a/custom/shared/contexts/ParticipantsProvider.js
+++ b/custom/shared/contexts/ParticipantsProvider.js
@@ -1,44 +1,36 @@
-import React, {
+import {
createContext,
useCallback,
useContext,
useEffect,
+ useMemo,
useReducer,
useState,
- useMemo,
} from 'react';
-import {
- useUIState,
-} from '@custom/shared/contexts/UIStateProvider';
-import PropTypes from 'prop-types';
-import { sortByKey } from '../lib/sortByKey';
-
+import { sortByKey } from '@custom/shared/lib/sortByKey';
+import { useNetworkState } from '../hooks/useNetworkState';
import { useCallState } from './CallProvider';
-
+import { useUIState } from './UIStateProvider';
import {
initialParticipantsState,
isLocalId,
- ACTIVE_SPEAKER,
- PARTICIPANT_JOINED,
- PARTICIPANT_LEFT,
- PARTICIPANT_UPDATED,
participantsReducer,
- SWAP_POSITION,
} from './participantsState';
-export const ParticipantsContext = createContext();
+export const ParticipantsContext = createContext(null);
export const ParticipantsProvider = ({ children }) => {
- const { callObject, videoQuality, networkState, broadcast } = useCallState();
- const [state, dispatch] = useReducer(
- participantsReducer,
- initialParticipantsState
- );
- const { viewMode } = useUIState();
- const [
- participantMarkedForRemoval,
- setParticipantMarkedForRemoval,
- ] = useState(null);
+ const { isMobile, pinnedId, viewMode } = useUIState();
+ const {
+ broadcast,
+ broadcastRole,
+ callObject: daily,
+ videoQuality,
+ } = useCallState();
+ const [state, dispatch] = useReducer(participantsReducer, initialParticipantsState);
+ const [participantMarkedForRemoval, setParticipantMarkedForRemoval] = useState(null);
+
+ const { threshold } = useNetworkState();
/**
* ALL participants (incl. shared screens) in a convenient array
@@ -58,14 +50,6 @@ export const ParticipantsProvider = ({ children }) => {
return state.participants;
}, [broadcast, state.participants]);
- /**
- * Array of participant IDs
- */
- const participantIds = useMemo(
- () => participants.map((p) => p.id).join(','),
- [participants]
- );
-
/**
* The number of participants, who are not a shared screen
* (technically a shared screen counts as a participant, but we shouldn't tell humans)
@@ -103,28 +87,26 @@ export const ParticipantsProvider = ({ children }) => {
*/
const currentSpeaker = useMemo(() => {
/**
- * If the activeParticipant is still in the call, return the activeParticipant.
+ * Ensure activeParticipant is still present in the call.
* The activeParticipant only updates to a new active participant so
* if everyone else is muted when AP leaves, the value will be stale.
*/
const isPresent = participants.some((p) => p?.id === activeParticipant?.id);
- if (isPresent) {
- return activeParticipant;
- }
+ const pinned = participants.find((p) => p?.id === pinnedId);
- /**
- * If the activeParticipant has left, calculate the remaining displayable participants
- */
- const displayableParticipants = participants.filter((p) => !p?.isLocal);
+ if (pinned) return pinned;
+
+ const displayableParticipants = participants.filter((p) =>
+ isMobile ? !p?.isLocal && !p?.isScreenshare : !p?.isLocal
+ );
- /**
- * If nobody ever unmuted, return the first participant with a camera on
- * Or, if all cams are off, return the first remote participant
- */
if (
+ !isPresent &&
displayableParticipants.length > 0 &&
displayableParticipants.every((p) => p.isMicMuted && !p.lastActiveDate)
) {
+ // Return first cam on participant in case everybody is muted and nobody ever talked
+ // or first remote participant, in case everybody's cam is muted, too.
return (
displayableParticipants.find((p) => !p.isCamMuted) ??
displayableParticipants?.[0]
@@ -132,13 +114,20 @@ export const ParticipantsProvider = ({ children }) => {
}
const sorted = displayableParticipants
- .sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
+ .sort(sortByKey('lastActiveDate'))
.reverse();
- const lastActiveSpeaker = sorted?.[0];
+ const fallback = broadcastRole === 'attendee' ? null : localParticipant;
- return lastActiveSpeaker || localParticipant;
- }, [activeParticipant, localParticipant, participants]);
+ return isPresent ? activeParticipant : sorted?.[0] ?? fallback;
+ }, [
+ activeParticipant,
+ broadcastRole,
+ isMobile,
+ localParticipant,
+ participants,
+ pinnedId,
+ ]);
/**
* Screen shares
@@ -148,33 +137,17 @@ export const ParticipantsProvider = ({ children }) => {
/**
* The local participant's name
*/
- const username = callObject?.participants()?.local?.user_name ?? '';
+ const username = daily?.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 [muteNewParticipants, setMuteNewParticipants] = useState(false);
-
- const muteAll = useCallback(
- (muteFutureParticipants = false) => {
- if (!localParticipant.isOwner) return;
- setMuteNewParticipants(muteFutureParticipants);
- const unmutedParticipants = participants.filter(
- (p) => !p.isLocal && !p.isMicMuted
- );
- if (!unmutedParticipants.length) return;
- const result = unmutedParticipants.reduce(
- (o, p) => ({ ...o[p.id], setAudio: false }),
- {}
- );
- callObject.updateParticipants(result);
+ const setUsername = useCallback(
+ (name) => {
+ daily.setUserName(name);
},
- [callObject, localParticipant, participants]
+ [daily]
);
const swapParticipantPosition = useCallback((id1, id2) => {
@@ -192,68 +165,89 @@ export const ParticipantsProvider = ({ children }) => {
});
}, []);
+ const [muteNewParticipants, setMuteNewParticipants] = useState(false);
+
+ const muteAll = useCallback(
+ (muteFutureParticipants = false) => {
+ if (!localParticipant.isOwner) return;
+ setMuteNewParticipants(muteFutureParticipants);
+ const unmutedParticipants = participants.filter(
+ (p) => !p.isLocal && !p.isMicMuted
+ );
+ if (!unmutedParticipants.length) return;
+ daily.updateParticipants(
+ unmutedParticipants.reduce((o, p) => {
+ o[p.id] = {
+ setAudio: false,
+ };
+ return o;
+ }, {})
+ );
+ },
+ [daily, localParticipant, participants]
+ );
+
+ const handleParticipantJoined = useCallback(() => {
+ dispatch({
+ type: 'JOINED_MEETING',
+ participant: daily.participants().local,
+ });
+ }, [daily]);
+
const handleNewParticipantsState = useCallback(
(event = null) => {
switch (event?.action) {
case 'participant-joined':
dispatch({
- type: PARTICIPANT_JOINED,
+ type: 'PARTICIPANT_JOINED',
participant: event.participant,
});
+ if (muteNewParticipants && daily) {
+ daily.updateParticipant(event.participant.session_id, {
+ setAudio: false,
+ });
+ }
break;
case 'participant-updated':
dispatch({
- type: PARTICIPANT_UPDATED,
+ type: 'PARTICIPANT_UPDATED',
participant: event.participant,
});
break;
case 'participant-left':
dispatch({
- type: PARTICIPANT_LEFT,
+ type: 'PARTICIPANT_LEFT',
participant: event.participant,
});
break;
- default:
- break;
}
},
- [dispatch]
+ [daily, dispatch, muteNewParticipants]
);
- /**
- * Start listening for participant changes, when the callObject is set.
- */
useEffect(() => {
- if (!callObject) return false;
+ if (!daily) return;
- console.log('👥 Participant provider events bound');
+ daily.on('participant-joined', handleParticipantJoined);
+ daily.on('participant-joined', handleNewParticipantsState);
+ daily.on('participant-updated', handleNewParticipantsState);
+ daily.on('participant-left', handleNewParticipantsState);
- const events = [
- 'joined-meeting',
- 'participant-joined',
- 'participant-updated',
- 'participant-left',
- ];
+ return () => {
+ daily.off('participant-joined', handleParticipantJoined);
+ daily.off('participant-joined', handleNewParticipantsState);
+ daily.off('participant-updated', handleNewParticipantsState);
+ daily.off('participant-left', handleNewParticipantsState);
+ };
+ }, [daily, handleNewParticipantsState, handleParticipantJoined]);
- // Use initial state
- handleNewParticipantsState();
-
- // Listen for changes in state
- events.forEach((event) => callObject.on(event, handleNewParticipantsState));
-
- // Stop listening for changes in state
- return () =>
- events.forEach((event) =>
- callObject.off(event, handleNewParticipantsState)
- );
- }, [callObject, handleNewParticipantsState]);
-
- /**
- * Change between the simulcast layers based on view / available bandwidth
- */
+ const participantIds = useMemo(
+ () => participants.map((p) => p.id).join(','),
+ [participants]
+ );
const setBandWidthControls = useCallback(() => {
- if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
+ if (!(daily && daily.meetingState() === 'joined-meeting')) return;
const ids = participantIds.split(',').filter(Boolean);
const receiveSettings = {};
@@ -263,7 +257,7 @@ export const ParticipantsProvider = ({ children }) => {
if (
// weak or bad network
- (['low', 'very-low'].includes(networkState) && videoQuality === 'auto') ||
+ (['low', 'very-low'].includes(threshold) && videoQuality === 'auto') ||
// Low quality or Bandwidth saver mode enabled
['bandwidth-saver', 'low'].includes(videoQuality)
) {
@@ -284,38 +278,44 @@ export const ParticipantsProvider = ({ children }) => {
// Mobile view settings are handled separately in MobileCall
});
- callObject.updateReceiveSettings(receiveSettings);
- }, [callObject, participantIds, networkState, videoQuality, viewMode, currentSpeaker?.id]);
+ daily.updateReceiveSettings(receiveSettings);
+ }, [
+ currentSpeaker?.id,
+ daily,
+ participantIds,
+ threshold,
+ videoQuality,
+ viewMode,
+ ]);
useEffect(() => {
setBandWidthControls();
}, [setBandWidthControls]);
useEffect(() => {
- if (!callObject) return false;
+ if (!daily) return;
const handleActiveSpeakerChange = ({ activeSpeaker }) => {
/**
* Ignore active-speaker-change events for the local user.
* Our UX doesn't ever highlight the local user as the active speaker.
*/
- const localId = callObject.participants().local.session_id;
+ const localId = daily.participants().local.session_id;
const activeSpeakerId = activeSpeaker?.peerId;
if (localId === activeSpeakerId) return;
dispatch({
- type: ACTIVE_SPEAKER,
+ type: 'ACTIVE_SPEAKER',
id: activeSpeakerId,
});
};
- callObject.on('active-speaker-change', handleActiveSpeakerChange);
+ daily.on('active-speaker-change', handleActiveSpeakerChange);
return () =>
- callObject.off('active-speaker-change', handleActiveSpeakerChange);
- }, [callObject]);
+ daily.off('active-speaker-change', handleActiveSpeakerChange);
+ }, [daily]);
return (
{
);
};
-ParticipantsProvider.propTypes = {
- children: PropTypes.node,
-};
-
-export const useParticipants = () => useContext(ParticipantsContext);
+export const useParticipants = () => useContext(ParticipantsContext);
\ No newline at end of file
diff --git a/custom/shared/contexts/TracksProvider.js b/custom/shared/contexts/TracksProvider.js
index 06931ba..d4f17e5 100644
--- a/custom/shared/contexts/TracksProvider.js
+++ b/custom/shared/contexts/TracksProvider.js
@@ -1,38 +1,29 @@
-/* global rtcpeers */
-
-import React, {
+import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
+ useRef,
+ useState,
} from 'react';
-
+import { sortByKey } from '@custom/shared/lib/sortByKey';
import deepEqual from 'fast-deep-equal';
-import PropTypes from 'prop-types';
-import { useDeepCompareEffect } from 'use-deep-compare';
-import { sortByKey } from '../lib/sortByKey';
+import { useDeepCompareCallback } from 'use-deep-compare';
+
import { useCallState } from './CallProvider';
import { useParticipants } from './ParticipantsProvider';
import { useUIState } from './UIStateProvider';
-import { isLocalId, isScreenId } from './participantsState';
-import {
- initialTracksState,
- REMOVE_TRACKS,
- TRACK_STARTED,
- TRACK_STOPPED,
- TRACK_VIDEO_UPDATED,
- TRACK_AUDIO_UPDATED,
- tracksReducer,
-} from './tracksState';
+import { getScreenId, isLocalId, isScreenId } from './participantsState';
+import { initialTracksState, tracksReducer } from './tracksState';
/**
- * Maximum amount of concurrently subscribed most recent speakers.
+ * Maximum amount of concurrently subscribed or staged most recent speakers.
*/
-const MAX_RECENT_SPEAKER_COUNT = 6;
+export const MAX_RECENT_SPEAKER_COUNT = 8;
/**
- * Threshold up to which all videos will be subscribed.
+ * Threshold up to which all cams will be subscribed to or staged.
* If the remote participant count passes this threshold,
* cam subscriptions are defined by UI view modes.
*/
@@ -41,16 +32,17 @@ const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => {
- const { callObject, subscribeToTracksAutomatically } = useCallState();
+ const { callObject: daily, optimizeLargeCalls } = useCallState();
const { participants } = useParticipants();
const { viewMode } = useUIState();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
+ const [maxCamSubscriptions, setMaxCamSubscriptions] = useState(null);
const recentSpeakerIds = useMemo(
() =>
participants
- .filter((p) => Boolean(p.lastActiveDate) && !p.isLocal)
- .sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
+ .filter((p) => Boolean(p.lastActiveDate))
+ .sort(sortByKey('lastActiveDate'))
.slice(-MAX_RECENT_SPEAKER_COUNT)
.map((p) => p.id)
.reverse(),
@@ -64,22 +56,27 @@ export const TracksProvider = ({ children }) => {
const subscribeToCam = useCallback(
(id) => {
- // Ignore undefined, local or screenshare.
+ /**
+ * Ignore undefined, local or screenshare.
+ */
if (!id || isLocalId(id) || isScreenId(id)) return;
- callObject.updateParticipant(id, {
+ daily.updateParticipant(id, {
setSubscribedTracks: { video: true },
});
},
- [callObject]
+ [daily]
);
/**
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
- * For ids not provided, cam tracks will be unsubscribed from
+ * For ids not provided, cam tracks will be unsubscribed from.
+ *
+ * @param subscribedIds Participant ids whose cam tracks should be subscribed to.
+ * @param stagedIds Participant ids whose cam tracks should be staged.
*/
const updateCamSubscriptions = useCallback(
(subscribedIds, stagedIds = []) => {
- if (!callObject) return;
+ if (!daily) return;
// If total number of remote participants is less than a threshold, simply
// stage all remote cams that aren't already marked for subscription.
@@ -103,7 +100,7 @@ export const TracksProvider = ({ children }) => {
const updates = remoteParticipantIds.reduce((u, id) => {
let desiredSubscription;
const currentSubscription =
- callObject.participants()?.[id]?.tracks?.video?.subscribed;
+ daily.participants()?.[id]?.tracks?.video?.subscribed;
// Ignore undefined, local or screenshare participant ids
if (!id || isLocalId(id) || isScreenId(id)) return u;
@@ -131,110 +128,126 @@ export const TracksProvider = ({ children }) => {
}, {});
if (Object.keys(updates).length === 0) return;
- callObject.updateParticipants(updates);
+ daily.updateParticipants(updates);
},
- [callObject, remoteParticipantIds, viewMode, recentSpeakerIds]
+ [daily, remoteParticipantIds, recentSpeakerIds, viewMode]
);
+ /**
+ * Automatically update audio subscriptions.
+ */
useEffect(() => {
- if (!callObject) return false;
-
- const trackStoppedQueue = [];
-
- const handleTrackStarted = ({ participant, track }) => {
- /**
- * If track for participant was recently stopped, remove it from queue,
- * so we don't run into a stale state
- */
- const stoppingIdx = trackStoppedQueue.findIndex(
- ([p, t]) =>
- p.session_id === participant.session_id && t.kind === track.kind
- );
- if (stoppingIdx >= 0) {
- trackStoppedQueue.splice(stoppingIdx, 1);
- }
- dispatch({
- type: TRACK_STARTED,
- participant,
- track,
- });
- };
-
- const trackStoppedBatchInterval = setInterval(() => {
- if (!trackStoppedQueue.length) return;
- dispatch({
- type: TRACK_STOPPED,
- items: trackStoppedQueue.splice(0, trackStoppedQueue.length),
- });
- }, 3000);
-
- const handleTrackStopped = ({ participant, track }) => {
- if (participant) {
- trackStoppedQueue.push([participant, track]);
- }
- };
-
- const handleParticipantLeft = ({ participant }) => {
- dispatch({
- type: REMOVE_TRACKS,
- participant,
- });
- };
-
- const joinedSubscriptionQueue = [];
-
- const handleParticipantJoined = ({ participant }) => {
- joinedSubscriptionQueue.push(participant.session_id);
- };
-
- const joinBatchInterval = setInterval(async () => {
- if (!joinedSubscriptionQueue.length) return;
- const ids = joinedSubscriptionQueue.splice(0);
- const participants = callObject.participants();
- const topology = (await callObject.getNetworkTopology())?.topology;
- const updates = ids.reduce((o, id) => {
- if (!participants?.[id]?.tracks?.audio?.subscribed) {
- o[id] = {
+ if (!daily) return;
+ /**
+ * A little throttling as we want daily-js to have some room to breathe ☺️
+ */
+ const timeout = setTimeout(() => {
+ const participants = daily.participants();
+ const updates = remoteParticipantIds.reduce((u, id) => {
+ // Ignore undefined, local or screenshare participant ids
+ if (!id || isLocalId(id) || isScreenId(id)) return u;
+ const isSpeaker = recentSpeakerIds.includes(id);
+ const hasSubscribed = participants[id]?.tracks?.audio?.subscribed;
+ const shouldSubscribe = optimizeLargeCalls ? isSpeaker : true;
+ /**
+ * In optimized calls:
+ * - subscribe to speakers we're not subscribed to, yet
+ * - unsubscribe from non-speakers we're subscribed to
+ * In non-optimized calls:
+ * - subscribe to all who we're not to subscribed to, yet
+ */
+ if (
+ (!hasSubscribed && shouldSubscribe) ||
+ (hasSubscribed && !shouldSubscribe)
+ ) {
+ u[id] = {
setSubscribedTracks: {
- audio: true,
- screenAudio: true,
- screenVideo: true,
+ audio: shouldSubscribe,
},
};
}
- if (topology === 'peer') {
- o[id] = { setSubscribedTracks: true };
- }
- return o;
+ return u;
}, {});
-
- if (!subscribeToTracksAutomatically && Object.keys(updates).length0) {
- callObject.updateParticipants(updates);
- }
+ if (Object.keys(updates).length === 0) return;
+ daily.updateParticipants(updates);
}, 100);
-
- callObject.on('track-started', handleTrackStarted);
- callObject.on('track-stopped', handleTrackStopped);
- callObject.on('participant-joined', handleParticipantJoined);
- callObject.on('participant-left', handleParticipantLeft);
return () => {
- clearInterval(joinBatchInterval);
- clearInterval(trackStoppedBatchInterval);
- callObject.off('track-started', handleTrackStarted);
- callObject.off('track-stopped', handleTrackStopped);
- callObject.off('participant-joined', handleParticipantJoined);
- callObject.off('participant-left', handleParticipantLeft);
+ clearTimeout(timeout);
};
- }, [callObject, subscribeToTracksAutomatically]);
+ }, [daily, optimizeLargeCalls, recentSpeakerIds, remoteParticipantIds]);
- useDeepCompareEffect(() => {
- if (!callObject) return;
+ /**
+ * Notify user when pushed out of recent speakers queue.
+ */
+ const showMutedMessage = useRef(false);
+ useEffect(() => {
+ if (!daily || !optimizeLargeCalls) return;
- const handleParticipantUpdated = ({ participant }) => {
+ if (recentSpeakerIds.some((id) => isLocalId(id))) {
+ showMutedMessage.current = true;
+ return;
+ }
+ if (showMutedMessage.current && daily.participants().local.audio) {
+ daily.setLocalAudio(false);
+ showMutedMessage.current = false;
+ }
+ }, [daily, optimizeLargeCalls, recentSpeakerIds]);
+
+ const trackStoppedQueue = useRef([]);
+ useEffect(() => {
+ const trackStoppedBatchInterval = setInterval(() => {
+ if (!trackStoppedQueue.current.length) return;
+ dispatch({
+ type: 'TRACKS_STOPPED',
+ items: trackStoppedQueue.current.splice(
+ 0,
+ trackStoppedQueue.current.length
+ ),
+ });
+ }, 3000);
+ return () => {
+ clearInterval(trackStoppedBatchInterval);
+ };
+ }, []);
+
+ const handleTrackStarted = useCallback(({ participant, track }) => {
+ /**
+ * If track for participant was recently stopped, remove it from queue,
+ * so we don't run into a stale state.
+ */
+ const stoppingIdx = trackStoppedQueue.current.findIndex(
+ ([p, t]) =>
+ p.session_id === participant.session_id && t.kind === track.kind
+ );
+ if (stoppingIdx >= 0) {
+ trackStoppedQueue.current.splice(stoppingIdx, 1);
+ }
+ dispatch({
+ type: 'TRACK_STARTED',
+ participant,
+ track,
+ });
+ }, []);
+
+ const handleTrackStopped = useCallback(({ participant, track }) => {
+ if (participant) {
+ trackStoppedQueue.current.push([participant, track]);
+ }
+ }, []);
+
+ const handleParticipantJoined = useCallback(({ participant }) => {
+ joinedSubscriptionQueue.current.push(participant.session_id);
+ }, []);
+
+ const handleParticipantUpdated = useDeepCompareCallback(
+ ({ participant }) => {
const hasAudioChanged =
// State changed
- participant.tracks.audio?.state !==
- state.audioTracks?.[participant.user_id]?.state ||
+ participant.tracks.audio.state !==
+ state.audioTracks?.[participant.user_id]?.state ||
+ // Screen state changed
+ participant.tracks.screenAudio.state !==
+ state.audioTracks?.[getScreenId(participant.user_id)]?.state ||
// Off/blocked reason changed
!deepEqual(
{
@@ -242,14 +255,14 @@ export const TracksProvider = ({ children }) => {
...(participant.tracks.audio?.off ?? {}),
},
{
- ...(state.audioTracks?.[participant.user_id]?.blocked ?? {}),
- ...(state.audioTracks?.[participant.user_id]?.off ?? {}),
+ ...(state.audioTracks?.[participant.user_id].blocked ?? {}),
+ ...(state.audioTracks?.[participant.user_id].off ?? {}),
}
);
const hasVideoChanged =
// State changed
participant.tracks.video?.state !==
- state.videoTracks?.[participant.user_id]?.state ||
+ state.videoTracks?.[participant.user_id]?.state ||
// Off/blocked reason changed
!deepEqual(
{
@@ -265,7 +278,7 @@ export const TracksProvider = ({ children }) => {
if (hasAudioChanged) {
// Update audio track state
dispatch({
- type: TRACK_AUDIO_UPDATED,
+ type: 'UPDATE_AUDIO_TRACK',
participant,
});
}
@@ -273,27 +286,92 @@ export const TracksProvider = ({ children }) => {
if (hasVideoChanged) {
// Update video track state
dispatch({
- type: TRACK_VIDEO_UPDATED,
+ type: 'UPDATE_VIDEO_TRACK',
participant,
});
}
- };
+ },
+ [state.audioTracks, state.videoTracks]
+ );
- callObject.on('participant-updated', handleParticipantUpdated);
+ const handleParticipantLeft = useCallback(({ participant }) => {
+ dispatch({
+ type: 'REMOVE_TRACKS',
+ participant,
+ });
+ }, []);
+
+ useEffect(() => {
+ if (!daily) return;
+
+ daily.on('track-started', handleTrackStarted);
+ daily.on('track-stopped', handleTrackStopped);
+ daily.on('participant-joined', handleParticipantJoined);
+ daily.on('participant-updated', handleParticipantUpdated);
+ daily.on('participant-left', handleParticipantLeft);
return () => {
- callObject.off('participant-updated', handleParticipantUpdated);
+ daily.off('track-started', handleTrackStarted);
+ daily.off('track-stopped', handleTrackStopped);
+ daily.off('participant-joined', handleParticipantJoined);
+ daily.off('participant-updated', handleParticipantUpdated);
+ daily.off('participant-left', handleParticipantLeft);
};
- }, [callObject, state.audioTracks, state.videoTracks]);
+ }, [
+ daily,
+ handleParticipantJoined,
+ handleParticipantLeft,
+ handleParticipantUpdated,
+ handleTrackStarted,
+ handleTrackStopped
+ ]);
+
+ const joinedSubscriptionQueue = useRef([]);
+ useEffect(() => {
+ if (!daily) return;
+ const joinBatchInterval = setInterval(async () => {
+ if (!joinedSubscriptionQueue.current.length) return;
+ const ids = joinedSubscriptionQueue.current.splice(0);
+ const participants = daily.participants();
+ const topology = (await daily.getNetworkTopology())?.topology;
+ const updates = ids.reduce(
+ (o, id) => {
+ if (!participants?.[id]?.tracks?.audio?.subscribed) {
+ o[id] = {
+ setSubscribedTracks: {
+ screenAudio: true,
+ screenVideo: true,
+ },
+ };
+ }
+ if (topology === 'peer') {
+ o[id] = { setSubscribedTracks: true };
+ }
+ return o;
+ },
+ {}
+ );
+ if (Object.keys(updates).length === 0) return;
+ daily.updateParticipants(updates);
+ }, 100);
+ return () => {
+ clearInterval(joinBatchInterval);
+ };
+ }, [daily]);
+
+ useEffect(() => {
+ if (optimizeLargeCalls) {
+ setMaxCamSubscriptions(30);
+ }
+ }, [optimizeLargeCalls]);
return (
{children}
@@ -301,8 +379,4 @@ export const TracksProvider = ({ children }) => {
);
};
-TracksProvider.propTypes = {
- children: PropTypes.node,
-};
-
-export const useTracks = () => useContext(TracksContext);
+export const useTracks = () => useContext(TracksContext);
\ No newline at end of file
diff --git a/custom/shared/contexts/participantsState.js b/custom/shared/contexts/participantsState.js
index 081b8a5..6829d2e 100644
--- a/custom/shared/contexts/participantsState.js
+++ b/custom/shared/contexts/participantsState.js
@@ -1,19 +1,6 @@
-/**
- * Call state is comprised of:
- * - "Call items" (inputs to the call, i.e. participants or shared screens)
- * - UI state that depends on call items (for now, just whether to show "click allow" message)
- *
- * Call items are keyed by id:
- * - "local" for the current participant
- * - A session id for each remote participant
- * - "-screen" for each shared screen
- */
import fasteq from 'fast-deep-equal';
-import {
- DEVICE_STATE_OFF,
- DEVICE_STATE_BLOCKED,
- DEVICE_STATE_LOADING,
-} from './useDevices';
+
+import { MAX_RECENT_SPEAKER_COUNT } from './TracksProvider';
const initialParticipantsState = {
lastPendingUnknownActiveSpeaker: null,
@@ -39,26 +26,168 @@ const initialParticipantsState = {
screens: [],
};
-// --- Derived data ---
+// --- Reducer and helpers --
-function getId(participant) {
- return participant.local ? 'local' : participant.session_id;
+function participantsReducer(
+ prevState,
+ action
+) {
+ switch (action.type) {
+ case 'ACTIVE_SPEAKER': {
+ const { participants, ...state } = prevState;
+ if (!action.id)
+ return {
+ ...prevState,
+ lastPendingUnknownActiveSpeaker: null,
+ };
+ const date = new Date();
+ const isParticipantKnown = participants.some((p) => p.id === action.id);
+ return {
+ ...state,
+ lastPendingUnknownActiveSpeaker: isParticipantKnown
+ ? null
+ : {
+ date,
+ id: action.id,
+ },
+ participants: participants.map((p) => ({
+ ...p,
+ isActiveSpeaker: p.id === action.id,
+ lastActiveDate: p.id === action.id ? date : p?.lastActiveDate,
+ })),
+ };
+ }
+ case 'JOINED_MEETING': {
+ const localItem = getNewParticipant(action.participant);
+
+ const participants = [...prevState.participants].map((p) =>
+ p.isLocal ? localItem : p
+ );
+
+ return {
+ ...prevState,
+ participants,
+ };
+ }
+ case 'PARTICIPANT_JOINED': {
+ const item = getNewParticipant(action.participant);
+
+ const participants = [...prevState.participants];
+ const screens = [...prevState.screens];
+
+ const isPendingActiveSpeaker =
+ item.id === prevState.lastPendingUnknownActiveSpeaker?.id;
+ if (isPendingActiveSpeaker) {
+ item.isActiveSpeaker = true;
+ item.lastActiveDate = prevState.lastPendingUnknownActiveSpeaker?.date;
+ }
+
+ if (item.isCamMuted) {
+ participants.push(item);
+ } else {
+ const firstInactiveCamOffIndex = prevState.participants.findIndex(
+ (p) => p.isCamMuted && !p.isLocal && !p.isActiveSpeaker
+ );
+ if (firstInactiveCamOffIndex >= 0) {
+ participants.splice(firstInactiveCamOffIndex, 0, item);
+ } else {
+ participants.push(item);
+ }
+ }
+
+ // Mark new participant as active speaker, for quicker audio subscription
+ if (
+ !item.isMicMuted &&
+ participants.length <= MAX_RECENT_SPEAKER_COUNT + 1 // + 1 for local participant
+ ) {
+ item.lastActiveDate = new Date();
+ }
+
+ // Participant is sharing screen
+ if (action.participant.screen) {
+ screens.push(getScreenItem(action.participant));
+ }
+
+ return {
+ ...prevState,
+ lastPendingUnknownActiveSpeaker: isPendingActiveSpeaker
+ ? null
+ : prevState.lastPendingUnknownActiveSpeaker,
+ participants,
+ screens,
+ };
+ }
+ case 'PARTICIPANT_UPDATED': {
+ const item = getUpdatedParticipant(
+ action.participant,
+ prevState.participants
+ );
+ const { id } = item;
+ const screenId = getScreenId(id);
+
+ const participants = [...prevState.participants];
+ const idx = participants.findIndex((p) => p.id === id);
+ if (!item.isMicMuted && participants[idx].isMicMuted) {
+ // Participant unmuted mic
+ item.lastActiveDate = new Date();
+ }
+ participants[idx] = item;
+
+ const screens = [...prevState.screens];
+ const screenIdx = screens.findIndex((s) => s.id === screenId);
+
+ if (action.participant.screen) {
+ const screenItem = getScreenItem(action.participant);
+ if (screenIdx >= 0) {
+ screens[screenIdx] = screenItem;
+ } else {
+ screens.push(screenItem);
+ }
+ } else if (screenIdx >= 0) {
+ screens.splice(screenIdx, 1);
+ }
+
+ const newState = {
+ ...prevState,
+ participants,
+ screens,
+ };
+
+ if (fasteq(newState, prevState)) {
+ return prevState;
+ }
+
+ return newState;
+ }
+ case 'PARTICIPANT_LEFT': {
+ const id = getId(action.participant);
+ const screenId = getScreenId(id);
+
+ return {
+ ...prevState,
+ participants: [...prevState.participants].filter((p) => p.id !== id),
+ screens: [...prevState.screens].filter((s) => s.id !== screenId),
+ };
+ }
+ case 'SWAP_POSITION': {
+ const participants = [...prevState.participants];
+ if (!action.id1 || !action.id2) return prevState;
+ const idx1 = participants.findIndex((p) => p.id === action.id1);
+ const idx2 = participants.findIndex((p) => p.id === action.id2);
+ if (idx1 === -1 || idx2 === -1) return prevState;
+ const tmp = participants[idx1];
+ participants[idx1] = participants[idx2];
+ participants[idx2] = tmp;
+ return {
+ ...prevState,
+ participants,
+ };
+ }
+ default:
+ throw new Error();
+ }
}
-function getScreenId(id) {
- return `${id}-screen`;
-}
-
-function isLocalId(id) {
- return typeof id === 'string' && id === 'local';
-}
-
-function isScreenId(id) {
- return typeof id === 'string' && id.endsWith('-screen');
-}
-
-// ---Helpers ---
-
function getNewParticipant(participant) {
const id = getId(participant);
@@ -128,159 +257,29 @@ function getScreenItem(participant) {
};
}
-// --- Actions ---
+// --- Derived data ---
-const ACTIVE_SPEAKER = 'ACTIVE_SPEAKER';
-const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
-const PARTICIPANT_UPDATED = 'PARTICIPANT_UPDATED';
-const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
-const SWAP_POSITION = 'SWAP_POSITION';
+function getId(participant) {
+ return participant.local ? 'local' : participant.session_id;
+}
-// --- Reducer --
+function getScreenId(id) {
+ return `${id}-screen`;
+}
-function participantsReducer(prevState, action) {
- switch (action.type) {
- case ACTIVE_SPEAKER: {
- const { participants, ...state } = prevState;
- if (!action.id)
- return {
- ...prevState,
- lastPendingUnknownActiveSpeaker: null,
- };
- const date = new Date();
- const isParticipantKnown = participants.some((p) => p.id === action.id);
- return {
- ...state,
- lastPendingUnknownActiveSpeaker: isParticipantKnown
- ? null
- : {
- date,
- id: action.id,
- },
- participants: participants.map((p) => ({
- ...p,
- isActiveSpeaker: p.id === action.id,
- lastActiveDate: p.id === action.id ? date : p?.lastActiveDate,
- })),
- };
- }
- case PARTICIPANT_JOINED: {
- const item = getNewParticipant(action.participant);
+function isLocalId(id) {
+ return typeof id === 'string' && id === 'local';
+}
- const participants = [...prevState.participants];
- const screens = [...prevState.screens];
-
- const isPendingActiveSpeaker =
- item.id === prevState.lastPendingUnknownActiveSpeaker?.id;
- if (isPendingActiveSpeaker) {
- item.isActiveSpeaker = true;
- item.lastActiveDate = prevState.lastPendingUnknownActiveSpeaker?.date;
- }
-
- if (item.isCamMuted) {
- participants.push(item);
- } else {
- const firstInactiveCamOffIndex = prevState.participants.findIndex(
- (p) => p.isCamMuted && !p.isLocal && !p.isActiveSpeaker
- );
- if (firstInactiveCamOffIndex >= 0) {
- participants.splice(firstInactiveCamOffIndex, 0, item);
- } else {
- participants.push(item);
- }
- }
-
- // Participant is sharing screen
- if (action.participant.screen) {
- screens.push(getScreenItem(action.participant));
- }
-
- return {
- ...prevState,
- lastPendingUnknownActiveSpeaker: isPendingActiveSpeaker
- ? null
- : prevState.lastPendingUnknownActiveSpeaker,
- participants,
- screens,
- };
- }
- case PARTICIPANT_UPDATED: {
- const item = getUpdatedParticipant(
- action.participant,
- prevState.participants
- );
- const { id } = item;
- const screenId = getScreenId(id);
-
- const participants = [...prevState.participants];
- const idx = participants.findIndex((p) => p.id === id);
- participants[idx] = item;
-
- const screens = [...prevState.screens];
- const screenIdx = screens.findIndex((s) => s.id === screenId);
-
- if (action.participant.screen) {
- const screenItem = getScreenItem(action.participant);
- if (screenIdx >= 0) {
- screens[screenIdx] = screenItem;
- } else {
- screens.push(screenItem);
- }
- } else if (screenIdx >= 0) {
- screens.splice(screenIdx, 1);
- }
-
- const newState = {
- ...prevState,
- participants,
- screens,
- };
-
- if (fasteq(newState, prevState)) {
- return prevState;
- }
-
- return newState;
- }
- case PARTICIPANT_LEFT: {
- const id = getId(action.participant);
- const screenId = getScreenId(id);
-
- return {
- ...prevState,
- participants: [...prevState.participants].filter((p) => p.id !== id),
- screens: [...prevState.screens].filter((s) => s.id !== screenId),
- };
- }
- case SWAP_POSITION: {
- const participants = [...prevState.participants];
- if (!action.id1 || !action.id2) return prevState;
- const idx1 = participants.findIndex((p) => p.id === action.id1);
- const idx2 = participants.findIndex((p) => p.id === action.id2);
- if (idx1 === -1 || idx2 === -1) return prevState;
- const tmp = participants[idx1];
- participants[idx1] = participants[idx2];
- participants[idx2] = tmp;
- return {
- ...prevState,
- participants,
- };
- }
- default:
- throw new Error();
- }
+function isScreenId(id) {
+ return typeof id === 'string' && id.endsWith('-screen');
}
export {
- ACTIVE_SPEAKER,
getId,
getScreenId,
+ initialParticipantsState,
isLocalId,
isScreenId,
participantsReducer,
- initialParticipantsState,
- PARTICIPANT_JOINED,
- PARTICIPANT_LEFT,
- PARTICIPANT_UPDATED,
- SWAP_POSITION,
-};
+};
\ No newline at end of file
diff --git a/custom/shared/contexts/tracksState.js b/custom/shared/contexts/tracksState.js
index 60b4383..a07ae64 100644
--- a/custom/shared/contexts/tracksState.js
+++ b/custom/shared/contexts/tracksState.js
@@ -1,31 +1,18 @@
-/**
- * Track state & reducer
- * ---
- * All (participant & screen) video and audio tracks indexed on participant ID
- * If using manual track subscriptions, we'll also keep a record of those
- * and their playing / paused state
- */
-
import { getId, getScreenId } from './participantsState';
-export const initialTracksState = {
+const initialTracksState = {
audioTracks: {},
videoTracks: {},
};
-// --- Actions ---
-
-export const TRACK_STARTED = 'TRACK_STARTED';
-export const TRACK_STOPPED = 'TRACK_STOPPED';
-export const TRACK_VIDEO_UPDATED = 'TRACK_VIDEO_UPDATED';
-export const TRACK_AUDIO_UPDATED = 'TRACK_AUDIO_UPDATED';
-export const REMOVE_TRACKS = 'REMOVE_TRACKS';
-
// --- Reducer and helpers --
-export function tracksReducer(prevState, action) {
+function tracksReducer(
+ prevState,
+ action
+) {
switch (action.type) {
- case TRACK_STARTED: {
+ case 'TRACK_STARTED': {
const id = getId(action.participant);
const screenId = getScreenId(id);
@@ -63,17 +50,15 @@ export function tracksReducer(prevState, action) {
},
};
}
-
- case TRACK_STOPPED: {
+ case 'TRACKS_STOPPED': {
const { audioTracks, videoTracks } = prevState;
const newAudioTracks = { ...audioTracks };
const newVideoTracks = { ...videoTracks };
- action.items.forEach(([participant, track]) => {
+ for (const [participant, track] of action.items) {
const id = participant ? getId(participant) : null;
const screenId = participant ? getScreenId(id) : null;
-
if (track.kind === 'audio') {
if (!participant?.local) {
// Ignore local audio from mic and screen share
@@ -88,16 +73,16 @@ export function tracksReducer(prevState, action) {
newVideoTracks[screenId] = participant.tracks.screenVideo;
}
}
- });
+ }
return {
audioTracks: newAudioTracks,
videoTracks: newVideoTracks,
};
}
-
- case TRACK_AUDIO_UPDATED: {
+ case 'UPDATE_AUDIO_TRACK': {
const id = getId(action.participant);
+ const screenId = getScreenId(id);
if (action.participant?.local) {
// Ignore local audio from mic and screen share
return prevState;
@@ -105,14 +90,14 @@ export function tracksReducer(prevState, action) {
const newAudioTracks = {
...prevState.audioTracks,
[id]: action.participant.tracks.audio,
+ [screenId]: action.participant.tracks.screenAudio,
};
return {
...prevState,
audioTracks: newAudioTracks,
};
}
-
- case TRACK_VIDEO_UPDATED: {
+ case 'UPDATE_VIDEO_TRACK': {
const id = getId(action.participant);
const newVideoTracks = {
...prevState.videoTracks,
@@ -123,8 +108,7 @@ export function tracksReducer(prevState, action) {
videoTracks: newVideoTracks,
};
}
-
- case REMOVE_TRACKS: {
+ case 'REMOVE_TRACKS': {
const { audioTracks, videoTracks } = prevState;
const id = getId(action.participant);
const screenId = getScreenId(id);
@@ -139,8 +123,9 @@ export function tracksReducer(prevState, action) {
videoTracks,
};
}
-
default:
throw new Error();
}
}
+
+export { initialTracksState, tracksReducer };
\ No newline at end of file
diff --git a/custom/shared/hooks/useNetworkState.js b/custom/shared/hooks/useNetworkState.js
index 16a739f..e8883d3 100644
--- a/custom/shared/hooks/useNetworkState.js
+++ b/custom/shared/hooks/useNetworkState.js
@@ -1,50 +1,55 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallState } from '../contexts/CallProvider';
-import {
- VIDEO_QUALITY_HIGH,
- VIDEO_QUALITY_LOW,
- VIDEO_QUALITY_BANDWIDTH_SAVER,
-} from '../constants';
-
-export const NETWORK_STATE_GOOD = 'good';
-export const NETWORK_STATE_LOW = 'low';
-export const NETWORK_STATE_VERY_LOW = 'very-low';
const STANDARD_HIGH_BITRATE_CAP = 980;
const STANDARD_LOW_BITRATE_CAP = 300;
export const useNetworkState = (
- callObject = null,
- quality = VIDEO_QUALITY_HIGH
+ co = null,
+ quality = 'high'
) => {
- const [threshold, setThreshold] = useState(NETWORK_STATE_GOOD);
+ const [threshold, setThreshold] = useState('good');
+ const lastSetKBS = useRef(null);
+
+ const callState = useCallState();
+
+ const callObject = co ?? callState?.callObject;
const setQuality = useCallback(
- (q) => {
+ async (q) => {
if (!callObject) return;
-
const peers = Object.keys(callObject.participants()).length - 1;
- const isSFU = callObject.getNetworkTopology().topology === 'sfu';
-
+ const isSFU = (await callObject.getNetworkTopology()).topology === 'sfu';
const lowKbs = isSFU
? STANDARD_LOW_BITRATE_CAP
- : STANDARD_LOW_BITRATE_CAP / Math.max(1, peers);
+ : Math.floor(STANDARD_LOW_BITRATE_CAP / Math.max(1, peers));
+ const highKbs = isSFU
+ ? STANDARD_HIGH_BITRATE_CAP
+ : Math.floor(STANDARD_HIGH_BITRATE_CAP / Math.max(1, peers));
switch (q) {
- case VIDEO_QUALITY_HIGH:
- callObject.setBandwidth({ kbs: STANDARD_HIGH_BITRATE_CAP });
+ case 'auto':
+ case 'high':
+ if (lastSetKBS.current === highKbs) break;
+ callObject.setBandwidth({
+ kbs: highKbs,
+ });
+ lastSetKBS.current = highKbs;
break;
- case VIDEO_QUALITY_LOW:
+ case 'low':
+ if (lastSetKBS.current === lowKbs) break;
callObject.setBandwidth({
kbs: lowKbs,
});
+ lastSetKBS.current = lowKbs;
break;
- case VIDEO_QUALITY_BANDWIDTH_SAVER:
+ case 'bandwidth-saver':
callObject.setLocalVideo(false);
+ if (lastSetKBS.current === lowKbs) break;
callObject.setBandwidth({
kbs: lowKbs,
});
- break;
- default:
+ lastSetKBS.current = lowKbs;
break;
}
},
@@ -56,43 +61,50 @@ export const useNetworkState = (
if (ev.threshold === threshold) return;
switch (ev.threshold) {
- case NETWORK_STATE_VERY_LOW:
- setQuality(VIDEO_QUALITY_BANDWIDTH_SAVER);
- setThreshold(NETWORK_STATE_VERY_LOW);
+ case 'very-low':
+ setQuality('bandwidth-saver');
+ setThreshold('very-low');
break;
- case NETWORK_STATE_LOW:
+ case 'low':
+ setQuality(quality === 'bandwidth-saver' ? quality : 'low');
+ setThreshold('low');
+ break;
+ case 'good':
setQuality(
- quality === VIDEO_QUALITY_BANDWIDTH_SAVER
- ? quality
- : NETWORK_STATE_LOW
+ ['bandwidth-saver', 'low'].includes(quality) ? quality : 'high'
);
- setThreshold(NETWORK_STATE_LOW);
- break;
- case NETWORK_STATE_GOOD:
- setQuality(
- [VIDEO_QUALITY_BANDWIDTH_SAVER, VIDEO_QUALITY_LOW].includes(quality)
- ? quality
- : VIDEO_QUALITY_HIGH
- );
- setThreshold(NETWORK_STATE_GOOD);
- break;
- default:
+ setThreshold('good');
break;
}
},
- [setQuality, threshold, quality]
+ [quality, setQuality, threshold]
);
useEffect(() => {
- if (!callObject) return false;
+ if (!callObject) return;
callObject.on('network-quality-change', handleNetworkQualityChange);
- return () =>
+ return () => {
callObject.off('network-quality-change', handleNetworkQualityChange);
+ };
}, [callObject, handleNetworkQualityChange]);
useEffect(() => {
+ if (!callObject) return;
setQuality(quality);
- }, [quality, setQuality]);
+ let timeout;
+ const handleParticipantCountChange = () => {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ setQuality(quality);
+ }, 500);
+ };
+ callObject.on('participant-joined', handleParticipantCountChange);
+ callObject.on('participant-left', handleParticipantCountChange);
+ return () => {
+ callObject.off('participant-joined', handleParticipantCountChange);
+ callObject.off('participant-left', handleParticipantCountChange);
+ };
+ }, [callObject, quality, setQuality]);
return threshold;
-};
+};
\ No newline at end of file
diff --git a/custom/shared/lib/sortByKey.js b/custom/shared/lib/sortByKey.js
index de09b11..6525050 100644
--- a/custom/shared/lib/sortByKey.js
+++ b/custom/shared/lib/sortByKey.js
@@ -1,4 +1,5 @@
-export const sortByKey = (a, b, key, caseSensitive = true) => {
+export const sortByKey = (key, caseSensitive = true) =>
+(a, b) => {
const aKey =
!caseSensitive && typeof a[key] === 'string'
? String(a[key])?.toLowerCase()