/* 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 } = 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 || isLocalId(id) || isScreenId(id) || rtcpeers.getCurrentType() !== 'sfu' ) { return; } if (!rtcpeers.soup.implementationIsAcceptingCalls) { return; } const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); if (!consumer) { rtcpeers.soup.setResumeOnSubscribeForTrack(id, 'cam-video', false); } else { rtcpeers.soup.pauseConsumer(consumer); } }, []); const resumeVideoTrack = useCallback( (id) => { /** * Ignore undefined, local or screenshare. */ if (!id || 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; const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); if (!(consumer && consumer.appData)) { rtcpeers.soup.setResumeOnSubscribeForTrack(id, 'cam-video', true); subscribe(); } else { rtcpeers.soup.resumeConsumer(consumer); } break; } default: break; } }, [callObject] ); 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) 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, 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 }; }, {}); 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, pauseVideoTrack]); return ( {children} ); }; TracksProvider.propTypes = { children: PropTypes.node, }; export const useTracks = () => useContext(TracksContext);