updated contexts

This commit is contained in:
J Taylor 2021-06-30 12:18:34 +01:00
parent 9e44a018b4
commit e8fd32ad56
5 changed files with 271 additions and 115 deletions

View File

@ -26,7 +26,7 @@ import {
export const ParticipantsContext = createContext(); export const ParticipantsContext = createContext();
export const ParticipantsProvider = ({ children }) => { export const ParticipantsProvider = ({ children }) => {
const { broadcast, callObject } = useCallState(); const { callObject } = useCallState();
const [state, dispatch] = useReducer( const [state, dispatch] = useReducer(
participantsReducer, participantsReducer,
initialParticipantsState initialParticipantsState
@ -37,20 +37,17 @@ export const ParticipantsProvider = ({ children }) => {
/** /**
* ALL participants (incl. shared screens) in a convenient array * ALL participants (incl. shared screens) in a convenient array
*/ */
const allParticipants = useDeepCompareMemo( const allParticipants = useMemo(
() => Object.values(state.participants), () => [...state.participants, ...state.screens],
[state?.participants] [state?.participants, state?.screens]
); );
/** /**
* Only return participants that should be visible in the call * Only return participants that should be visible in the call
*/ */
const participants = useDeepCompareMemo( const participants = useDeepCompareMemo(
() => () => allParticipants.filter((p) => p?.isOwner),
!broadcast [allParticipants]
? allParticipants
: allParticipants.filter((p) => p?.isOwner || p?.isScreenshare),
[broadcast, allParticipants]
); );
/** /**
@ -99,6 +96,19 @@ export const ParticipantsProvider = ({ children }) => {
const displayableParticipants = participants.filter((p) => !p?.isLocal); 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 const sorted = displayableParticipants
.sort((a, b) => sortByKey(a, b, 'lastActiveDate')) .sort((a, b) => sortByKey(a, b, 'lastActiveDate'))
.reverse(); .reverse();

View File

@ -1,46 +1,203 @@
/* global rtcpeers */
import React, { import React, {
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo,
useReducer, useReducer,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sortByKey } from '../lib/sortByKey';
import { useCallState } from './CallProvider'; import { useCallState } from './CallProvider';
import { useParticipants } from './ParticipantsProvider';
import { isLocalId, isScreenId } from './participantsState';
import { import {
initialTracksState, initialTracksState,
REMOVE_TRACKS, REMOVE_TRACKS,
TRACK_STARTED, TRACK_STARTED,
TRACK_STOPPED, TRACK_STOPPED,
UPDATE_TRACKS, UPDATE_SUBSCRIPTIONS,
tracksReducer, tracksReducer,
} from './tracksState'; } 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); const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => { export const TracksProvider = ({ children }) => {
const { callObject } = useCallState(); const { callObject } = useCallState();
const { participants } = useParticipants();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState); 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(() => { useEffect(() => {
if (!callObject) return false; if (!callObject) {
return false;
}
const trackStoppedQueue = [];
const handleTrackStarted = ({ participant, track }) => { 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({ dispatch({
type: TRACK_STARTED, type: TRACK_STARTED,
participant, participant,
track, track,
}); });
}; };
const trackStoppedBatchInterval = setInterval(() => {
if (!trackStoppedQueue.length) {
return;
}
dispatch({
type: TRACK_STOPPED,
items: trackStoppedQueue.splice(0, trackStoppedQueue.length),
});
}, 3000);
const handleTrackStopped = ({ participant, track }) => { const handleTrackStopped = ({ participant, track }) => {
if (participant) { if (participant) {
dispatch({ trackStoppedQueue.push([participant, track]);
type: TRACK_STOPPED,
participant,
track,
});
} }
}; };
const handleParticipantLeft = ({ participant }) => { const handleParticipantLeft = ({ participant }) => {
@ -49,12 +206,6 @@ export const TracksProvider = ({ children }) => {
participant, participant,
}); });
}; };
const handleParticipantUpdated = ({ participant }) => {
dispatch({
type: UPDATE_TRACKS,
participant,
});
};
const joinedSubscriptionQueue = []; const joinedSubscriptionQueue = [];
@ -62,14 +213,15 @@ export const TracksProvider = ({ children }) => {
joinedSubscriptionQueue.push(participant.session_id); joinedSubscriptionQueue.push(participant.session_id);
}; };
const batchInterval = setInterval(() => { const joinBatchInterval = setInterval(() => {
if (!joinedSubscriptionQueue.length) return; if (!joinedSubscriptionQueue.length) return;
const ids = joinedSubscriptionQueue.splice(0); const ids = joinedSubscriptionQueue.splice(0);
const participants = callObject.participants(); const callParticipants = callObject.participants();
const updates = ids.reduce((o, id) => { const updates = ids.reduce((o, id) => {
const { subscribed } = participants?.[id]?.tracks?.audio; const { subscribed } = callParticipants?.[id]?.tracks?.audio;
const result = {};
if (!subscribed) { if (!subscribed) {
o[id] = { result[id] = {
setSubscribedTracks: { setSubscribedTracks: {
audio: true, audio: true,
screenAudio: 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); callObject.updateParticipants(updates);
}, 100); }, 100);
@ -86,63 +242,23 @@ export const TracksProvider = ({ children }) => {
callObject.on('track-stopped', handleTrackStopped); callObject.on('track-stopped', handleTrackStopped);
callObject.on('participant-joined', handleParticipantJoined); callObject.on('participant-joined', handleParticipantJoined);
callObject.on('participant-left', handleParticipantLeft); callObject.on('participant-left', handleParticipantLeft);
callObject.on('participant-updated', handleParticipantUpdated);
return () => { return () => {
clearInterval(batchInterval); clearInterval(joinBatchInterval);
clearInterval(trackStoppedBatchInterval);
callObject.off('track-started', handleTrackStarted); callObject.off('track-started', handleTrackStarted);
callObject.off('track-stopped', handleTrackStopped); callObject.off('track-stopped', handleTrackStopped);
callObject.off('participant-joined', handleParticipantJoined); callObject.off('participant-joined', handleParticipantJoined);
callObject.off('participant-left', handleParticipantLeft); callObject.off('participant-left', handleParticipantLeft);
callObject.off('participant-updated', handleParticipantUpdated);
}; };
}, [callObject]); }, [callObject, pauseVideoTrack, state.subscriptions.video]);
const pauseVideoTrack = useCallback( useEffect(() => {
(id) => { Object.values(state.subscriptions.video).forEach(({ id, paused }) => {
if (!callObject) return; if (paused) {
/** pauseVideoTrack(id);
* Ignore undefined, local or screenshare.
*/
if (!id || id.includes('local') || id.includes('screen')) return;
// eslint-disable-next-line
if (!rtcpeers.soup.implementationIsAcceptingCalls) {
return;
} }
// eslint-disable-next-line });
const consumer = rtcpeers.soup?.findConsumerForTrack(id, 'cam-video'); }, [pauseVideoTrack, state.subscriptions.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]
);
return ( return (
<TracksContext.Provider <TracksContext.Provider
@ -150,7 +266,10 @@ export const TracksProvider = ({ children }) => {
audioTracks: state.audioTracks, audioTracks: state.audioTracks,
pauseVideoTrack, pauseVideoTrack,
resumeVideoTrack, resumeVideoTrack,
remoteParticipantIds,
updateCamSubscriptions,
videoTracks: state.videoTracks, videoTracks: state.videoTracks,
recentSpeakerIds,
}} }}
> >
{children} {children}

View File

@ -46,6 +46,14 @@ function getScreenId(id) {
return `${id}-screen`; return `${id}-screen`;
} }
function isLocalId(id) {
return typeof id === 'string' && id === 'local';
}
function isScreenId(id) {
return typeof id === 'string' && id.endsWith('-screen');
}
// ---Helpers --- // ---Helpers ---
function getMaxPosition(participants) { function getMaxPosition(participants) {
@ -261,10 +269,12 @@ export {
ACTIVE_SPEAKER, ACTIVE_SPEAKER,
getId, getId,
getScreenId, getScreenId,
isLocalId,
isScreenId,
participantsReducer,
initialParticipantsState, initialParticipantsState,
PARTICIPANT_JOINED, PARTICIPANT_JOINED,
PARTICIPANT_LEFT, PARTICIPANT_LEFT,
PARTICIPANT_UPDATED, PARTICIPANT_UPDATED,
participantsReducer,
SWAP_POSITION, SWAP_POSITION,
}; };

View File

@ -3,6 +3,9 @@ import { getId, getScreenId } from './participantsState';
const initialTracksState = { const initialTracksState = {
audioTracks: {}, audioTracks: {},
videoTracks: {}, videoTracks: {},
subscriptions: {
video: {},
},
}; };
// --- Actions --- // --- Actions ---
@ -10,19 +13,21 @@ const initialTracksState = {
const TRACK_STARTED = 'TRACK_STARTED'; const TRACK_STARTED = 'TRACK_STARTED';
const TRACK_STOPPED = 'TRACK_STOPPED'; const TRACK_STOPPED = 'TRACK_STOPPED';
const REMOVE_TRACKS = 'REMOVE_TRACKS'; const REMOVE_TRACKS = 'REMOVE_TRACKS';
const UPDATE_TRACKS = 'UPDATE_TRACKS'; const UPDATE_SUBSCRIPTIONS = 'UPDATE_SUBSCRIPTIONS';
// --- Reducer and helpers -- // --- Reducer and helpers --
function tracksReducer(prevState, action) { function tracksReducer(prevState, action) {
switch (action.type) { switch (action.type) {
case TRACK_STARTED: case TRACK_STARTED: {
case TRACK_STOPPED: { const id = getId(action.participant);
const id = action.participant ? getId(action.participant) : null; const screenId = getScreenId(id);
const screenId = action.participant ? getScreenId(id) : null;
if (action.track.kind === 'audio' && !action.participant?.local) { if (action.track.kind === 'audio') {
// Ignore local audio from mic and screen share if (action.participant?.local) {
// Ignore local audio from mic and screen share
return prevState;
}
const newAudioTracks = { const newAudioTracks = {
[id]: action.participant.tracks.audio, [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: { case REMOVE_TRACKS: {
const { audioTracks, videoTracks } = prevState; const { audioTracks, subscriptions, videoTracks } = prevState;
const id = getId(action.participant); const id = getId(action.participant);
const screenId = getScreenId(id); const screenId = getScreenId(id);
@ -64,39 +103,17 @@ function tracksReducer(prevState, action) {
return { return {
audioTracks, audioTracks,
subscriptions,
videoTracks, 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 { return {
audioTracks: newAudioTracks, ...prevState,
videoTracks: newVideoTracks, subscriptions: action.subscriptions,
}; };
}
default: default:
throw new Error(); throw new Error();
} }
@ -104,9 +121,9 @@ function tracksReducer(prevState, action) {
export { export {
initialTracksState, initialTracksState,
tracksReducer,
REMOVE_TRACKS, REMOVE_TRACKS,
TRACK_STARTED, TRACK_STARTED,
TRACK_STOPPED, TRACK_STOPPED,
UPDATE_TRACKS, UPDATE_SUBSCRIPTIONS,
tracksReducer,
}; };

View File

@ -15,7 +15,7 @@ export const useVideoTrack = (participant) => {
!participant.isScreenshare) !participant.isScreenshare)
) )
return null; return null;
return videoTrack?.track; return videoTrack?.persistentTrack;
}, [participant?.id, videoTracks]); }, [participant?.id, videoTracks]);
}; };