import { RosterAttendeeType } from 'amazon-chime-sdk-component-library-react'; import {VideoPriorityBasedPolicy, VideoSource} from 'amazon-chime-sdk-js'; import { Layout } from '../../types'; import { isContentShare, updateDownlinkPreferences } from './Utils'; export interface GridState { layout: Layout; isZoomed: boolean; threshold: number; } export interface AttendeeState { attendeeId: string; name: string; videoEnabled: boolean; bandwidthConstrained: boolean; } export interface VideoSourceState { cameraSources: string[]; activeSpeakersWithCameraSource: string[]; contentShareId: string | null; hasLocalVideo: boolean; hasLocalContentSharing: boolean; } export type State = { gridState: GridState; attendeeStates: { [attendeeId: string]: AttendeeState }; videoSourceState: VideoSourceState; priorityBasedPolicy: VideoPriorityBasedPolicy | undefined; }; export const initialState: State = { gridState: { layout: Layout.Gallery, isZoomed: false, threshold: 8, }, attendeeStates: {}, videoSourceState: { cameraSources: [], activeSpeakersWithCameraSource: [], contentShareId: null, hasLocalVideo: false, hasLocalContentSharing: false, }, priorityBasedPolicy: undefined, }; export type RosterType = { [attendeeId: string]: RosterAttendeeType; }; export type Controls = { zoomIn: () => void; zoomOut: () => void; }; export enum VideoTileGridAction { UpdateAttendeeStates, UpdateVideoSources, UpdateActiveSpeakers, UpdateLocalSourceState, UpdateLayout, PauseVideoTile, UnpauseVideoTile, ZoomIn, ZoomOut, SetPriorityBasedPolicy, } type SetPriorityBasedPolicy = { type: VideoTileGridAction.SetPriorityBasedPolicy; payload: { policy: VideoPriorityBasedPolicy | undefined; }; } type UpdateVideoSources = { type: VideoTileGridAction.UpdateVideoSources; payload: { videoSources: VideoSource[]; localAttendeeId: string | null; }; }; type UpdateActiveSpeakers = { type: VideoTileGridAction.UpdateActiveSpeakers; payload: { activeSpeakers: string[]; }; }; type UpdateLayout = { type: VideoTileGridAction.UpdateLayout; payload: { layout: Layout; }; }; type UpdateAttendeeStates = { type: VideoTileGridAction.UpdateAttendeeStates; payload: { roster: RosterType; }; }; type UpdateLocalSourceState = { type: VideoTileGridAction.UpdateLocalSourceState; payload: { isVideoEnabled: boolean; localAttendeeId: string | null; isLocalUserSharing: boolean; sharingAttendeeId: string | null; }; }; type PauseVideoTile = { type: VideoTileGridAction.PauseVideoTile; payload: { attendeeId: string; }; }; type UnpauseVideoTile = { type: VideoTileGridAction.UnpauseVideoTile; payload: { attendeeId: string; }; }; type ZoomIn = { type: VideoTileGridAction.ZoomIn; // eslint-disable-next-line @typescript-eslint/no-explicit-any payload?: any; }; type ZoomOut = { type: VideoTileGridAction.ZoomOut; // eslint-disable-next-line @typescript-eslint/no-explicit-any payload?: any; }; export type Action = | SetPriorityBasedPolicy | UpdateVideoSources | UpdateLayout | UpdateAttendeeStates | UpdateLocalSourceState | UpdateActiveSpeakers | PauseVideoTile | UnpauseVideoTile | ZoomIn | ZoomOut; export function reducer(state: State, { type, payload }: Action): State { const { gridState, attendeeStates, videoSourceState, priorityBasedPolicy } = state; switch (type) { case VideoTileGridAction.UpdateAttendeeStates: { const { roster } = payload; // Remove attendee that left the meeting for (const attendeeId in attendeeStates) { if (!isContentShare(attendeeId) && !(attendeeId in roster)) { delete attendeeStates[attendeeId]; } } // Add attendee that joined the meeting for (const attendeeId in roster) { const name = roster[attendeeId]?.name || ''; if (attendeeId in attendeeStates) { attendeeStates[attendeeId].name = name; } else { attendeeStates[attendeeId] = { attendeeId, name, videoEnabled: false, bandwidthConstrained: false, } as AttendeeState; } } // Ensure the state of `videoEnabled` in the racing condition of UpdateAttendeeStates and UpdateVideoSources for (const attendeeId of videoSourceState.cameraSources) { if (attendeeId in attendeeStates) { attendeeStates[attendeeId].videoEnabled = true; } } return { ...state, attendeeStates, }; } case VideoTileGridAction.UpdateVideoSources: { const { videoSources, localAttendeeId } = payload as { videoSources: VideoSource[]; localAttendeeId: string | null }; const cameraSources: string[] = []; const videoSourceIdSet = new Set( videoSources.map(videoSource => videoSource.attendee.attendeeId) ); for (const attendeeId in attendeeStates) { // Reset the `videoEnabled` for all remote attendees if (!localAttendeeId || attendeeId !== localAttendeeId) { attendeeStates[attendeeId].videoEnabled = false; } // Remove content share from attendeeStates, // if content share is not in video sources if ( isContentShare(attendeeId) && !videoSourceIdSet.has(attendeeId) ) { delete attendeeStates[attendeeId]; } } // Update the `videoEnabled` according to `videoSources` for (const attendeeId of videoSourceIdSet) { if (!(attendeeId in attendeeStates)) { if (isContentShare(attendeeId)) { attendeeStates[attendeeId] = { attendeeId, name: 'content share', bandwidthConstrained: false, } as AttendeeState; } else { attendeeStates[attendeeId] = { attendeeId, name: '', bandwidthConstrained: false, } as AttendeeState; } } attendeeStates[attendeeId].videoEnabled = true; } // Populate the `cameraSources` based on the order of `attendeeStates` for (const attendeeId in attendeeStates) { const attendee = attendeeStates[attendeeId]; if ( attendee.videoEnabled && !isContentShare(attendee.attendeeId) && attendee.attendeeId !== localAttendeeId ) { cameraSources.push(attendee.attendeeId); } } // Remove the attendee from `activeSpeakersWithCameraSource`, // if this attendee close the camera while still active for (const attendeeId in videoSourceState.activeSpeakersWithCameraSource) { if (!attendeeStates[attendeeId]?.videoEnabled) { videoSourceState.activeSpeakersWithCameraSource.filter(id => id !== attendeeId); } } videoSourceState.cameraSources = cameraSources; updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); return { ...state, attendeeStates, videoSourceState, }; } case VideoTileGridAction.UpdateActiveSpeakers: { const { activeSpeakers } = payload; const activeSpeakersWithCameraSource = []; const { cameraSources } = videoSourceState; for (const attendeeId of activeSpeakers) { if ( attendeeStates[attendeeId]?.videoEnabled && cameraSources.includes(attendeeId) ) { activeSpeakersWithCameraSource.push(attendeeId); } } videoSourceState.activeSpeakersWithCameraSource = activeSpeakersWithCameraSource; updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); return { ...state, videoSourceState, }; } case VideoTileGridAction.UpdateLocalSourceState: { const { isVideoEnabled, localAttendeeId, isLocalUserSharing, sharingAttendeeId, } = payload; videoSourceState.hasLocalVideo = isVideoEnabled; videoSourceState.hasLocalContentSharing = isLocalUserSharing; videoSourceState.contentShareId = sharingAttendeeId; if (localAttendeeId && localAttendeeId in attendeeStates) { attendeeStates[localAttendeeId].videoEnabled = isVideoEnabled; } updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); return { ...state, attendeeStates, videoSourceState, }; } case VideoTileGridAction.UpdateLayout: { const { layout } = payload; gridState.layout = layout; updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); return { ...state, gridState, }; } case VideoTileGridAction.PauseVideoTile: { const { attendeeId } = payload; if (attendeeId in attendeeStates) { attendeeStates[attendeeId].bandwidthConstrained = true; } return { ...state, attendeeStates, }; } case VideoTileGridAction.UnpauseVideoTile: { const { attendeeId } = payload; if (attendeeId in attendeeStates) { attendeeStates[attendeeId].bandwidthConstrained = false; } return { ...state, attendeeStates, }; } case VideoTileGridAction.ZoomIn: { const { threshold } = gridState; const { cameraSources, hasLocalVideo } = videoSourceState; const numberOfTiles = cameraSources.length + (hasLocalVideo ? 1 : 0); if (numberOfTiles > threshold) { gridState.isZoomed = true; updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); } return { ...state, gridState, }; } case VideoTileGridAction.ZoomOut: { if (gridState.isZoomed) { gridState.isZoomed = false; updateDownlinkPreferences(gridState, videoSourceState, attendeeStates, priorityBasedPolicy); } return { ...state, }; } case VideoTileGridAction.SetPriorityBasedPolicy: { const { policy } = payload as { policy: VideoPriorityBasedPolicy | undefined}; return { ...state, ...{ priorityBasedPolicy: policy }, }; } default: throw new Error('Incorrect type in VideoTileGridProvider'); } }