import { CommunicationIdentifierKind } from '@azure/communication-common' ; import { AudioDeviceInfo , DeviceAccess , DominantSpeakersInfo , VideoDeviceInfo } from '@azure/communication-calling' ; import EventEmitter from 'events' ; import { enableMapSet , produce } from 'immer' ; import { CallEndReason , CallState as CallStatus , RemoteParticipantState as RemoteParticipantStatus } from '@azure/communication-calling' ; import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common' ; import { CallState , CallClientState , LocalVideoStreamState , RemoteParticipantState , RemoteVideoStreamState , IncomingCallState , VideoStreamRendererViewState , CallAgentState , CallErrors , CallErrorTarget , CallError } from './CallClientState' ; import { CallStateModifier } from './StatefulCallClient' ; enableMapSet ( ) ; private export const MAX_CALL_HISTORY_LENGTH = 10 ; private export class CallContext { private _state : CallClientState ; private _emitter : EventEmitter ; private _atomicId : number ; private _batchMode : boolean ; constructor ( userId , maxListeners = 50 ) { this . _state = { calls : { } , callsEnded : { } , incomingCalls : { } , incomingCallsEnded : { } , deviceManager : { isSpeakerSelectionAvailable : false , cameras : [ ] , microphones : [ ] , speakers : [ ] , unparentedViews : [ ] } , callAgent : undefined , userId : userId , latestErrors : { } as CallErrors } ; this . _emitter = new EventEmitter ( ) ; this . _emitter . setMaxListeners ( maxListeners ) ; this . _batchMode = false ; this . _atomicId = 0 ; } public getState ( ) { return this . _state ; } public setState ( state ) { this . _state = state ; if ( ! this . _batchMode ) { this . _emitter . emit ( 'stateChanged' , this . _state ) ; } } public modifyState ( modifier ) { this . batch ( ( ) => { this . setState ( produce ( this . _state , ( draft ) => { modifier ( draft ) ; } ) ) ; } ) ; } /**
     * Batch updates to minimize `stateChanged` events across related operations.
     *
     * - A maximum of one `stateChanged` event is emitted, at the end of the operations.
     * - No `stateChanged` event is emitted if the state did not change through the operations.
     * - In case of an exception, state is reset to the prior value and no `stateChanged` event is emitted.
     *
     * All operations finished in this batch should be synchronous.
     * This function is not reentrant -- do not call batch() from within another batch().
     */ public batch ( operations ) { if ( this . _batchMode ) { throw new Error ( 'batch() called from within another batch()' ) ; } this . _batchMode = true ; const priorState = this . _state ; try { operations ( ) ; if ( this . _state !== priorState ) { this . _emitter . emit ( 'stateChanged' , this . _state ) ; } } catch ( e ) { this . _state = priorState ; throw e ; } finally { this . _batchMode = false ; } } public onStateChange ( handler ) { this . _emitter . on ( 'stateChanged' , handler ) ; } public offStateChange ( handler ) { this . _emitter . off ( 'stateChanged' , handler ) ; } public clearCallRelatedState ( ) { this . setState ( produce ( this . _state , ( draft ) => { draft . calls = { } ; draft . incomingCalls = { } ; draft . callsEnded = { } ; draft . incomingCallsEnded = { } ; } ) ) ; } public setCallAgent ( callAgent ) { this . setState ( produce ( this . _state , ( draft ) => { draft . callAgent = callAgent ; } ) ) ; } public setCall ( call ) { this . setState ( produce ( this . _state , ( draft ) => { const existingCall = draft . calls [ call . id ] ; 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 . recording . isRecordingActive = call . recording . isRecordingActive ; } else { draft . calls [ call . id ] = call ; } } ) ) ; } public removeCall ( callId ) { this . setState ( produce ( this . _state , ( draft ) => { delete draft . calls [ callId ] ; } ) ) ; } public setCallEnded ( callId , callEndReason ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . endTime = new Date ( ) ; call . callEndReason = callEndReason ; delete draft . calls [ callId ] ; while ( Object . keys ( draft . callsEnded ) . length >= MAX_CALL_HISTORY_LENGTH ) { delete draft . callsEnded [ findOldestCallEnded ( draft . callsEnded ) ] ; } draft . callsEnded [ callId ] = call ; } } ) ) ; } public setCallState ( callId , state ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . state = state ; } } ) ) ; } public setCallId ( newCallId , oldCallId ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ oldCallId ] ; if ( call ) { delete draft . calls [ oldCallId ] ; draft . calls [ newCallId ] = call ; } } ) ) ; } public setCallIsScreenSharingOn ( callId , isScreenSharingOn ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . isScreenSharingOn = isScreenSharingOn ; } } ) ) ; } public setCallRemoteParticipants ( callId , addRemoteParticipant , removeRemoteParticipant ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { removeRemoteParticipant . forEach ( ( id ) => { delete call . remoteParticipants [ id ] ; } ) ; addRemoteParticipant . forEach ( ( participant ) => { call . remoteParticipants [ toFlatCommunicationIdentifier ( participant . identifier ) ] = participant ; } ) ; } } ) ) ; } public setCallRemoteParticipantsEnded ( callId , addRemoteParticipant , removeRemoteParticipant ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { removeRemoteParticipant . forEach ( ( id ) => { delete call . remoteParticipantsEnded [ id ] ; } ) ; addRemoteParticipant . forEach ( ( participant ) => { call . remoteParticipantsEnded [ toFlatCommunicationIdentifier ( participant . identifier ) ] = participant ; } ) ; } } ) ) ; } public setCallLocalVideoStream ( callId , streams ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . localVideoStreams = streams ; } } ) ) ; } public setCallIsMicrophoneMuted ( callId , isMicrophoneMuted ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . isMuted = isMicrophoneMuted ; } } ) ) ; } public setCallDominantSpeakers ( callId , dominantSpeakers ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . dominantSpeakers = dominantSpeakers ; } } ) ) ; } public setCallRecordingActive ( callId , isRecordingActive ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . recording . isRecordingActive = isRecordingActive ; } } ) ) ; } public setCallTranscriptionActive ( callId , isTranscriptionActive ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . transcription . isTranscriptionActive = isTranscriptionActive ; } } ) ) ; } public setCallScreenShareParticipant ( callId , participantKey ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { call . screenShareRemoteParticipant = participantKey ; } } ) ) ; } public setLocalVideoStreamRendererView ( callId , view ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { if ( call . localVideoStreams . length > 0 ) { call . localVideoStreams [ 0 ] . view = view ; } } } ) ) ; } public setParticipantState ( callId , participantKey , state ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { participant . state = state ; } } } ) ) ; } public setParticipantIsMuted ( callId , participantKey , muted ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { participant . isMuted = muted ; } } } ) ) ; } public setParticipantDisplayName ( callId , participantKey , displayName ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { participant . displayName = displayName ; } } } ) ) ; } public setParticipantIsSpeaking ( callId , participantKey , isSpeaking ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { participant . isSpeaking = isSpeaking ; } } } ) ) ; } public setParticipantVideoStream ( callId , participantKey , stream ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { const existingStream = participant . videoStreams [ stream . id ] ; if ( existingStream ) { existingStream . isAvailable = stream . isAvailable ; existingStream . mediaStreamType = stream . mediaStreamType ; } else { participant . videoStreams [ stream . id ] = stream ; } } } } ) ) ; } public setRemoteVideoStreamIsAvailable ( callId , participantKey , streamId , isAvailable ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { const stream = participant . videoStreams [ streamId ] ; if ( stream ) { stream . isAvailable = isAvailable ; } } } } ) ) ; } public setRemoteVideoStreams ( callId , participantKey , addRemoteVideoStream , removeRemoteVideoStream ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { for ( const id of removeRemoteVideoStream ) { delete participant . videoStreams [ id ] ; } for ( const newStream of addRemoteVideoStream ) { const stream = participant . videoStreams [ newStream . id ] ; if ( stream ) { stream . mediaStreamType = newStream . mediaStreamType ; stream . isAvailable = newStream . isAvailable ; } else { participant . videoStreams [ newStream . id ] = newStream ; } } } } } ) ) ; } public setRemoteVideoStreamRendererView ( callId , participantKey , streamId , view ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . calls [ callId ] ; if ( call ) { const participant = call . remoteParticipants [ participantKey ] ; if ( participant ) { const stream = participant . videoStreams [ streamId ] ; if ( stream ) { stream . view = view ; } } } } ) ) ; } public setIncomingCall ( call ) { this . setState ( produce ( this . _state , ( draft ) => { const existingCall = draft . incomingCalls [ call . id ] ; if ( existingCall ) { existingCall . callerInfo = call . callerInfo ; } else { draft . incomingCalls [ call . id ] = call ; } } ) ) ; } public removeIncomingCall ( callId ) { this . setState ( produce ( this . _state , ( draft ) => { delete draft . incomingCalls [ callId ] ; } ) ) ; } public setIncomingCallEnded ( callId , callEndReason ) { this . setState ( produce ( this . _state , ( draft ) => { const call = draft . incomingCalls [ callId ] ; if ( call ) { call . endTime = new Date ( ) ; call . callEndReason = callEndReason ; delete draft . incomingCalls [ callId ] ; while ( Object . keys ( draft . incomingCallsEnded ) . length >= MAX_CALL_HISTORY_LENGTH ) { delete draft . incomingCallsEnded [ findOldestCallEnded ( draft . incomingCallsEnded ) ] ; } draft . incomingCallsEnded [ callId ] = call ; } } ) ) ; } public setDeviceManagerIsSpeakerSelectionAvailable ( isSpeakerSelectionAvailable ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . isSpeakerSelectionAvailable = isSpeakerSelectionAvailable ; } ) ) ; } public setDeviceManagerSelectedMicrophone ( selectedMicrophone ? ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . selectedMicrophone = selectedMicrophone ; } ) ) ; } public setDeviceManagerSelectedSpeaker ( selectedSpeaker ? ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . selectedSpeaker = selectedSpeaker ; } ) ) ; } public setDeviceManagerSelectedCamera ( selectedCamera ? ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . selectedCamera = selectedCamera ; } ) ) ; } public setDeviceManagerCameras ( cameras ) { this . setState ( produce ( this . _state , ( 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 ) => camera . id === draft . deviceManager . selectedCamera ?. id ) ) { draft . deviceManager . selectedCamera = cameras [ 0 ] ; } draft . deviceManager . cameras = cameras ; } ) ) ; } public setDeviceManagerMicrophones ( microphones ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . microphones = microphones ; } ) ) ; } public setDeviceManagerSpeakers ( speakers ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . speakers = speakers ; } ) ) ; } public setDeviceManagerDeviceAccess ( deviceAccess ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . deviceAccess = deviceAccess ; } ) ) ; } public setDeviceManagerUnparentedView ( localVideoStream , view ) { this . setState ( produce ( this . _state , ( draft ) => { draft . deviceManager . unparentedViews . push ( { source : localVideoStream . source , mediaStreamType : localVideoStream . mediaStreamType , view : view } ) ; } ) ) ; } public deleteDeviceManagerUnparentedView ( localVideoStream ) { this . setState ( produce ( this . _state , ( draft ) => { const foundIndex = draft . deviceManager . unparentedViews . findIndex ( ( stream ) => stream . source . id === localVideoStream . source . id && stream . mediaStreamType === localVideoStream . mediaStreamType ) ; if ( foundIndex !== - 1 ) { draft . deviceManager . unparentedViews . splice ( foundIndex , 1 ) ; } } ) ) ; } public getAndIncrementAtomicId ( ) { const id = this . _atomicId ; this . _atomicId ++ ; return id ; } param action param target returns throws public withAsyncErrorTeedToState < Args extends unknown [ ] , R > ( action , target ) { return async ( ... args ) : Promise < R > => { try { return await action ( ... args ) ; } catch ( error ) { const callError = toCallError ( target , error ) ; this . setLatestError ( target , callError ) ; throw callError ; } } ; } param action param target returns throws public withErrorTeedToState < Args extends unknown [ ] , R > ( action , target ) { return ( ... args ) : R => { try { return action ( ... args ) ; } catch ( error ) { const callError = toCallError ( target , error ) ; this . setLatestError ( target , callError ) ; throw callError ; } } ; } private setLatestError ( target , error ) { this . setState ( produce ( this . _state , ( draft ) => { draft . latestErrors [ target ] = error ; } ) ) ; } } const toCallError = ( target , error ) : CallError => { if ( error instanceof Error ) { return new CallError ( target , error ) ; } return new CallError ( target , new Error ( error as string ) ) ; } ; const findOldestCallEnded = ( calls ) : string => { const callEntries = Object . entries ( calls ) ; let [ oldestCallId , oldestCall ] = callEntries [ 0 ] ; if ( oldestCall . endTime === undefined ) { return oldestCallId ; } for ( const [ callId , call ] of callEntries . slice ( 1 ) ) { if ( call . endTime === undefined ) { return callId ; } if ( ( call . endTime ?. getTime ( ) ?? 0 ) < ( oldestCall . endTime ?. getTime ( ) ?? 0 ) ) { [ oldestCallId , oldestCall ] = [ callId , call ] ; } } return oldestCallId ; } ;