From e8fd32ad564798f31b8d7f9dec891455c6576e89 Mon Sep 17 00:00:00 2001 From: J Taylor Date: Wed, 30 Jun 2021 12:18:34 +0100 Subject: [PATCH] updated contexts --- .../shared/contexts/ParticipantsProvider.js | 28 +- dailyjs/shared/contexts/TracksProvider.js | 253 +++++++++++++----- dailyjs/shared/contexts/participantsState.js | 12 +- dailyjs/shared/contexts/tracksState.js | 91 ++++--- dailyjs/shared/hooks/useVideoTrack.js | 2 +- 5 files changed, 271 insertions(+), 115 deletions(-) diff --git a/dailyjs/shared/contexts/ParticipantsProvider.js b/dailyjs/shared/contexts/ParticipantsProvider.js index 8fc28c5..a39a984 100644 --- a/dailyjs/shared/contexts/ParticipantsProvider.js +++ b/dailyjs/shared/contexts/ParticipantsProvider.js @@ -26,7 +26,7 @@ import { export const ParticipantsContext = createContext(); export const ParticipantsProvider = ({ children }) => { - const { broadcast, callObject } = useCallState(); + const { callObject } = useCallState(); const [state, dispatch] = useReducer( participantsReducer, initialParticipantsState @@ -37,20 +37,17 @@ export const ParticipantsProvider = ({ children }) => { /** * ALL participants (incl. shared screens) in a convenient array */ - const allParticipants = useDeepCompareMemo( - () => Object.values(state.participants), - [state?.participants] + const allParticipants = useMemo( + () => [...state.participants, ...state.screens], + [state?.participants, state?.screens] ); /** * Only return participants that should be visible in the call */ const participants = useDeepCompareMemo( - () => - !broadcast - ? allParticipants - : allParticipants.filter((p) => p?.isOwner || p?.isScreenshare), - [broadcast, allParticipants] + () => allParticipants.filter((p) => p?.isOwner), + [allParticipants] ); /** @@ -99,6 +96,19 @@ export const ParticipantsProvider = ({ children }) => { const displayableParticipants = participants.filter((p) => !p?.isLocal); + 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] + ); + } + const sorted = displayableParticipants .sort((a, b) => sortByKey(a, b, 'lastActiveDate')) .reverse(); diff --git a/dailyjs/shared/contexts/TracksProvider.js b/dailyjs/shared/contexts/TracksProvider.js index f67986c..121f065 100644 --- a/dailyjs/shared/contexts/TracksProvider.js +++ b/dailyjs/shared/contexts/TracksProvider.js @@ -1,46 +1,203 @@ +/* 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, - UPDATE_TRACKS, + UPDATE_SUBSCRIPTIONS, 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)) return; + if (!rtcpeers.soup.implementationIsAcceptingCalls) { + return; + } + const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); + if (!consumer) return; + 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; + if (!videoTrack?.subscribed) { + callObject.updateParticipant(id, { + setSubscribedTracks: true, + }); + return; + } + if (!rtcpeers.soup.implementationIsAcceptingCalls) { + return; + } + const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); + if (!consumer) return; + rtcpeers.soup?.resumeConsumer(consumer); + }, + [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 isSubscribed = + callObject.participants()?.[id]?.tracks?.video?.subscribed; + if ( + isLocalId(id) || + isScreenId(id) || + (shouldSubscribe && isSubscribed) + ) + return u; + const result = { + setSubscribedTracks: { + audio: true, + screenAudio: true, + screenVideo: true, + video: shouldSubscribe, + }, + }; + return { ...u, [id]: result }; + }, {}); + + dispatch({ + type: UPDATE_SUBSCRIPTIONS, + subscriptions: { + video: subscribedIds.reduce((v, id) => { + const result = { + id, + paused: pausedIds.includes(id) || !ids.includes(id), + }; + return { ...v, [id]: result }; + }, {}), + }, + }); + + ids + .filter((id) => !pausedIds.includes(id)) + .forEach((id) => { + const p = callObject.participants()?.[id]; + if (p?.tracks?.video?.subscribed) { + resumeVideoTrack(id); + } + }); + + callObject.updateParticipants(updates); + }, + [callObject, remoteParticipantIds, recentSpeakerIds, resumeVideoTrack] + ); + useEffect(() => { - if (!callObject) return false; + if (!callObject) { + return false; + } + + const trackStoppedQueue = []; const handleTrackStarted = ({ participant, track }) => { + if (state.subscriptions.video?.[participant.session_id]?.paused) { + pauseVideoTrack(participant.session_id); + } + /** + * 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) { - dispatch({ - type: TRACK_STOPPED, - participant, - track, - }); + trackStoppedQueue.push([participant, track]); } }; const handleParticipantLeft = ({ participant }) => { @@ -49,12 +206,6 @@ export const TracksProvider = ({ children }) => { participant, }); }; - const handleParticipantUpdated = ({ participant }) => { - dispatch({ - type: UPDATE_TRACKS, - participant, - }); - }; const joinedSubscriptionQueue = []; @@ -62,14 +213,15 @@ export const TracksProvider = ({ children }) => { joinedSubscriptionQueue.push(participant.session_id); }; - const batchInterval = setInterval(() => { + const joinBatchInterval = setInterval(() => { if (!joinedSubscriptionQueue.length) return; const ids = joinedSubscriptionQueue.splice(0); - const participants = callObject.participants(); + const callParticipants = callObject.participants(); const updates = ids.reduce((o, id) => { - const { subscribed } = participants?.[id]?.tracks?.audio; + const { subscribed } = callParticipants?.[id]?.tracks?.audio; + const result = {}; if (!subscribed) { - o[id] = { + result[id] = { setSubscribedTracks: { audio: true, screenAudio: true, @@ -77,7 +229,11 @@ export const TracksProvider = ({ children }) => { }, }; } - return o; + + if (rtcpeers?.getCurrentType?.() === 'peer-to-peer') { + result[id].setSubscribedTracks.video = true; + } + return { ...o, ...result }; }, {}); callObject.updateParticipants(updates); }, 100); @@ -86,63 +242,23 @@ export const TracksProvider = ({ children }) => { callObject.on('track-stopped', handleTrackStopped); callObject.on('participant-joined', handleParticipantJoined); callObject.on('participant-left', handleParticipantLeft); - callObject.on('participant-updated', handleParticipantUpdated); return () => { - clearInterval(batchInterval); + 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.off('participant-updated', handleParticipantUpdated); }; - }, [callObject]); + }, [callObject, pauseVideoTrack, state.subscriptions.video]); - const pauseVideoTrack = useCallback( - (id) => { - if (!callObject) return; - /** - * Ignore undefined, local or screenshare. - */ - if (!id || id.includes('local') || id.includes('screen')) return; - // eslint-disable-next-line - if (!rtcpeers.soup.implementationIsAcceptingCalls) { - return; + useEffect(() => { + Object.values(state.subscriptions.video).forEach(({ id, paused }) => { + if (paused) { + pauseVideoTrack(id); } - // eslint-disable-next-line - const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); - if (!consumer) return; - // eslint-disable-next-line - rtcpeers.soup?.pauseConsumer(consumer); - }, - [callObject] - ); - - const resumeVideoTrack = useCallback( - (id) => { - /** - * Ignore undefined, local or screenshare. - */ - if (!id || id.includes('local') || id.includes('screen')) return; - - const videoTrack = callObject.participants()?.[id]?.tracks?.video; - if (!videoTrack?.subscribed) { - callObject.updateParticipant(id, { - setSubscribedTracks: true, - }); - return; - } - // eslint-disable-next-line - if (!rtcpeers.soup.implementationIsAcceptingCalls) { - return; - } - // eslint-disable-next-line - const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); - if (!consumer) return; - // eslint-disable-next-line - rtcpeers.soup?.resumeConsumer(consumer); - }, - [callObject] - ); + }); + }, [pauseVideoTrack, state.subscriptions.video]); return ( { audioTracks: state.audioTracks, pauseVideoTrack, resumeVideoTrack, + remoteParticipantIds, + updateCamSubscriptions, videoTracks: state.videoTracks, + recentSpeakerIds, }} > {children} diff --git a/dailyjs/shared/contexts/participantsState.js b/dailyjs/shared/contexts/participantsState.js index cffab31..77a1e27 100644 --- a/dailyjs/shared/contexts/participantsState.js +++ b/dailyjs/shared/contexts/participantsState.js @@ -46,6 +46,14 @@ 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 getMaxPosition(participants) { @@ -261,10 +269,12 @@ export { ACTIVE_SPEAKER, getId, getScreenId, + isLocalId, + isScreenId, + participantsReducer, initialParticipantsState, PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED, - participantsReducer, SWAP_POSITION, }; diff --git a/dailyjs/shared/contexts/tracksState.js b/dailyjs/shared/contexts/tracksState.js index 36d5966..6bcdd19 100644 --- a/dailyjs/shared/contexts/tracksState.js +++ b/dailyjs/shared/contexts/tracksState.js @@ -3,6 +3,9 @@ import { getId, getScreenId } from './participantsState'; const initialTracksState = { audioTracks: {}, videoTracks: {}, + subscriptions: { + video: {}, + }, }; // --- Actions --- @@ -10,19 +13,21 @@ const initialTracksState = { const TRACK_STARTED = 'TRACK_STARTED'; const TRACK_STOPPED = 'TRACK_STOPPED'; const REMOVE_TRACKS = 'REMOVE_TRACKS'; -const UPDATE_TRACKS = 'UPDATE_TRACKS'; +const UPDATE_SUBSCRIPTIONS = 'UPDATE_SUBSCRIPTIONS'; // --- Reducer and helpers -- function tracksReducer(prevState, action) { switch (action.type) { - case TRACK_STARTED: - case TRACK_STOPPED: { - const id = action.participant ? getId(action.participant) : null; - const screenId = action.participant ? getScreenId(id) : null; + case TRACK_STARTED: { + const id = getId(action.participant); + const screenId = getScreenId(id); - if (action.track.kind === 'audio' && !action.participant?.local) { - // Ignore local audio from mic and screen share + if (action.track.kind === 'audio') { + if (action.participant?.local) { + // Ignore local audio from mic and screen share + return prevState; + } const newAudioTracks = { [id]: action.participant.tracks.audio, }; @@ -52,8 +57,42 @@ function tracksReducer(prevState, action) { }, }; } + case TRACK_STOPPED: { + const { audioTracks, subscriptions, videoTracks } = prevState; + + const newAudioTracks = { ...audioTracks }; + const newSubscriptions = { ...subscriptions }; + const newVideoTracks = { ...videoTracks }; + + action.items.forEach(({ participant, track }) => { + 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 + newAudioTracks[id] = participant.tracks.audio; + if (participant.screen) { + newAudioTracks[screenId] = participant.tracks.screenAudio; + } + } + } else if (track.kind === 'video') { + newVideoTracks[id] = participant.tracks.video; + if (participant.screen) { + newVideoTracks[screenId] = participant.tracks.screenVideo; + } + } + }); + + return { + audioTracks: newAudioTracks, + subscriptions: newSubscriptions, + videoTracks: newVideoTracks, + }; + } + case REMOVE_TRACKS: { - const { audioTracks, videoTracks } = prevState; + const { audioTracks, subscriptions, videoTracks } = prevState; const id = getId(action.participant); const screenId = getScreenId(id); @@ -64,39 +103,17 @@ function tracksReducer(prevState, action) { return { audioTracks, + subscriptions, videoTracks, }; } - case UPDATE_TRACKS: { - const { audioTracks, videoTracks } = prevState; - const id = getId(action.participant); - const screenId = getScreenId(id); - - const newAudioTracks = { - ...audioTracks, - }; - const newVideoTracks = { - ...videoTracks, - [id]: action.participant.tracks.video, - }; - if (!action.participant.local) { - newAudioTracks[id] = action.participant.tracks.audio; - } - if (action.participant.screen) { - newVideoTracks[screenId] = action.participant.tracks.screenVideo; - if (!action.participant.local) { - newAudioTracks[screenId] = action.participant.tracks.screenAudio; - } - } else { - delete newAudioTracks[screenId]; - delete newVideoTracks[screenId]; - } + case UPDATE_SUBSCRIPTIONS: return { - audioTracks: newAudioTracks, - videoTracks: newVideoTracks, + ...prevState, + subscriptions: action.subscriptions, }; - } + default: throw new Error(); } @@ -104,9 +121,9 @@ function tracksReducer(prevState, action) { export { initialTracksState, + tracksReducer, REMOVE_TRACKS, TRACK_STARTED, TRACK_STOPPED, - UPDATE_TRACKS, - tracksReducer, + UPDATE_SUBSCRIPTIONS, }; diff --git a/dailyjs/shared/hooks/useVideoTrack.js b/dailyjs/shared/hooks/useVideoTrack.js index 3b7a44f..0b2129f 100644 --- a/dailyjs/shared/hooks/useVideoTrack.js +++ b/dailyjs/shared/hooks/useVideoTrack.js @@ -15,7 +15,7 @@ export const useVideoTrack = (participant) => { !participant.isScreenshare) ) return null; - return videoTrack?.track; + return videoTrack?.persistentTrack; }, [participant?.id, videoTracks]); };