daily-examples/custom/shared/contexts/participantsState.js

285 lines
7.5 KiB
JavaScript

import fasteq from 'fast-deep-equal';
import { MAX_RECENT_SPEAKER_COUNT } from './TracksProvider';
const initialParticipantsState = {
lastPendingUnknownActiveSpeaker: null,
participants: [
{
camMutedByHost: false,
hasNameSet: false,
id: 'local',
isActiveSpeaker: false,
isCamMuted: false,
isLoading: true,
isLocal: true,
isMicMuted: false,
isOwner: false,
isRecording: false,
isScreenshare: false,
lastActiveDate: null,
micMutedByHost: false,
name: '',
sessionId: '',
},
],
screens: [],
};
// --- Reducer and helpers --
function participantsReducer(
prevState,
action
) {
switch (action.type) {
case 'ACTIVE_SPEAKER': {
const { participants, ...state } = prevState;
if (!action.id)
return {
...prevState,
lastPendingUnknownActiveSpeaker: null,
};
const date = new Date();
const isParticipantKnown = participants.some((p) => p.id === action.id);
return {
...state,
lastPendingUnknownActiveSpeaker: isParticipantKnown
? null
: {
date,
id: action.id,
},
participants: participants.map((p) => ({
...p,
isActiveSpeaker: p.id === action.id,
lastActiveDate: p.id === action.id ? date : p?.lastActiveDate,
})),
};
}
case 'JOINED_MEETING': {
const localItem = getNewParticipant(action.participant);
const participants = [...prevState.participants].map((p) =>
p.isLocal ? localItem : p
);
return {
...prevState,
participants,
};
}
case 'PARTICIPANT_JOINED': {
const item = getNewParticipant(action.participant);
const participants = [...prevState.participants];
const screens = [...prevState.screens];
const isPendingActiveSpeaker =
item.id === prevState.lastPendingUnknownActiveSpeaker?.id;
if (isPendingActiveSpeaker) {
item.isActiveSpeaker = true;
item.lastActiveDate = prevState.lastPendingUnknownActiveSpeaker?.date;
}
if (item.isCamMuted) {
participants.push(item);
} else {
const firstInactiveCamOffIndex = prevState.participants.findIndex(
(p) => p.isCamMuted && !p.isLocal && !p.isActiveSpeaker
);
if (firstInactiveCamOffIndex >= 0) {
participants.splice(firstInactiveCamOffIndex, 0, item);
} else {
participants.push(item);
}
}
// Mark new participant as active speaker, for quicker audio subscription
if (
!item.isMicMuted &&
participants.length <= MAX_RECENT_SPEAKER_COUNT + 1 // + 1 for local participant
) {
item.lastActiveDate = new Date();
}
// Participant is sharing screen
if (action.participant.screen) {
screens.push(getScreenItem(action.participant));
}
return {
...prevState,
lastPendingUnknownActiveSpeaker: isPendingActiveSpeaker
? null
: prevState.lastPendingUnknownActiveSpeaker,
participants,
screens,
};
}
case 'PARTICIPANT_UPDATED': {
const item = getUpdatedParticipant(
action.participant,
prevState.participants
);
const { id } = item;
const screenId = getScreenId(id);
const participants = [...prevState.participants];
const idx = participants.findIndex((p) => p.id === id);
if (!item.isMicMuted && participants[idx].isMicMuted) {
// Participant unmuted mic
item.lastActiveDate = new Date();
}
participants[idx] = item;
const screens = [...prevState.screens];
const screenIdx = screens.findIndex((s) => s.id === screenId);
if (action.participant.screen) {
const screenItem = getScreenItem(action.participant);
if (screenIdx >= 0) {
screens[screenIdx] = screenItem;
} else {
screens.push(screenItem);
}
} else if (screenIdx >= 0) {
screens.splice(screenIdx, 1);
}
const newState = {
...prevState,
participants,
screens,
};
if (fasteq(newState, prevState)) {
return prevState;
}
return newState;
}
case 'PARTICIPANT_LEFT': {
const id = getId(action.participant);
const screenId = getScreenId(id);
return {
...prevState,
participants: [...prevState.participants].filter((p) => p.id !== id),
screens: [...prevState.screens].filter((s) => s.id !== screenId),
};
}
case 'SWAP_POSITION': {
const participants = [...prevState.participants];
if (!action.id1 || !action.id2) return prevState;
const idx1 = participants.findIndex((p) => p.id === action.id1);
const idx2 = participants.findIndex((p) => p.id === action.id2);
if (idx1 === -1 || idx2 === -1) return prevState;
const tmp = participants[idx1];
participants[idx1] = participants[idx2];
participants[idx2] = tmp;
return {
...prevState,
participants,
};
}
default:
throw new Error();
}
}
function getNewParticipant(participant) {
const id = getId(participant);
const { local } = participant;
const { audio, video } = participant.tracks;
return {
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
isActiveSpeaker: false,
isCamMuted: video?.state === 'off' || video?.state === 'blocked',
isLoading: audio?.state === 'loading' || video?.state === 'loading',
isLocal: local,
isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
isOwner: !!participant.owner,
isRecording: !!participant.record,
isScreenshare: false,
lastActiveDate: null,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
sessionId: participant.session_id,
};
}
function getUpdatedParticipant(
participant,
participants
) {
const id = getId(participant);
const prevItem = participants.find((p) => p.id === id);
// In case we haven't set up this participant, yet.
if (!prevItem) return getNewParticipant(participant);
const { local } = participant;
const { audio, video } = participant.tracks;
return {
...prevItem,
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
isCamMuted: video?.state === 'off' || video?.state === 'blocked',
isLoading: audio?.state === 'loading' || video?.state === 'loading',
isLocal: local,
isMicMuted: audio?.state === 'off' || audio?.state === 'blocked',
isOwner: !!participant.owner,
isRecording: !!participant.record,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
sessionId: participant.session_id,
};
}
function getScreenItem(participant) {
const id = getId(participant);
return {
hasNameSet: null,
id: getScreenId(id),
isLoading: false,
isLocal: participant.local,
isScreenshare: true,
lastActiveDate: null,
name: participant.user_name,
sessionId: participant.session_id,
};
}
// --- Derived data ---
function getId(participant) {
return participant.local ? 'local' : participant.session_id;
}
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');
}
export {
getId,
getScreenId,
initialParticipantsState,
isLocalId,
isScreenId,
participantsReducer,
};