300 lines
7.9 KiB
JavaScript
300 lines
7.9 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_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 pauseVideoTrack = useCallback(
|
|
(id) => {
|
|
/**
|
|
* Ignore undefined, local or screenshare.
|
|
*/
|
|
if (
|
|
!id ||
|
|
subscribeToTracksAutomatically ||
|
|
isLocalId(id) ||
|
|
isScreenId(id) ||
|
|
rtcpeers.getCurrentType() !== 'sfu'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
|
|
return;
|
|
}
|
|
|
|
rtcpeers.soup.pauseTrack(id, 'cam-video');
|
|
},
|
|
[subscribeToTracksAutomatically]
|
|
);
|
|
|
|
const resumeVideoTrack = useCallback(
|
|
(id) => {
|
|
/**
|
|
* Ignore undefined, local or screenshare.
|
|
*/
|
|
if (
|
|
!id ||
|
|
subscribeToTracksAutomatically ||
|
|
isLocalId(id) ||
|
|
isScreenId(id)
|
|
)
|
|
return;
|
|
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
|
|
|
|
const subscribe = () => {
|
|
if (videoTrack?.subscribed) return;
|
|
callObject.updateParticipant(id, {
|
|
setSubscribedTracks: true,
|
|
});
|
|
};
|
|
|
|
switch (rtcpeers.getCurrentType()) {
|
|
case 'peer-to-peer':
|
|
subscribe();
|
|
break;
|
|
case 'sfu': {
|
|
if (!rtcpeers.soup.implementationIsAcceptingCalls) return;
|
|
rtcpeers.soup.resumeTrack(id, 'cam-video');
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
[callObject, subscribeToTracksAutomatically]
|
|
);
|
|
|
|
const remoteParticipantIds = useMemo(
|
|
() => participants.filter((p) => !p.isLocal).map((p) => p.id),
|
|
[participants]
|
|
);
|
|
|
|
/**
|
|
* Updates cam subscriptions based on passed ids.
|
|
*
|
|
* @param ids Array of ids to subscribe to, all others will be unsubscribed.
|
|
* @param pausedIds Array of ids that should be subscribed, but paused.
|
|
*/
|
|
const updateCamSubscriptions = useCallback(
|
|
(ids, pausedIds = []) => {
|
|
if (!callObject || subscribeToTracksAutomatically) return;
|
|
const subscribedIds =
|
|
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
|
? [...remoteParticipantIds]
|
|
: [...ids, ...recentSpeakerIds];
|
|
|
|
const updates = remoteParticipantIds.reduce((u, id) => {
|
|
const shouldSubscribe = subscribedIds.includes(id);
|
|
const shouldPause = pausedIds.includes(id);
|
|
const isSubscribed =
|
|
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
|
|
|
/**
|
|
* Pause already subscribed tracks.
|
|
*/
|
|
if (shouldSubscribe && shouldPause) {
|
|
pauseVideoTrack(id);
|
|
}
|
|
|
|
/**
|
|
* Fast resume tracks.
|
|
*/
|
|
if (shouldSubscribe && !shouldPause) {
|
|
resumeVideoTrack(id);
|
|
}
|
|
|
|
if (
|
|
isLocalId(id) ||
|
|
isScreenId(id) ||
|
|
(shouldSubscribe && isSubscribed)
|
|
) {
|
|
return u;
|
|
}
|
|
|
|
const result = {
|
|
setSubscribedTracks: {
|
|
audio: true,
|
|
screenAudio: true,
|
|
screenVideo: true,
|
|
video: shouldSubscribe,
|
|
},
|
|
};
|
|
return { ...u, [id]: result };
|
|
}, {});
|
|
|
|
callObject.updateParticipants(updates);
|
|
},
|
|
[
|
|
callObject,
|
|
subscribeToTracksAutomatically,
|
|
remoteParticipantIds,
|
|
recentSpeakerIds,
|
|
pauseVideoTrack,
|
|
resumeVideoTrack,
|
|
]
|
|
);
|
|
|
|
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, pauseVideoTrack]);
|
|
|
|
return (
|
|
<TracksContext.Provider
|
|
value={{
|
|
audioTracks: state.audioTracks,
|
|
videoTracks: state.videoTracks,
|
|
pauseVideoTrack,
|
|
resumeVideoTrack,
|
|
updateCamSubscriptions,
|
|
remoteParticipantIds,
|
|
recentSpeakerIds,
|
|
}}
|
|
>
|
|
{children}
|
|
</TracksContext.Provider>
|
|
);
|
|
};
|
|
|
|
TracksProvider.propTypes = {
|
|
children: PropTypes.node,
|
|
};
|
|
|
|
export const useTracks = () => useContext(TracksContext);
|