UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

1,117 lines (1,116 loc) • 55.9 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { convertConferencePhoneInfo } from './Converter'; import { convertFromSDKRealTimeTextToRealTimeTextInfoState } from './Converter'; import { createClientLogger, getLogLevel } from '@azure/logger'; import { EventEmitter } from 'events'; import { enableMapSet, enablePatches, produce } from 'immer'; import { _safeJSONStringify, toFlatCommunicationIdentifier } from "../../acs-ui-common/src"; import { CallError } from './CallClientState'; import { callingStatefulLogger } from './Logger'; import { CallIdHistory } from './CallIdHistory'; import { convertFromTeamsSDKToCaptionInfoState } from './Converter'; import { convertFromSDKToCaptionInfoState } from './Converter'; import { convertFromSDKToRaisedHandState } from './Converter'; enableMapSet(); // Needed to generate state diff for verbose logging. enablePatches(); // TODO: How can we make this configurable? /** * @private */ export const MAX_CALL_HISTORY_LENGTH = 10; /** * @private */ export const REACTION_ANIMATION_TIME_MS = 4133; /** * @private */ export class CallContext { constructor(userId, maxListeners = 50) { this._callIdHistory = new CallIdHistory(); this._timeOutId = {}; this._latestCallIdOfNotification = {}; /** * Tees direct errors to state. * @remarks * This is typically used for errors that are caught and intended to be shown to the user. * * @param error The raw error to report. * @param target The error target to tee error to. * * @private */ this.teeErrorToState = (error, target) => { const callError = toCallError(target, error); this.setLatestError(target, callError); }; this._logger = createClientLogger('communication-react:calling-context'); this._state = { calls: {}, callsEnded: {}, incomingCalls: {}, incomingCallsEnded: {}, deviceManager: { isSpeakerSelectionAvailable: false, cameras: [], microphones: [], speakers: [], unparentedViews: [] }, callAgent: undefined, userId: userId, environmentInfo: undefined, latestErrors: {}, latestNotifications: {} }; this._emitter = new EventEmitter(); this._emitter.setMaxListeners(maxListeners); this._atomicId = 0; } getState() { return this._state; } modifyState(modifier) { const priorState = this._state; this._state = produce(this._state, modifier, (patches) => { if (getLogLevel() === 'verbose') { // Log to `info` because AzureLogger.verbose() doesn't show up in console. this._logger.info(`State change: ${_safeJSONStringify(patches)}`); } }); if (this._state !== priorState) { this._emitter.emit('stateChanged', this._state); } } onStateChange(handler) { this._emitter.on('stateChanged', handler); } offStateChange(handler) { this._emitter.off('stateChanged', handler); } // Disposing of the CallAgentDeclarative will not clear the state. If we create a new CallAgentDeclarative, we should // make sure the state is clean because any left over state (if previous CallAgentDeclarative was disposed) may be // invalid. clearCallRelatedState() { this.modifyState((draft) => { draft.calls = {}; draft.incomingCalls = {}; draft.callsEnded = {}; draft.incomingCallsEnded = {}; }); } setCallAgent(callAgent) { this.modifyState((draft) => { draft.callAgent = callAgent; }); } setCall(call) { this.modifyState((draft) => { const latestCallId = this._callIdHistory.latestCallId(call.id); const existingCall = draft.calls[latestCallId]; if (existingCall) { existingCall.callerInfo = call.callerInfo; existingCall.state = call.state; existingCall.callEndReason = call.callEndReason; existingCall.direction = call.direction; existingCall.isMuted = call.isMuted; existingCall.isScreenSharingOn = call.isScreenSharingOn; existingCall.localVideoStreams = call.localVideoStreams; existingCall.remoteParticipants = call.remoteParticipants; existingCall.transcription.isTranscriptionActive = call.transcription.isTranscriptionActive; existingCall.optimalVideoCount.maxRemoteVideoStreams = call.optimalVideoCount.maxRemoteVideoStreams; existingCall.recording.isRecordingActive = call.recording.isRecordingActive; existingCall.raiseHand.raisedHands = call.raiseHand.raisedHands; existingCall.pptLive.isActive = call.pptLive.isActive; existingCall.raiseHand.localParticipantRaisedHand = call.raiseHand.localParticipantRaisedHand; existingCall.role = call.role; // We don't update the startTime and endTime if we are updating an existing active call existingCall.captionsFeature.currentSpokenLanguage = call.captionsFeature.currentSpokenLanguage; existingCall.captionsFeature.currentCaptionLanguage = call.captionsFeature.currentCaptionLanguage; existingCall.info = call.info; existingCall.meetingConference = call.meetingConference; } else { draft.calls[latestCallId] = call; } }); /** * Remove the incoming call that matches the call if there is one */ if (this._state.incomingCalls[call.id]) { this.removeIncomingCall(call.id); } } removeCall(callId) { this.modifyState((draft) => { delete draft.calls[this._callIdHistory.latestCallId(callId)]; }); } setCallEnded(callId, callEndReason) { const latestCallId = this._callIdHistory.latestCallId(callId); this.modifyState((draft) => { const call = draft.calls[latestCallId]; if (call) { call.endTime = new Date(); call.callEndReason = callEndReason; delete draft.calls[latestCallId]; // Performance note: This loop should run only once because the number of entries // is never allowed to exceed MAX_CALL_HISTORY_LENGTH. A loop is used for correctness. while (Object.keys(draft.callsEnded).length >= MAX_CALL_HISTORY_LENGTH) { const oldestCall = findOldestCallEnded(draft.callsEnded); if (oldestCall) { delete draft.callsEnded[oldestCall]; } } draft.callsEnded[latestCallId] = call; } }); } setCallState(callId, state) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.state = state; } }); } setCallId(newCallId, oldCallId) { this._callIdHistory.updateCallIdHistory(newCallId, oldCallId); this.modifyState((draft) => { const call = draft.calls[oldCallId]; if (call) { call.id = newCallId; delete draft.calls[oldCallId]; draft.calls[newCallId] = call; } // Update call ids in latestCallIdsThatPushedNotifications Object.keys(this._latestCallIdOfNotification).forEach((key) => { if (this._latestCallIdOfNotification[key] === oldCallId) { this._latestCallIdOfNotification[key] = newCallId; } }); // Update the open breakout room call id if it matches the old call id if (this._openBreakoutRoom === oldCallId) { this._openBreakoutRoom = newCallId; } }); } setEnvironmentInfo(envInfo) { this.modifyState((draft) => { draft.environmentInfo = envInfo; }); } setCallIsScreenSharingOn(callId, isScreenSharingOn) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.isScreenSharingOn = isScreenSharingOn; } }); } setCallRemoteParticipants(callId, addRemoteParticipant, removeRemoteParticipant) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { removeRemoteParticipant.forEach((id) => { delete call.remoteParticipants[id]; }); addRemoteParticipant.forEach((participant) => { call.remoteParticipants[toFlatCommunicationIdentifier(participant.identifier)] = participant; }); } }); } setCallRemoteParticipantsEnded(callId, addRemoteParticipant, removeRemoteParticipant) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { removeRemoteParticipant.forEach((id) => { delete call.remoteParticipantsEnded[id]; }); addRemoteParticipant.forEach((participant) => { call.remoteParticipantsEnded[toFlatCommunicationIdentifier(participant.identifier)] = participant; }); } }); } setCallLocalVideoStream(callId, streamsAdded, streamsRemoved) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { for (const removedStream of streamsRemoved) { const index = call.localVideoStreams.findIndex(i => i.mediaStreamType === removedStream.mediaStreamType); if (index > -1) { call.localVideoStreams.splice(index, 1); } } for (const addedStream of streamsAdded) { const index = call.localVideoStreams.findIndex(i => i.mediaStreamType === addedStream.mediaStreamType); if (index > -1) { call.localVideoStreams[index] = addedStream; } else { call.localVideoStreams.push(addedStream); } } } }); } setCallLocalVideoStreamVideoEffects(callId, videoEffects) { this.modifyState((draft) => { var _a; const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const stream = (_a = call.localVideoStreams) === null || _a === void 0 ? void 0 : _a.find(i => i.mediaStreamType === 'Video'); if (stream) { stream.videoEffects = videoEffects; } } }); } setCallIsMicrophoneMuted(callId, isMicrophoneMuted) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.isMuted = isMicrophoneMuted; } }); } setRole(callId, role) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.role = role; } }); } setCallDominantSpeakers(callId, dominantSpeakers) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.dominantSpeakers = dominantSpeakers; } }); } setCallRecordingActive(callId, isRecordingActive) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.recording.isRecordingActive = isRecordingActive; } }); } setCallPPTLiveActive(callId, isActive) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.pptLive.isActive = isActive; } }); } setCallParticipantPPTLive(callId, participantKey, target) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call && participantKey) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.contentSharingStream = target; call.contentSharingRemoteParticipant = participantKey; } } }); } setTogetherModeVideoStreams(callId, addedStreams, removedStreams) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { for (const stream of removedStreams) { if (stream.mediaStreamType === 'Video') { call.togetherMode.streams.mainVideoStream = undefined; call.togetherMode.isActive = false; call.togetherMode.seatingPositions = {}; } } for (const newStream of addedStreams) { // This should only be called by the subscriber and some properties are add by other components so if the // stream already exists, only update the values that subscriber knows about. const mainVideoStream = call.togetherMode.streams.mainVideoStream; if (mainVideoStream && mainVideoStream.id === newStream.id) { mainVideoStream.mediaStreamType = newStream.mediaStreamType; mainVideoStream.isAvailable = newStream.isAvailable; mainVideoStream.isReceiving = newStream.isReceiving; } else { call.togetherMode.streams.mainVideoStream = newStream; } call.togetherMode.isActive = true; } } }); } setTogetherModeVideoStreamIsAvailable(callId, streamId, isAvailable) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const stream = call.togetherMode.streams.mainVideoStream; if (stream && (stream === null || stream === void 0 ? void 0 : stream.id) === streamId) { stream.isAvailable = isAvailable; } } }); } setTogetherModeVideoStreamIsReceiving(callId, streamId, isReceiving) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const stream = call.togetherMode.streams.mainVideoStream; if (stream && (stream === null || stream === void 0 ? void 0 : stream.id) === streamId) { stream.isReceiving = isReceiving; } } }); } setTogetherModeVideoStreamSize(callId, streamId, size) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const stream = call.togetherMode.streams.mainVideoStream; if (stream && (stream === null || stream === void 0 ? void 0 : stream.id) === streamId) { stream.streamSize = size; } } }); } removeTogetherModeVideoStream(callId, removedStream) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { for (const stream of removedStream) { if (stream.mediaStreamType === 'Video') { call.togetherMode.streams.mainVideoStream = undefined; call.togetherMode.isActive = false; } } } }); } setTogetherModeSeatingCoordinates(callId, seatingMap) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const seatingPositions = {}; for (const [userId, seatingPosition] of seatingMap.entries()) { seatingPositions[userId] = seatingPosition; } call.togetherMode.seatingPositions = seatingPositions; } }); } setCallRaisedHands(callId, raisedHands) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.raiseHand.raisedHands = raisedHands.map(raisedHand => { return convertFromSDKToRaisedHandState(raisedHand); }); const raisedHand = raisedHands.find(raisedHand => toFlatCommunicationIdentifier(raisedHand.identifier) === toFlatCommunicationIdentifier(this._state.userId)); if (raisedHand) { call.raiseHand.localParticipantRaisedHand = convertFromSDKToRaisedHandState(raisedHand); } else { call.raiseHand.localParticipantRaisedHand = undefined; } } }); } setParticipantIsRaisedHand(callId, participantKey, raisedHand) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.raisedHand = raisedHand ? convertFromSDKToRaisedHandState(raisedHand) : raisedHand; } } }); } setReceivedReactionFromParticipant(callId, participantKey, reactionMessage) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (!call) { return; } clearTimeout(this._timeOutId[participantKey]); const participant = call.remoteParticipants[participantKey]; const newReactionState = reactionMessage ? { reactionMessage: reactionMessage, receivedOn: new Date() } : undefined; if (participantKey === toFlatCommunicationIdentifier(this._state.userId)) { call.localParticipantReaction = newReactionState; } else if (!participant) { // Warn if we can't find the participant in the call but we are trying to set their reaction state to a new reaction. if (reactionMessage !== null) { console.warn(`Participant ${participantKey} not found in call ${callId}. Cannot set reaction state.`); } } else { participant.reactionState = newReactionState; } if (reactionMessage) { this._timeOutId[participantKey] = setTimeout(() => { clearParticipantReactionState(this, callId, participantKey); }, REACTION_ANIMATION_TIME_MS); } }); } setCallTranscriptionActive(callId, isTranscriptionActive) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.transcription.isTranscriptionActive = isTranscriptionActive; } }); } setCapabilities(callId, capabilities, capabilitiesChangeInfo) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.capabilitiesFeature = { capabilities, latestCapabilitiesChangeInfo: capabilitiesChangeInfo }; } }); } setHideAttendeeNames(callId, capabilitiesChangeInfo) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (capabilitiesChangeInfo.oldValue.viewAttendeeNames !== capabilitiesChangeInfo.newValue.viewAttendeeNames) { const viewAttendeeNames = capabilitiesChangeInfo.newValue.viewAttendeeNames; if (call && viewAttendeeNames && !viewAttendeeNames.isPresent && viewAttendeeNames.reason === 'MeetingRestricted') { call.hideAttendeeNames = true; } else if (call) { call.hideAttendeeNames = false; } } }); } setSpotlight(callId, spotlightedParticipants, maxParticipantsToSpotlight) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.spotlight = Object.assign(Object.assign({}, call.spotlight), { spotlightedParticipants, maxParticipantsToSpotlight }); } }); } setTeamsMeetingConference(callId, teamsMeetingConferenceDetails) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.meetingConference = { conferencePhones: convertConferencePhoneInfo(teamsMeetingConferenceDetails) }; } }); } setParticipantSpotlighted(callId, spotlightedParticipant) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[toFlatCommunicationIdentifier(spotlightedParticipant.identifier)]; if (participant) { participant.spotlight = { spotlightedOrderPosition: spotlightedParticipant.order }; } else if (call.spotlight && toFlatCommunicationIdentifier(draft.userId) === toFlatCommunicationIdentifier(spotlightedParticipant.identifier)) { call.spotlight.localParticipantSpotlight = { spotlightedOrderPosition: spotlightedParticipant.order }; } } }); } setParticipantNotSpotlighted(callId, spotlightedParticipant) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[toFlatCommunicationIdentifier(spotlightedParticipant.identifier)]; if (participant) { participant.spotlight = undefined; } else if (call.spotlight && toFlatCommunicationIdentifier(draft.userId) === toFlatCommunicationIdentifier(spotlightedParticipant.identifier)) { call.spotlight.localParticipantSpotlight = undefined; } } }); } setAssignedBreakoutRoom(callId, breakoutRoom) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.breakoutRooms = Object.assign(Object.assign({}, call.breakoutRooms), { assignedBreakoutRoom: breakoutRoom }); } }); } setBreakoutRoomSettings(callId, breakoutRoomSettings) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.breakoutRooms = Object.assign(Object.assign({}, call.breakoutRooms), { breakoutRoomSettings: breakoutRoomSettings }); } }); } setBreakoutRoomDisplayName(callId, breakoutRoomDisplayName) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.breakoutRooms = Object.assign(Object.assign({}, call.breakoutRooms), { breakoutRoomDisplayName }); } }); } setOpenBreakoutRoom(callId) { this._openBreakoutRoom = callId; } deleteOpenBreakoutRoom() { this._openBreakoutRoom = undefined; } getOpenBreakoutRoom() { return this._openBreakoutRoom; } setCallScreenShareParticipant(callId, participantKey) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.screenShareRemoteParticipant = participantKey; } }); } setLocalVideoStreamRendererView(callId, localVideoMediaStreamType, view) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const localVideoStream = call.localVideoStreams.find(localVideoStream => localVideoStream.mediaStreamType === localVideoMediaStreamType); if (localVideoStream) { localVideoStream.view = view; } } }); } setTogetherModeVideoStreamRendererView(callId, togetherModeStreamType, view) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { if (togetherModeStreamType === 'Video') { const togetherModeStream = call.togetherMode.streams.mainVideoStream; if (togetherModeStream) { togetherModeStream.view = view; } } } }); } setParticipantState(callId, participantKey, state) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.state = state; } } }); } setParticipantIsMuted(callId, participantKey, muted) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.isMuted = muted; } } }); } setOptimalVideoCount(callId, optimalVideoCount) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.optimalVideoCount.maxRemoteVideoStreams = optimalVideoCount; } }); } setParticipantRole(callId, participantKey, role) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.role = role; } } }); } setParticipantDisplayName(callId, participantKey, displayName) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.displayName = displayName; } } }); } setParticipantIsSpeaking(callId, participantKey, isSpeaking) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { participant.isSpeaking = isSpeaking; } } }); } setParticipantVideoStream(callId, participantKey, stream) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { // Set is called by subscriber will not modify any rendered stream so if there is existing stream only // modify the values that subscriber has access to. const existingStream = participant.videoStreams[stream.id]; if (existingStream) { existingStream.isAvailable = stream.isAvailable; existingStream.isReceiving = stream.isReceiving; existingStream.mediaStreamType = stream.mediaStreamType; } else { participant.videoStreams[stream.id] = stream; } } } }); } setRemoteVideoStreamIsAvailable(callId, participantKey, streamId, isAvailable) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { const stream = participant.videoStreams[streamId]; if (stream) { stream.isAvailable = isAvailable; } } } }); } setRemoteVideoStreamIsReceiving(callId, participantKey, streamId, isReceiving) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { const stream = participant.videoStreams[streamId]; if (stream) { stream.isReceiving = isReceiving; } } } }); } setRemoteVideoStreamSize(callId, participantKey, streamId, size) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { const stream = participant.videoStreams[streamId]; if (stream) { stream.streamSize = size; } } } }); } setRemoteVideoStreams(callId, participantKey, addRemoteVideoStream, removeRemoteVideoStream) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { for (const id of removeRemoteVideoStream) { delete participant.videoStreams[id]; } for (const newStream of addRemoteVideoStream) { // This should only be called by the subscriber and some properties are add by other components so if the // stream already exists, only update the values that subscriber knows about. const stream = participant.videoStreams[newStream.id]; if (stream) { stream.mediaStreamType = newStream.mediaStreamType; stream.isAvailable = newStream.isAvailable; stream.isReceiving = newStream.isReceiving; } else { participant.videoStreams[newStream.id] = newStream; } } } } }); } setRemoteVideoStreamRendererView(callId, participantKey, streamId, view) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { const stream = participant.videoStreams[streamId]; if (stream) { stream.view = view; } } } }); } setRemoteVideoStreamViewScalingMode(callId, participantKey, streamId, scalingMode) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const participant = call.remoteParticipants[participantKey]; if (participant) { const stream = participant.videoStreams[streamId]; if (stream && stream.view) { stream.view.scalingMode = scalingMode; } } } }); } setIncomingCall(call) { this.modifyState((draft) => { const existingCall = draft.incomingCalls[call.id]; if (existingCall) { existingCall.callerInfo = call.callerInfo; } else { draft.incomingCalls[call.id] = call; } }); } removeIncomingCall(callId) { this.modifyState((draft) => { delete draft.incomingCalls[callId]; }); } setIncomingCallEnded(callId, callEndReason) { this.modifyState((draft) => { const call = draft.incomingCalls[callId]; if (call) { call.endTime = new Date(); call.callEndReason = callEndReason; delete draft.incomingCalls[callId]; // Performance note: This loop should run only once because the number of entries // is never allowed to exceed MAX_CALL_HISTORY_LENGTH. A loop is used for correctness. while (Object.keys(draft.incomingCallsEnded).length >= MAX_CALL_HISTORY_LENGTH) { const oldestCall = findOldestCallEnded(draft.incomingCallsEnded); if (oldestCall) { delete draft.incomingCallsEnded[oldestCall]; } } draft.incomingCallsEnded[callId] = call; } }); } setDeviceManagerIsSpeakerSelectionAvailable(isSpeakerSelectionAvailable) { this.modifyState((draft) => { draft.deviceManager.isSpeakerSelectionAvailable = isSpeakerSelectionAvailable; }); } setDeviceManagerSelectedMicrophone(selectedMicrophone) { this.modifyState((draft) => { draft.deviceManager.selectedMicrophone = selectedMicrophone; }); } setDeviceManagerSelectedSpeaker(selectedSpeaker) { this.modifyState((draft) => { draft.deviceManager.selectedSpeaker = selectedSpeaker; }); } setDeviceManagerSelectedCamera(selectedCamera) { this.modifyState((draft) => { draft.deviceManager.selectedCamera = selectedCamera; }); } setDeviceManagerCameras(cameras) { this.modifyState((draft) => { /** * SDK initializes cameras with one dummy camera with value { id: 'camera:id', name: '', deviceType: 'USBCamera' } immediately after * camera permissions are granted. So selectedCamera will have this value before the actual cameras are obtained. Therefore we should reset * selectedCamera to the first camera when there are cameras AND when current selectedCamera does not exist in the new array of cameras * */ if (cameras.length > 0 && !cameras.some(camera => { var _a; return camera.id === ((_a = draft.deviceManager.selectedCamera) === null || _a === void 0 ? void 0 : _a.id); })) { draft.deviceManager.selectedCamera = cameras[0]; } draft.deviceManager.cameras = cameras; }); } setDeviceManagerMicrophones(microphones) { this.modifyState((draft) => { draft.deviceManager.microphones = microphones; }); } setDeviceManagerSpeakers(speakers) { this.modifyState((draft) => { draft.deviceManager.speakers = speakers; }); } setDeviceManagerDeviceAccess(deviceAccess) { this.modifyState((draft) => { draft.deviceManager.deviceAccess = deviceAccess; }); } setDeviceManagerUnparentedView(localVideoStream, view) { this.modifyState((draft) => { draft.deviceManager.unparentedViews.push({ source: localVideoStream.source, mediaStreamType: localVideoStream.mediaStreamType, view: view }); }); } deleteDeviceManagerUnparentedView(localVideoStream) { this.modifyState((draft) => { const foundIndex = draft.deviceManager.unparentedViews.findIndex(stream => stream.mediaStreamType === localVideoStream.mediaStreamType); if (foundIndex !== -1) { draft.deviceManager.unparentedViews.splice(foundIndex, 1); } }); } setDeviceManagerUnparentedViewVideoEffects(localVideoStream, videoEffects) { this.modifyState((draft) => { const view = draft.deviceManager.unparentedViews.find(stream => stream.mediaStreamType === localVideoStream.mediaStreamType); if (view) { view.videoEffects = videoEffects; } }); } getAndIncrementAtomicId() { const id = this._atomicId; this._atomicId++; return id; } processNewRealTimeText(realTimeTexts, newRealTimeText) { var _a, _b, _c, _d, _e; // if the new message is final, push it to completed messages if (newRealTimeText.resultType === 'Final') { if (!realTimeTexts.completedMessages) { realTimeTexts.completedMessages = []; } (_a = realTimeTexts.completedMessages) === null || _a === void 0 ? void 0 : _a.push(newRealTimeText); // clear the corresponding in progress message if (newRealTimeText.isMe) { realTimeTexts.myInProgress = undefined; } else { realTimeTexts.currentInProgress = (_b = realTimeTexts.currentInProgress) === null || _b === void 0 ? void 0 : _b.filter(message => message.sequenceId !== newRealTimeText.sequenceId); } } else { // if receive partial real time text // if message is from me, assign it to myInProgress if (newRealTimeText.isMe) { realTimeTexts.myInProgress = newRealTimeText; } // if message is from others, assign it to currentInProgress else { if (!realTimeTexts.currentInProgress) { realTimeTexts.currentInProgress = []; realTimeTexts.currentInProgress.push(newRealTimeText); return; } // find the index of the existing in progress message // replace the existing in progress message with the new one const existingIndex = (_c = realTimeTexts.currentInProgress) === null || _c === void 0 ? void 0 : _c.findIndex(message => message.sequenceId === newRealTimeText.sequenceId); if (existingIndex === -1) { (_d = realTimeTexts.currentInProgress) === null || _d === void 0 ? void 0 : _d.push(newRealTimeText); } else { realTimeTexts.currentInProgress[existingIndex] = newRealTimeText; } } } // edge case check for in progress messages time out if (!realTimeTexts.completedMessages) { realTimeTexts.completedMessages = []; } this.findTimeoutRealTimeText(realTimeTexts.currentInProgress, realTimeTexts.completedMessages); this.findTimeoutRealTimeText(realTimeTexts.myInProgress, realTimeTexts.completedMessages); // we only want to store up to 50 finalized messages if (realTimeTexts.completedMessages && ((_e = realTimeTexts.completedMessages) === null || _e === void 0 ? void 0 : _e.length) > 50) { realTimeTexts.completedMessages.shift(); } } findTimeoutRealTimeText(inProgressRealTimeTexts, completedRealTimeTexts) { // if inProgressRealTimeTexts is an array if (inProgressRealTimeTexts && Array.isArray(inProgressRealTimeTexts)) { // find the in progress real time text that has not been updated for 5 seconds for (let i = inProgressRealTimeTexts.length - 1; i >= 0; i--) { const realTimeText = inProgressRealTimeTexts[i]; if ((realTimeText === null || realTimeText === void 0 ? void 0 : realTimeText.updatedTimestamp) && Date.now() - (realTimeText === null || realTimeText === void 0 ? void 0 : realTimeText.updatedTimestamp.getTime()) > 5000) { // turn the in progress real time text to final realTimeText.resultType = 'Final'; // move the in progress real time text to completed completedRealTimeTexts.push(realTimeText); // remove the in progress real time text from in progress if (inProgressRealTimeTexts) { inProgressRealTimeTexts.splice(i, 1); } } } } else { // if inProgressRealTimeTexts is a single object if (inProgressRealTimeTexts && inProgressRealTimeTexts.updatedTimestamp && Date.now() - inProgressRealTimeTexts.updatedTimestamp.getTime() > 5000) { // turn the in progress real time text to final inProgressRealTimeTexts.resultType = 'Final'; // move the in progress real time text to completed completedRealTimeTexts.push(inProgressRealTimeTexts); // remove the in progress real time text from in progress inProgressRealTimeTexts = undefined; } } } processNewCaption(captions, newCaption) { var _a; // time stamp when new caption comes in newCaption.lastUpdatedTimestamp = new Date(); // if this is the first caption, push it in if (captions.length === 0) { captions.push(newCaption); } // if the last caption is final, then push the new one in else if (((_a = captions[captions.length - 1]) === null || _a === void 0 ? void 0 : _a.resultType) === 'Final') { captions.push(newCaption); } // if the last caption is Partial, then check if the speaker is the same as the new caption, if so, update the last caption else { const lastCaption = captions[captions.length - 1]; if (lastCaption && lastCaption.speaker.identifier && newCaption.speaker.identifier && toFlatCommunicationIdentifier(lastCaption.speaker.identifier) === toFlatCommunicationIdentifier(newCaption.speaker.identifier)) { captions[captions.length - 1] = newCaption; } // if different speaker, ignore the interjector until the current speaker finishes // edge case: if we dont receive the final caption from the current speaker for 5 secs, we turn the current speaker caption to final and push in the new interjector else if (lastCaption === null || lastCaption === void 0 ? void 0 : lastCaption.lastUpdatedTimestamp) { if (Date.now() - lastCaption.lastUpdatedTimestamp.getTime() > 5000) { lastCaption.resultType = 'Final'; captions.push(newCaption); } } } // If the array length exceeds 50, remove the oldest caption if (captions.length > 50) { captions.shift(); } } addTeamsCaption(callId, caption) { this.modifyState((draft) => { var _a; const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { const currentCaptionLanguage = call.captionsFeature.currentCaptionLanguage; if (caption.captionLanguage.toUpperCase() === currentCaptionLanguage.toUpperCase() || currentCaptionLanguage === '' || currentCaptionLanguage === undefined) { this.processNewCaption((_a = call === null || call === void 0 ? void 0 : call.captionsFeature.captions) !== null && _a !== void 0 ? _a : [], convertFromTeamsSDKToCaptionInfoState(caption)); } } }); } addCaption(callId, caption) { this.modifyState((draft) => { var _a; const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { this.processNewCaption((_a = call === null || call === void 0 ? void 0 : call.captionsFeature.captions) !== null && _a !== void 0 ? _a : [], convertFromSDKToCaptionInfoState(caption)); } }); } addRealTimeText(callId, realTimeText) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { this.processNewRealTimeText(call.realTimeTextFeature.realTimeTexts, convertFromSDKRealTimeTextToRealTimeTextInfoState(realTimeText)); } }); } setCaptionsKind(callId, kind) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.captionsKind = kind; } }); } clearCaptions(callId) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.captions = call.captionsFeature.captions.filter(c => 'message' in c); } }); } setIsCaptionActive(callId, isCaptionsActive) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.isCaptionsFeatureActive = isCaptionsActive; } }); } setIsRealTimeTextActive(callId, isRealTimeTextActive) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.realTimeTextFeature.isRealTimeTextFeatureActive = isRealTimeTextActive; } }); } setStartCaptionsInProgress(callId, startCaptionsInProgress) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.startCaptionsInProgress = startCaptionsInProgress; } }); } setSelectedSpokenLanguage(callId, spokenLanguage) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.currentSpokenLanguage = spokenLanguage; } }); } setSelectedCaptionLanguage(callId, captionLanguage) { this.modifyState((draft) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { call.captionsFeature.currentCaptionLanguage = captionLanguage; }