updated contexts
This commit is contained in:
parent
9e44a018b4
commit
e8fd32ad56
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<TracksContext.Provider
|
||||
|
|
@ -150,7 +266,10 @@ export const TracksProvider = ({ children }) => {
|
|||
audioTracks: state.audioTracks,
|
||||
pauseVideoTrack,
|
||||
resumeVideoTrack,
|
||||
remoteParticipantIds,
|
||||
updateCamSubscriptions,
|
||||
videoTracks: state.videoTracks,
|
||||
recentSpeakerIds,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const useVideoTrack = (participant) => {
|
|||
!participant.isScreenshare)
|
||||
)
|
||||
return null;
|
||||
return videoTrack?.track;
|
||||
return videoTrack?.persistentTrack;
|
||||
}, [participant?.id, videoTracks]);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue