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

271 lines
6.7 KiB
JavaScript

/**
* Call state is comprised of:
* - "Call items" (inputs to the call, i.e. participants or shared screens)
* - UI state that depends on call items (for now, just whether to show "click allow" message)
*
* Call items are keyed by id:
* - "local" for the current participant
* - A session id for each remote participant
* - "<id>-screen" for each shared screen
*/
import {
DEVICE_STATE_OFF,
DEVICE_STATE_BLOCKED,
DEVICE_STATE_LOADING,
} from './useDevices';
const initialParticipantsState = {
participants: {
local: {
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: '',
position: 1,
},
},
};
// --- Derived data ---
function getId(participant) {
return participant.local ? 'local' : participant.user_id;
}
function getScreenId(id) {
return `${id}-screen`;
}
// ---Helpers ---
function getMaxPosition(participants) {
return Math.max(
1,
Math.max(...Object.values(participants).map(({ position }) => position))
);
}
function getUpdatedParticipant(participant, participants) {
const id = getId(participant);
const prevItem = participants[id];
const { local } = participant;
const { audio, video } = participant.tracks;
return {
...prevItem,
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
isCamMuted:
video?.state === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isLocal: local,
isMicMuted:
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isOwner: !!participant.owner,
isRecording: !!participant.record,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
};
}
function getNewParticipant(participant, participants) {
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 === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,
isLoading:
audio?.state === DEVICE_STATE_LOADING ||
video?.state === DEVICE_STATE_LOADING,
isLocal: local,
isMicMuted:
audio?.state === DEVICE_STATE_OFF ||
audio?.state === DEVICE_STATE_BLOCKED,
isOwner: !!participant.owner,
isRecording: !!participant.record,
isScreenshare: false,
lastActiveDate: null,
micMutedByHost: audio?.off?.byRemoteRequest,
name: participant.user_name,
position: local ? 0 : getMaxPosition(participants) + 1,
};
}
function getScreenItem(participant, participants) {
const id = getId(participant);
return {
hasNameSet: null,
id: getScreenId(id),
isLoading: false,
isLocal: participant.local,
isScreenshare: true,
lastActiveDate: null,
name: participant.user_name,
position: getMaxPosition(participants) + 1,
};
}
// --- Actions ---
const ACTIVE_SPEAKER = 'ACTIVE_SPEAKER';
const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
const PARTICIPANT_UPDATED = 'PARTICIPANT_UPDATED';
const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
const SWAP_POSITION = 'SWAP_POSITION';
// --- Reducer --
function participantsReducer(prevState, action) {
switch (action.type) {
case ACTIVE_SPEAKER: {
const { participants, ...state } = prevState;
if (!action.id) return prevState;
return {
...state,
participants: Object.keys(participants).reduce(
(items, id) => ({
...items,
[id]: {
...participants[id],
isActiveSpeaker: id === action.id,
lastActiveDate:
id === action.id
? new Date()
: participants[id]?.lastActiveDate,
},
}),
{}
),
};
}
case PARTICIPANT_JOINED: {
const item = getNewParticipant(
action.participant,
prevState.participants
);
const { id } = item;
const screenId = getScreenId(id);
const newParticipants = {
...prevState.participants,
[id]: item,
};
// Participant is sharing screen
if (action.participant.screen) {
newParticipants[screenId] = getScreenItem(
action.participant,
newParticipants
);
}
return {
...prevState,
participants: newParticipants,
};
}
case PARTICIPANT_UPDATED: {
const item = getUpdatedParticipant(
action.participant,
prevState.participants
);
const { id } = item;
const screenId = getScreenId(id);
const newParticipants = {
...prevState.participants,
};
newParticipants[id] = item;
if (action.participant.screen) {
newParticipants[screenId] = getScreenItem(
action.participant,
newParticipants
);
} else {
delete newParticipants[screenId];
}
return {
...prevState,
participants: newParticipants,
};
}
case PARTICIPANT_LEFT: {
const id = getId(action.participant);
const screenId = getScreenId(id);
const { ...participants } = prevState.participants;
delete participants[id];
delete participants[screenId];
return {
...prevState,
participants,
};
}
case SWAP_POSITION: {
const { participants, ...state } = prevState;
if (!action.id1 || !action.id2) return prevState;
const pos1 = participants[action.id1]?.position;
const pos2 = participants[action.id2]?.position;
if (!pos1 || !pos2) return prevState;
return {
...state,
participants: Object.keys(participants).reduce((items, id) => {
let { position } = participants[id];
if (action.id1 === id) {
position = pos2;
}
if (action.id2 === id) {
position = pos1;
}
return {
...items,
[id]: {
...participants[id],
position,
},
};
}, {}),
};
}
default:
throw new Error();
}
}
export {
ACTIVE_SPEAKER,
getId,
getScreenId,
initialParticipantsState,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
participantsReducer,
SWAP_POSITION,
};