@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
JavaScript
// 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;
}