Update the track provider and participant providers
This commit is contained in:
parent
0927582f5c
commit
5a1fdc59b7
|
|
@ -4,7 +4,7 @@ import { ReactComponent as IconMicMute } from '@custom/shared/icons/mic-off-sm.s
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { DEFAULT_ASPECT_RATIO } from '../../constants';
|
import { DEFAULT_ASPECT_RATIO } from '../../constants';
|
||||||
import Video from './Video';
|
import { Video } from './Video';
|
||||||
import { ReactComponent as Avatar } from './avatar.svg';
|
import { ReactComponent as Avatar } from './avatar.svg';
|
||||||
|
|
||||||
const SM_TILE_MAX_WIDTH = 300;
|
const SM_TILE_MAX_WIDTH = 300;
|
||||||
|
|
@ -99,8 +99,9 @@ export const Tile = memo(
|
||||||
{videoTrack ? (
|
{videoTrack ? (
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
|
fit={videoFit}
|
||||||
|
isScreen={participant.isScreenshare}
|
||||||
participantId={participant?.id}
|
participantId={participant?.id}
|
||||||
videoTrack={videoTrack}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
showAvatar && (
|
showAvatar && (
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,30 @@
|
||||||
import React, { useMemo, forwardRef, memo, useEffect } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import Bowser from 'bowser';
|
import Bowser from 'bowser';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import { shallowEqualObjects } from 'shallow-equal';
|
|
||||||
|
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 isChrome92 = useMemo(() => {
|
||||||
const { browser, platform, os } = Bowser.parse(navigator.userAgent);
|
const { browser, platform, os } = Bowser.parse(navigator.userAgent);
|
||||||
return (
|
return (
|
||||||
|
|
@ -19,43 +35,114 @@ export const Video = memo(
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect: Umount
|
* Determine if video needs to be mirrored.
|
||||||
* 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 false;
|
if (!video) return;
|
||||||
// clean up when video renders for different participant
|
const handleCanPlay = () => {
|
||||||
video.srcObject = null;
|
if (!video.paused) return;
|
||||||
if (isChrome92) video.load();
|
video.play();
|
||||||
return () => {
|
|
||||||
// clean up when unmounted
|
|
||||||
video.srcObject = null;
|
|
||||||
if (isChrome92) video.load();
|
|
||||||
};
|
};
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
const video = videoEl.current;
|
const video = videoEl.current;
|
||||||
if (!video || !videoTrack) return;
|
if (!video || !videoTrack) return;
|
||||||
video.srcObject = new MediaStream([videoTrack]);
|
video.srcObject = new MediaStream([videoTrack]);
|
||||||
if (isChrome92) video.load();
|
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 <video autoPlay muted playsInline ref={videoEl} {...rest} />;
|
return (
|
||||||
}),
|
<>
|
||||||
(p, n) => shallowEqualObjects(p, n)
|
<video
|
||||||
|
className={classNames(fit, {
|
||||||
|
isMirrored,
|
||||||
|
isMobile,
|
||||||
|
playable: 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
Video.displayName = 'Video';
|
||||||
Video.displayName = 'Video';
|
|
||||||
|
|
||||||
Video.propTypes = {
|
|
||||||
videoTrack: PropTypes.any,
|
|
||||||
mirrored: PropTypes.bool,
|
|
||||||
participantId: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Video;
|
|
||||||
|
|
@ -1,44 +1,36 @@
|
||||||
import React, {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useReducer,
|
useReducer,
|
||||||
useState,
|
useState,
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { sortByKey } from '@custom/shared/lib/sortByKey';
|
||||||
useUIState,
|
import { useNetworkState } from '../hooks/useNetworkState';
|
||||||
} from '@custom/shared/contexts/UIStateProvider';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { sortByKey } from '../lib/sortByKey';
|
|
||||||
|
|
||||||
import { useCallState } from './CallProvider';
|
import { useCallState } from './CallProvider';
|
||||||
|
import { useUIState } from './UIStateProvider';
|
||||||
import {
|
import {
|
||||||
initialParticipantsState,
|
initialParticipantsState,
|
||||||
isLocalId,
|
isLocalId,
|
||||||
ACTIVE_SPEAKER,
|
|
||||||
PARTICIPANT_JOINED,
|
|
||||||
PARTICIPANT_LEFT,
|
|
||||||
PARTICIPANT_UPDATED,
|
|
||||||
participantsReducer,
|
participantsReducer,
|
||||||
SWAP_POSITION,
|
|
||||||
} from './participantsState';
|
} from './participantsState';
|
||||||
|
|
||||||
export const ParticipantsContext = createContext();
|
export const ParticipantsContext = createContext(null);
|
||||||
|
|
||||||
export const ParticipantsProvider = ({ children }) => {
|
export const ParticipantsProvider = ({ children }) => {
|
||||||
const { callObject, videoQuality, networkState, broadcast } = useCallState();
|
const { isMobile, pinnedId, viewMode } = useUIState();
|
||||||
const [state, dispatch] = useReducer(
|
const {
|
||||||
participantsReducer,
|
broadcast,
|
||||||
initialParticipantsState
|
broadcastRole,
|
||||||
);
|
callObject: daily,
|
||||||
const { viewMode } = useUIState();
|
videoQuality,
|
||||||
const [
|
} = useCallState();
|
||||||
participantMarkedForRemoval,
|
const [state, dispatch] = useReducer(participantsReducer, initialParticipantsState);
|
||||||
setParticipantMarkedForRemoval,
|
const [participantMarkedForRemoval, setParticipantMarkedForRemoval] = useState(null);
|
||||||
] = useState(null);
|
|
||||||
|
const { threshold } = useNetworkState();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ALL participants (incl. shared screens) in a convenient array
|
* ALL participants (incl. shared screens) in a convenient array
|
||||||
|
|
@ -58,14 +50,6 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
return state.participants;
|
return state.participants;
|
||||||
}, [broadcast, 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
|
* The number of participants, who are not a shared screen
|
||||||
* (technically a shared screen counts as a participant, but we shouldn't tell humans)
|
* (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(() => {
|
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
|
* The activeParticipant only updates to a new active participant so
|
||||||
* 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);
|
||||||
if (isPresent) {
|
const pinned = participants.find((p) => p?.id === pinnedId);
|
||||||
return activeParticipant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (pinned) return pinned;
|
||||||
* If the activeParticipant has left, calculate the remaining displayable participants
|
|
||||||
*/
|
const displayableParticipants = participants.filter((p) =>
|
||||||
const displayableParticipants = participants.filter((p) => !p?.isLocal);
|
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 (
|
if (
|
||||||
|
!isPresent &&
|
||||||
displayableParticipants.length > 0 &&
|
displayableParticipants.length > 0 &&
|
||||||
displayableParticipants.every((p) => p.isMicMuted && !p.lastActiveDate)
|
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 (
|
return (
|
||||||
displayableParticipants.find((p) => !p.isCamMuted) ??
|
displayableParticipants.find((p) => !p.isCamMuted) ??
|
||||||
displayableParticipants?.[0]
|
displayableParticipants?.[0]
|
||||||
|
|
@ -132,13 +114,20 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = displayableParticipants
|
const sorted = displayableParticipants
|
||||||
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
|
.sort(sortByKey('lastActiveDate'))
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
const lastActiveSpeaker = sorted?.[0];
|
const fallback = broadcastRole === 'attendee' ? null : localParticipant;
|
||||||
|
|
||||||
return lastActiveSpeaker || localParticipant;
|
return isPresent ? activeParticipant : sorted?.[0] ?? fallback;
|
||||||
}, [activeParticipant, localParticipant, participants]);
|
}, [
|
||||||
|
activeParticipant,
|
||||||
|
broadcastRole,
|
||||||
|
isMobile,
|
||||||
|
localParticipant,
|
||||||
|
participants,
|
||||||
|
pinnedId,
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen shares
|
* Screen shares
|
||||||
|
|
@ -148,33 +137,17 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
/**
|
/**
|
||||||
* The local participant's name
|
* 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
|
* Sets the local participant's name in daily-js
|
||||||
* @param name The new username
|
* @param name The new username
|
||||||
*/
|
*/
|
||||||
const setUsername = (name) => {
|
const setUsername = useCallback(
|
||||||
callObject.setUserName(name);
|
(name) => {
|
||||||
};
|
daily.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);
|
|
||||||
},
|
},
|
||||||
[callObject, localParticipant, participants]
|
[daily]
|
||||||
);
|
);
|
||||||
|
|
||||||
const swapParticipantPosition = useCallback((id1, id2) => {
|
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(
|
const handleNewParticipantsState = useCallback(
|
||||||
(event = null) => {
|
(event = null) => {
|
||||||
switch (event?.action) {
|
switch (event?.action) {
|
||||||
case 'participant-joined':
|
case 'participant-joined':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PARTICIPANT_JOINED,
|
type: 'PARTICIPANT_JOINED',
|
||||||
participant: event.participant,
|
participant: event.participant,
|
||||||
});
|
});
|
||||||
|
if (muteNewParticipants && daily) {
|
||||||
|
daily.updateParticipant(event.participant.session_id, {
|
||||||
|
setAudio: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'participant-updated':
|
case 'participant-updated':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PARTICIPANT_UPDATED,
|
type: 'PARTICIPANT_UPDATED',
|
||||||
participant: event.participant,
|
participant: event.participant,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'participant-left':
|
case 'participant-left':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PARTICIPANT_LEFT,
|
type: 'PARTICIPANT_LEFT',
|
||||||
participant: event.participant,
|
participant: event.participant,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[daily, dispatch, muteNewParticipants]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Start listening for participant changes, when the callObject is set.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
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 = [
|
return () => {
|
||||||
'joined-meeting',
|
daily.off('participant-joined', handleParticipantJoined);
|
||||||
'participant-joined',
|
daily.off('participant-joined', handleNewParticipantsState);
|
||||||
'participant-updated',
|
daily.off('participant-updated', handleNewParticipantsState);
|
||||||
'participant-left',
|
daily.off('participant-left', handleNewParticipantsState);
|
||||||
];
|
};
|
||||||
|
}, [daily, handleNewParticipantsState, handleParticipantJoined]);
|
||||||
|
|
||||||
// Use initial state
|
const participantIds = useMemo(
|
||||||
handleNewParticipantsState();
|
() => participants.map((p) => p.id).join(','),
|
||||||
|
[participants]
|
||||||
// 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 setBandWidthControls = useCallback(() => {
|
const setBandWidthControls = useCallback(() => {
|
||||||
if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
|
if (!(daily && daily.meetingState() === 'joined-meeting')) return;
|
||||||
|
|
||||||
const ids = participantIds.split(',').filter(Boolean);
|
const ids = participantIds.split(',').filter(Boolean);
|
||||||
const receiveSettings = {};
|
const receiveSettings = {};
|
||||||
|
|
@ -263,7 +257,7 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// weak or bad network
|
// 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
|
// Low quality or Bandwidth saver mode enabled
|
||||||
['bandwidth-saver', 'low'].includes(videoQuality)
|
['bandwidth-saver', 'low'].includes(videoQuality)
|
||||||
) {
|
) {
|
||||||
|
|
@ -284,38 +278,44 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
// Mobile view settings are handled separately in MobileCall
|
// Mobile view settings are handled separately in MobileCall
|
||||||
});
|
});
|
||||||
|
|
||||||
callObject.updateReceiveSettings(receiveSettings);
|
daily.updateReceiveSettings(receiveSettings);
|
||||||
}, [callObject, participantIds, networkState, videoQuality, viewMode, currentSpeaker?.id]);
|
}, [
|
||||||
|
currentSpeaker?.id,
|
||||||
|
daily,
|
||||||
|
participantIds,
|
||||||
|
threshold,
|
||||||
|
videoQuality,
|
||||||
|
viewMode,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBandWidthControls();
|
setBandWidthControls();
|
||||||
}, [setBandWidthControls]);
|
}, [setBandWidthControls]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!callObject) return false;
|
if (!daily) return;
|
||||||
const handleActiveSpeakerChange = ({ activeSpeaker }) => {
|
const handleActiveSpeakerChange = ({ activeSpeaker }) => {
|
||||||
/**
|
/**
|
||||||
* Ignore active-speaker-change events for the local user.
|
* Ignore active-speaker-change events for the local user.
|
||||||
* Our UX doesn't ever highlight the local user as the active speaker.
|
* 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;
|
const activeSpeakerId = activeSpeaker?.peerId;
|
||||||
if (localId === activeSpeakerId) return;
|
if (localId === activeSpeakerId) return;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIVE_SPEAKER,
|
type: 'ACTIVE_SPEAKER',
|
||||||
id: activeSpeakerId,
|
id: activeSpeakerId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
callObject.on('active-speaker-change', handleActiveSpeakerChange);
|
daily.on('active-speaker-change', handleActiveSpeakerChange);
|
||||||
return () =>
|
return () =>
|
||||||
callObject.off('active-speaker-change', handleActiveSpeakerChange);
|
daily.off('active-speaker-change', handleActiveSpeakerChange);
|
||||||
}, [callObject]);
|
}, [daily]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantsContext.Provider
|
<ParticipantsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
activeParticipant,
|
|
||||||
allParticipants,
|
allParticipants,
|
||||||
currentSpeaker,
|
currentSpeaker,
|
||||||
localParticipant,
|
localParticipant,
|
||||||
|
|
@ -337,8 +337,4 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ParticipantsProvider.propTypes = {
|
export const useParticipants = () => useContext(ParticipantsContext);
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useParticipants = () => useContext(ParticipantsContext);
|
|
||||||
|
|
@ -1,38 +1,29 @@
|
||||||
/* global rtcpeers */
|
import {
|
||||||
|
|
||||||
import React, {
|
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useReducer,
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { sortByKey } from '@custom/shared/lib/sortByKey';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import PropTypes from 'prop-types';
|
import { useDeepCompareCallback } from 'use-deep-compare';
|
||||||
import { useDeepCompareEffect } from 'use-deep-compare';
|
|
||||||
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 { useUIState } from './UIStateProvider';
|
||||||
import { isLocalId, isScreenId } from './participantsState';
|
import { getScreenId, isLocalId, isScreenId } from './participantsState';
|
||||||
import {
|
import { initialTracksState, tracksReducer } from './tracksState';
|
||||||
initialTracksState,
|
|
||||||
REMOVE_TRACKS,
|
|
||||||
TRACK_STARTED,
|
|
||||||
TRACK_STOPPED,
|
|
||||||
TRACK_VIDEO_UPDATED,
|
|
||||||
TRACK_AUDIO_UPDATED,
|
|
||||||
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,
|
* If the remote participant count passes this threshold,
|
||||||
* cam subscriptions are defined by UI view modes.
|
* cam subscriptions are defined by UI view modes.
|
||||||
*/
|
*/
|
||||||
|
|
@ -41,16 +32,17 @@ const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
|
||||||
const TracksContext = createContext(null);
|
const TracksContext = createContext(null);
|
||||||
|
|
||||||
export const TracksProvider = ({ children }) => {
|
export const TracksProvider = ({ children }) => {
|
||||||
const { callObject, subscribeToTracksAutomatically } = useCallState();
|
const { callObject: daily, optimizeLargeCalls } = useCallState();
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
const { viewMode } = useUIState();
|
const { viewMode } = useUIState();
|
||||||
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
|
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
|
||||||
|
const [maxCamSubscriptions, setMaxCamSubscriptions] = useState(null);
|
||||||
|
|
||||||
const recentSpeakerIds = useMemo(
|
const recentSpeakerIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
participants
|
participants
|
||||||
.filter((p) => Boolean(p.lastActiveDate) && !p.isLocal)
|
.filter((p) => Boolean(p.lastActiveDate))
|
||||||
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
|
.sort(sortByKey('lastActiveDate'))
|
||||||
.slice(-MAX_RECENT_SPEAKER_COUNT)
|
.slice(-MAX_RECENT_SPEAKER_COUNT)
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
.reverse(),
|
.reverse(),
|
||||||
|
|
@ -64,22 +56,27 @@ export const TracksProvider = ({ children }) => {
|
||||||
|
|
||||||
const subscribeToCam = useCallback(
|
const subscribeToCam = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
// Ignore undefined, local or screenshare.
|
/**
|
||||||
|
* Ignore undefined, local or screenshare.
|
||||||
|
*/
|
||||||
if (!id || isLocalId(id) || isScreenId(id)) return;
|
if (!id || isLocalId(id) || isScreenId(id)) return;
|
||||||
callObject.updateParticipant(id, {
|
daily.updateParticipant(id, {
|
||||||
setSubscribedTracks: { video: true },
|
setSubscribedTracks: { video: true },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[callObject]
|
[daily]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
|
* 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(
|
const updateCamSubscriptions = useCallback(
|
||||||
(subscribedIds, stagedIds = []) => {
|
(subscribedIds, stagedIds = []) => {
|
||||||
if (!callObject) return;
|
if (!daily) return;
|
||||||
|
|
||||||
// If total number of remote participants is less than a threshold, simply
|
// If total number of remote participants is less than a threshold, simply
|
||||||
// stage all remote cams that aren't already marked for subscription.
|
// 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) => {
|
const updates = remoteParticipantIds.reduce((u, id) => {
|
||||||
let desiredSubscription;
|
let desiredSubscription;
|
||||||
const currentSubscription =
|
const currentSubscription =
|
||||||
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
daily.participants()?.[id]?.tracks?.video?.subscribed;
|
||||||
|
|
||||||
// Ignore undefined, local or screenshare participant ids
|
// Ignore undefined, local or screenshare participant ids
|
||||||
if (!id || isLocalId(id) || isScreenId(id)) return u;
|
if (!id || isLocalId(id) || isScreenId(id)) return u;
|
||||||
|
|
@ -131,110 +128,126 @@ export const TracksProvider = ({ children }) => {
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
if (Object.keys(updates).length === 0) return;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!callObject) return false;
|
if (!daily) return;
|
||||||
|
/**
|
||||||
const trackStoppedQueue = [];
|
* A little throttling as we want daily-js to have some room to breathe ☺️
|
||||||
|
*/
|
||||||
const handleTrackStarted = ({ participant, track }) => {
|
const timeout = setTimeout(() => {
|
||||||
/**
|
const participants = daily.participants();
|
||||||
* If track for participant was recently stopped, remove it from queue,
|
const updates = remoteParticipantIds.reduce((u, id) => {
|
||||||
* so we don't run into a stale state
|
// Ignore undefined, local or screenshare participant ids
|
||||||
*/
|
if (!id || isLocalId(id) || isScreenId(id)) return u;
|
||||||
const stoppingIdx = trackStoppedQueue.findIndex(
|
const isSpeaker = recentSpeakerIds.includes(id);
|
||||||
([p, t]) =>
|
const hasSubscribed = participants[id]?.tracks?.audio?.subscribed;
|
||||||
p.session_id === participant.session_id && t.kind === track.kind
|
const shouldSubscribe = optimizeLargeCalls ? isSpeaker : true;
|
||||||
);
|
/**
|
||||||
if (stoppingIdx >= 0) {
|
* In optimized calls:
|
||||||
trackStoppedQueue.splice(stoppingIdx, 1);
|
* - subscribe to speakers we're not subscribed to, yet
|
||||||
}
|
* - unsubscribe from non-speakers we're subscribed to
|
||||||
dispatch({
|
* In non-optimized calls:
|
||||||
type: TRACK_STARTED,
|
* - subscribe to all who we're not to subscribed to, yet
|
||||||
participant,
|
*/
|
||||||
track,
|
if (
|
||||||
});
|
(!hasSubscribed && shouldSubscribe) ||
|
||||||
};
|
(hasSubscribed && !shouldSubscribe)
|
||||||
|
) {
|
||||||
const trackStoppedBatchInterval = setInterval(() => {
|
u[id] = {
|
||||||
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] = {
|
|
||||||
setSubscribedTracks: {
|
setSubscribedTracks: {
|
||||||
audio: true,
|
audio: shouldSubscribe,
|
||||||
screenAudio: true,
|
|
||||||
screenVideo: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (topology === 'peer') {
|
return u;
|
||||||
o[id] = { setSubscribedTracks: true };
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}, {});
|
}, {});
|
||||||
|
if (Object.keys(updates).length === 0) return;
|
||||||
if (!subscribeToTracksAutomatically && Object.keys(updates).length0) {
|
daily.updateParticipants(updates);
|
||||||
callObject.updateParticipants(updates);
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
callObject.on('track-started', handleTrackStarted);
|
|
||||||
callObject.on('track-stopped', handleTrackStopped);
|
|
||||||
callObject.on('participant-joined', handleParticipantJoined);
|
|
||||||
callObject.on('participant-left', handleParticipantLeft);
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(joinBatchInterval);
|
clearTimeout(timeout);
|
||||||
clearInterval(trackStoppedBatchInterval);
|
|
||||||
callObject.off('track-started', handleTrackStarted);
|
|
||||||
callObject.off('track-stopped', handleTrackStopped);
|
|
||||||
callObject.off('participant-joined', handleParticipantJoined);
|
|
||||||
callObject.off('participant-left', handleParticipantLeft);
|
|
||||||
};
|
};
|
||||||
}, [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 =
|
const hasAudioChanged =
|
||||||
// State changed
|
// State changed
|
||||||
participant.tracks.audio?.state !==
|
participant.tracks.audio.state !==
|
||||||
state.audioTracks?.[participant.user_id]?.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
|
// Off/blocked reason changed
|
||||||
!deepEqual(
|
!deepEqual(
|
||||||
{
|
{
|
||||||
|
|
@ -242,14 +255,14 @@ export const TracksProvider = ({ children }) => {
|
||||||
...(participant.tracks.audio?.off ?? {}),
|
...(participant.tracks.audio?.off ?? {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...(state.audioTracks?.[participant.user_id]?.blocked ?? {}),
|
...(state.audioTracks?.[participant.user_id].blocked ?? {}),
|
||||||
...(state.audioTracks?.[participant.user_id]?.off ?? {}),
|
...(state.audioTracks?.[participant.user_id].off ?? {}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const hasVideoChanged =
|
const hasVideoChanged =
|
||||||
// State changed
|
// State changed
|
||||||
participant.tracks.video?.state !==
|
participant.tracks.video?.state !==
|
||||||
state.videoTracks?.[participant.user_id]?.state ||
|
state.videoTracks?.[participant.user_id]?.state ||
|
||||||
// Off/blocked reason changed
|
// Off/blocked reason changed
|
||||||
!deepEqual(
|
!deepEqual(
|
||||||
{
|
{
|
||||||
|
|
@ -265,7 +278,7 @@ export const TracksProvider = ({ children }) => {
|
||||||
if (hasAudioChanged) {
|
if (hasAudioChanged) {
|
||||||
// Update audio track state
|
// Update audio track state
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TRACK_AUDIO_UPDATED,
|
type: 'UPDATE_AUDIO_TRACK',
|
||||||
participant,
|
participant,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -273,27 +286,92 @@ export const TracksProvider = ({ children }) => {
|
||||||
if (hasVideoChanged) {
|
if (hasVideoChanged) {
|
||||||
// Update video track state
|
// Update video track state
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TRACK_VIDEO_UPDATED,
|
type: 'UPDATE_VIDEO_TRACK',
|
||||||
participant,
|
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 () => {
|
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 (
|
return (
|
||||||
<TracksContext.Provider
|
<TracksContext.Provider
|
||||||
value={{
|
value={{
|
||||||
audioTracks: state.audioTracks,
|
audioTracks: state.audioTracks,
|
||||||
videoTracks: state.videoTracks,
|
videoTracks: state.videoTracks,
|
||||||
|
maxCamSubscriptions,
|
||||||
subscribeToCam,
|
subscribeToCam,
|
||||||
updateCamSubscriptions,
|
updateCamSubscriptions,
|
||||||
remoteParticipantIds,
|
|
||||||
recentSpeakerIds,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -301,8 +379,4 @@ export const TracksProvider = ({ children }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
TracksProvider.propTypes = {
|
export const useTracks = () => useContext(TracksContext);
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTracks = () => useContext(TracksContext);
|
|
||||||
|
|
@ -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
|
|
||||||
* - "<id>-screen" for each shared screen
|
|
||||||
*/
|
|
||||||
import fasteq from 'fast-deep-equal';
|
import fasteq from 'fast-deep-equal';
|
||||||
import {
|
|
||||||
DEVICE_STATE_OFF,
|
import { MAX_RECENT_SPEAKER_COUNT } from './TracksProvider';
|
||||||
DEVICE_STATE_BLOCKED,
|
|
||||||
DEVICE_STATE_LOADING,
|
|
||||||
} from './useDevices';
|
|
||||||
|
|
||||||
const initialParticipantsState = {
|
const initialParticipantsState = {
|
||||||
lastPendingUnknownActiveSpeaker: null,
|
lastPendingUnknownActiveSpeaker: null,
|
||||||
|
|
@ -39,26 +26,168 @@ const initialParticipantsState = {
|
||||||
screens: [],
|
screens: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Derived data ---
|
// --- Reducer and helpers --
|
||||||
|
|
||||||
function getId(participant) {
|
function participantsReducer(
|
||||||
return participant.local ? 'local' : participant.session_id;
|
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) {
|
function getNewParticipant(participant) {
|
||||||
const id = getId(participant);
|
const id = getId(participant);
|
||||||
|
|
||||||
|
|
@ -128,159 +257,29 @@ function getScreenItem(participant) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Derived data ---
|
||||||
|
|
||||||
const ACTIVE_SPEAKER = 'ACTIVE_SPEAKER';
|
function getId(participant) {
|
||||||
const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
|
return participant.local ? 'local' : participant.session_id;
|
||||||
const PARTICIPANT_UPDATED = 'PARTICIPANT_UPDATED';
|
}
|
||||||
const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
|
|
||||||
const SWAP_POSITION = 'SWAP_POSITION';
|
|
||||||
|
|
||||||
// --- Reducer --
|
function getScreenId(id) {
|
||||||
|
return `${id}-screen`;
|
||||||
|
}
|
||||||
|
|
||||||
function participantsReducer(prevState, action) {
|
function isLocalId(id) {
|
||||||
switch (action.type) {
|
return typeof id === 'string' && id === 'local';
|
||||||
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);
|
|
||||||
|
|
||||||
const participants = [...prevState.participants];
|
function isScreenId(id) {
|
||||||
const screens = [...prevState.screens];
|
return typeof id === 'string' && id.endsWith('-screen');
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ACTIVE_SPEAKER,
|
|
||||||
getId,
|
getId,
|
||||||
getScreenId,
|
getScreenId,
|
||||||
|
initialParticipantsState,
|
||||||
isLocalId,
|
isLocalId,
|
||||||
isScreenId,
|
isScreenId,
|
||||||
participantsReducer,
|
participantsReducer,
|
||||||
initialParticipantsState,
|
};
|
||||||
PARTICIPANT_JOINED,
|
|
||||||
PARTICIPANT_LEFT,
|
|
||||||
PARTICIPANT_UPDATED,
|
|
||||||
SWAP_POSITION,
|
|
||||||
};
|
|
||||||
|
|
@ -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';
|
import { getId, getScreenId } from './participantsState';
|
||||||
|
|
||||||
export const initialTracksState = {
|
const initialTracksState = {
|
||||||
audioTracks: {},
|
audioTracks: {},
|
||||||
videoTracks: {},
|
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 --
|
// --- Reducer and helpers --
|
||||||
|
|
||||||
export function tracksReducer(prevState, action) {
|
function tracksReducer(
|
||||||
|
prevState,
|
||||||
|
action
|
||||||
|
) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case TRACK_STARTED: {
|
case 'TRACK_STARTED': {
|
||||||
const id = getId(action.participant);
|
const id = getId(action.participant);
|
||||||
const screenId = getScreenId(id);
|
const screenId = getScreenId(id);
|
||||||
|
|
||||||
|
|
@ -63,17 +50,15 @@ export function tracksReducer(prevState, action) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'TRACKS_STOPPED': {
|
||||||
case TRACK_STOPPED: {
|
|
||||||
const { audioTracks, videoTracks } = prevState;
|
const { audioTracks, videoTracks } = prevState;
|
||||||
|
|
||||||
const newAudioTracks = { ...audioTracks };
|
const newAudioTracks = { ...audioTracks };
|
||||||
const newVideoTracks = { ...videoTracks };
|
const newVideoTracks = { ...videoTracks };
|
||||||
|
|
||||||
action.items.forEach(([participant, track]) => {
|
for (const [participant, track] of action.items) {
|
||||||
const id = participant ? getId(participant) : null;
|
const id = participant ? getId(participant) : null;
|
||||||
const screenId = participant ? getScreenId(id) : null;
|
const screenId = participant ? getScreenId(id) : null;
|
||||||
|
|
||||||
if (track.kind === 'audio') {
|
if (track.kind === 'audio') {
|
||||||
if (!participant?.local) {
|
if (!participant?.local) {
|
||||||
// Ignore local audio from mic and screen share
|
// Ignore local audio from mic and screen share
|
||||||
|
|
@ -88,16 +73,16 @@ export function tracksReducer(prevState, action) {
|
||||||
newVideoTracks[screenId] = participant.tracks.screenVideo;
|
newVideoTracks[screenId] = participant.tracks.screenVideo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audioTracks: newAudioTracks,
|
audioTracks: newAudioTracks,
|
||||||
videoTracks: newVideoTracks,
|
videoTracks: newVideoTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'UPDATE_AUDIO_TRACK': {
|
||||||
case TRACK_AUDIO_UPDATED: {
|
|
||||||
const id = getId(action.participant);
|
const id = getId(action.participant);
|
||||||
|
const screenId = getScreenId(id);
|
||||||
if (action.participant?.local) {
|
if (action.participant?.local) {
|
||||||
// Ignore local audio from mic and screen share
|
// Ignore local audio from mic and screen share
|
||||||
return prevState;
|
return prevState;
|
||||||
|
|
@ -105,14 +90,14 @@ export function tracksReducer(prevState, action) {
|
||||||
const newAudioTracks = {
|
const newAudioTracks = {
|
||||||
...prevState.audioTracks,
|
...prevState.audioTracks,
|
||||||
[id]: action.participant.tracks.audio,
|
[id]: action.participant.tracks.audio,
|
||||||
|
[screenId]: action.participant.tracks.screenAudio,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
audioTracks: newAudioTracks,
|
audioTracks: newAudioTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'UPDATE_VIDEO_TRACK': {
|
||||||
case TRACK_VIDEO_UPDATED: {
|
|
||||||
const id = getId(action.participant);
|
const id = getId(action.participant);
|
||||||
const newVideoTracks = {
|
const newVideoTracks = {
|
||||||
...prevState.videoTracks,
|
...prevState.videoTracks,
|
||||||
|
|
@ -123,8 +108,7 @@ export function tracksReducer(prevState, action) {
|
||||||
videoTracks: newVideoTracks,
|
videoTracks: newVideoTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'REMOVE_TRACKS': {
|
||||||
case REMOVE_TRACKS: {
|
|
||||||
const { audioTracks, videoTracks } = prevState;
|
const { audioTracks, videoTracks } = prevState;
|
||||||
const id = getId(action.participant);
|
const id = getId(action.participant);
|
||||||
const screenId = getScreenId(id);
|
const screenId = getScreenId(id);
|
||||||
|
|
@ -139,8 +123,9 @@ export function tracksReducer(prevState, action) {
|
||||||
videoTracks,
|
videoTracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { initialTracksState, tracksReducer };
|
||||||
|
|
@ -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_HIGH_BITRATE_CAP = 980;
|
||||||
const STANDARD_LOW_BITRATE_CAP = 300;
|
const STANDARD_LOW_BITRATE_CAP = 300;
|
||||||
|
|
||||||
export const useNetworkState = (
|
export const useNetworkState = (
|
||||||
callObject = null,
|
co = null,
|
||||||
quality = VIDEO_QUALITY_HIGH
|
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(
|
const setQuality = useCallback(
|
||||||
(q) => {
|
async (q) => {
|
||||||
if (!callObject) return;
|
if (!callObject) return;
|
||||||
|
|
||||||
const peers = Object.keys(callObject.participants()).length - 1;
|
const peers = Object.keys(callObject.participants()).length - 1;
|
||||||
const isSFU = callObject.getNetworkTopology().topology === 'sfu';
|
const isSFU = (await callObject.getNetworkTopology()).topology === 'sfu';
|
||||||
|
|
||||||
const lowKbs = isSFU
|
const lowKbs = isSFU
|
||||||
? STANDARD_LOW_BITRATE_CAP
|
? 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) {
|
switch (q) {
|
||||||
case VIDEO_QUALITY_HIGH:
|
case 'auto':
|
||||||
callObject.setBandwidth({ kbs: STANDARD_HIGH_BITRATE_CAP });
|
case 'high':
|
||||||
|
if (lastSetKBS.current === highKbs) break;
|
||||||
|
callObject.setBandwidth({
|
||||||
|
kbs: highKbs,
|
||||||
|
});
|
||||||
|
lastSetKBS.current = highKbs;
|
||||||
break;
|
break;
|
||||||
case VIDEO_QUALITY_LOW:
|
case 'low':
|
||||||
|
if (lastSetKBS.current === lowKbs) break;
|
||||||
callObject.setBandwidth({
|
callObject.setBandwidth({
|
||||||
kbs: lowKbs,
|
kbs: lowKbs,
|
||||||
});
|
});
|
||||||
|
lastSetKBS.current = lowKbs;
|
||||||
break;
|
break;
|
||||||
case VIDEO_QUALITY_BANDWIDTH_SAVER:
|
case 'bandwidth-saver':
|
||||||
callObject.setLocalVideo(false);
|
callObject.setLocalVideo(false);
|
||||||
|
if (lastSetKBS.current === lowKbs) break;
|
||||||
callObject.setBandwidth({
|
callObject.setBandwidth({
|
||||||
kbs: lowKbs,
|
kbs: lowKbs,
|
||||||
});
|
});
|
||||||
break;
|
lastSetKBS.current = lowKbs;
|
||||||
default:
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -56,43 +61,50 @@ export const useNetworkState = (
|
||||||
if (ev.threshold === threshold) return;
|
if (ev.threshold === threshold) return;
|
||||||
|
|
||||||
switch (ev.threshold) {
|
switch (ev.threshold) {
|
||||||
case NETWORK_STATE_VERY_LOW:
|
case 'very-low':
|
||||||
setQuality(VIDEO_QUALITY_BANDWIDTH_SAVER);
|
setQuality('bandwidth-saver');
|
||||||
setThreshold(NETWORK_STATE_VERY_LOW);
|
setThreshold('very-low');
|
||||||
break;
|
break;
|
||||||
case NETWORK_STATE_LOW:
|
case 'low':
|
||||||
|
setQuality(quality === 'bandwidth-saver' ? quality : 'low');
|
||||||
|
setThreshold('low');
|
||||||
|
break;
|
||||||
|
case 'good':
|
||||||
setQuality(
|
setQuality(
|
||||||
quality === VIDEO_QUALITY_BANDWIDTH_SAVER
|
['bandwidth-saver', 'low'].includes(quality) ? quality : 'high'
|
||||||
? quality
|
|
||||||
: NETWORK_STATE_LOW
|
|
||||||
);
|
);
|
||||||
setThreshold(NETWORK_STATE_LOW);
|
setThreshold('good');
|
||||||
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:
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setQuality, threshold, quality]
|
[quality, setQuality, threshold]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!callObject) return false;
|
if (!callObject) return;
|
||||||
callObject.on('network-quality-change', handleNetworkQualityChange);
|
callObject.on('network-quality-change', handleNetworkQualityChange);
|
||||||
return () =>
|
return () => {
|
||||||
callObject.off('network-quality-change', handleNetworkQualityChange);
|
callObject.off('network-quality-change', handleNetworkQualityChange);
|
||||||
|
};
|
||||||
}, [callObject, handleNetworkQualityChange]);
|
}, [callObject, handleNetworkQualityChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!callObject) return;
|
||||||
setQuality(quality);
|
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;
|
return threshold;
|
||||||
};
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export const sortByKey = (a, b, key, caseSensitive = true) => {
|
export const sortByKey = (key, caseSensitive = true) =>
|
||||||
|
(a, b) => {
|
||||||
const aKey =
|
const aKey =
|
||||||
!caseSensitive && typeof a[key] === 'string'
|
!caseSensitive && typeof a[key] === 'string'
|
||||||
? String(a[key])?.toLowerCase()
|
? String(a[key])?.toLowerCase()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue