daily-examples/dailyjs/shared/contexts/TracksProvider.js

245 lines
7.1 KiB
JavaScript

/* global rtcpeers */
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
import { sortByKey } from '../lib/sortByKey';
import { useCallState } from './CallProvider';
import { useParticipants } from './ParticipantsProvider';
import { isLocalId, isScreenId } from './participantsState';
import {
initialTracksState,
REMOVE_TRACKS,
TRACK_STARTED,
TRACK_STOPPED,
tracksReducer,
} from './tracksState';
/**
* Maximum amount of concurrently subscribed most recent speakers.
*/
const MAX_RECENT_SPEAKER_COUNT = 6;
/**
* Threshold up to which all videos will be subscribed.
* If the remote participant count passes this threshold,
* cam subscriptions are defined by UI view modes.
*/
const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => {
const { callObject, subscribeToTracksAutomatically } = useCallState();
const { participants } = useParticipants();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
const recentSpeakerIds = useMemo(
() =>
participants
.filter((p) => Boolean(p.lastActiveDate) && !p.isLocal)
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
.slice(-MAX_RECENT_SPEAKER_COUNT)
.map((p) => p.id)
.reverse(),
[participants]
);
const remoteParticipantIds = useMemo(
() => participants.filter((p) => !p.isLocal).map((p) => p.id),
[participants]
);
const subscribeToCam = useCallback(
(id) => {
// Ignore undefined, local or screenshare.
if (!id || isLocalId(id) || isScreenId(id)) return;
callObject.updateParticipant(id, {
setSubscribedTracks: { video: true },
});
},
[callObject]
);
/**
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
* For ids not provided, cam tracks will be unsubscribed from
*/
const updateCamSubscriptions = useCallback(
(subscribedIds, stagedIds = []) => {
if (!callObject) return;
// If total number of remote participants is less than a threshold, simply
// stage all remote cams that aren't already marked for subscription.
// Otherwise, honor the provided stagedIds, with recent speakers appended
// who aren't already marked for subscription.
const stagedIdsFiltered =
remoteParticipantIds.length <= SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD
? remoteParticipantIds.filter((id) => !subscribedIds.includes(id))
: [
...stagedIds,
...recentSpeakerIds.filter((id) => !subscribedIds.includes(id)),
];
// Assemble updates to get to desired cam subscriptions
const updates = remoteParticipantIds.reduce((u, id) => {
let desiredSubscription;
const currentSubscription =
callObject.participants()?.[id]?.tracks?.video?.subscribed;
// Ignore undefined, local or screenshare participant ids
if (!id || isLocalId(id) || isScreenId(id)) return u;
// Decide on desired cam subscription for this participant:
// subscribed, staged, or unsubscribed
if (subscribedIds.includes(id)) {
desiredSubscription = true;
} else if (stagedIdsFiltered.includes(id)) {
desiredSubscription = 'staged';
} else {
desiredSubscription = false;
}
// Skip if we already have the desired subscription to this
// participant's cam
if (desiredSubscription === currentSubscription) return u;
return {
...u,
[id]: {
setSubscribedTracks: {
audio: true,
screenAudio: true,
screenVideo: true,
video: desiredSubscription,
},
},
};
}, {});
callObject.updateParticipants(updates);
},
[callObject, remoteParticipantIds, recentSpeakerIds]
);
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(() => {
if (!joinedSubscriptionQueue.length) return;
const ids = joinedSubscriptionQueue.splice(0);
const callParticipants = callObject.participants();
const updates = ids.reduce((o, id) => {
const { subscribed } = callParticipants?.[id]?.tracks?.audio;
const result = { ...o[id] };
if (!subscribed) {
result.setSubscribedTracks = {
audio: true,
screenAudio: true,
screenVideo: true,
};
}
if (rtcpeers?.getCurrentType?.() === 'peer-to-peer') {
result.setSubscribedTracks = true;
}
return { [id]: result };
}, {});
if (!subscribeToTracksAutomatically) {
callObject.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);
};
}, [callObject, subscribeToTracksAutomatically]);
return (
<TracksContext.Provider
value={{
audioTracks: state.audioTracks,
videoTracks: state.videoTracks,
subscribeToCam,
updateCamSubscriptions,
remoteParticipantIds,
recentSpeakerIds,
}}
>
{children}
</TracksContext.Provider>
);
};
TracksProvider.propTypes = {
children: PropTypes.node,
};
export const useTracks = () => useContext(TracksContext);