import { CallingHandlers , createDefaultCallingHandlers , _isInCall , _isInLobbyOrConnecting } from '@internal/calling-component-bindings' ; import { CallClientState , CallError , CallState , createStatefulCallClient , StatefulCallClient , StatefulDeviceManager } from '@internal/calling-stateful-client' ; import { AudioOptions , CallAgent , Call , GroupCallLocator , TeamsMeetingLinkLocator , LocalVideoStream as SDKLocalVideoStream , AudioDeviceInfo , VideoDeviceInfo , RemoteParticipant , PermissionConstraints } from '@azure/communication-calling' ; import { EventEmitter } from 'events' ; import { CallAdapter , CallEndedListener , CallIdChangedListener , CallAdapterState , DisplayNameChangedListener , IsMutedChangedListener , IsLocalScreenSharingActiveChangedListener , IsSpeakingChangedListener , ParticipantsJoinedListener , ParticipantsLeftListener , DiagnosticChangedEventListner } from './CallAdapter' ; import { getCallCompositePage , isCameraOn } from '../utils' ; import { VideoStreamOptions } from '@internal/react-components' ; import { fromFlatCommunicationIdentifier , toFlatCommunicationIdentifier } from '@internal/acs-ui-common' ; import { CommunicationTokenCredential , CommunicationUserIdentifier } from '@azure/communication-common' ; import { ParticipantSubscriber } from './ParticipantSubcriber' ; import { AdapterError } from '../../common/adapters' ; import { DiagnosticsForwarder } from './DiagnosticsForwarder' ; /** Context of call, which is a centralized context for all state updates */ class CallContext { private emitter : EventEmitter = new EventEmitter ( ) ; private state : CallAdapterState ; private callId : string | undefined ; constructor ( clientState , isTeamsCall ) { this . state = { isLocalPreviewMicrophoneEnabled : false , userId : clientState . userId , displayName : clientState . callAgent ?. displayName , devices : clientState . deviceManager , call : undefined , page : 'configuration' , latestErrors : clientState . latestErrors , isTeamsCall } ; } public onStateChange ( handler ) { this . emitter . on ( 'stateChanged' , handler ) ; } public offStateChange ( handler ) { this . emitter . off ( 'stateChanged' , handler ) ; } public setState ( state ) { this . state = state ; this . emitter . emit ( 'stateChanged' , this . state ) ; } public getState ( ) { return this . state ; } public setIsLocalMicrophoneEnabled ( isLocalPreviewMicrophoneEnabled ) { this . setState ( { ... this . state , isLocalPreviewMicrophoneEnabled } ) ; } public setCallId ( callId ) { this . callId = callId ; } public updateClientState ( clientState ) { const call = this . callId ? clientState . calls [ this . callId ] : undefined ; const latestEndedCall = findLatestEndedCall ( clientState . callsEnded ) ; this . setState ( { ... this . state , userId : clientState . userId , displayName : clientState . callAgent ?. displayName , call , page : getCallCompositePage ( call , latestEndedCall ) , endedCall : latestEndedCall , devices : clientState . deviceManager , latestErrors : clientState . latestErrors } ) ; } } const findLatestEndedCall = ( calls ) : CallState | undefined => { const callStates = Object . values ( calls ) ; if ( callStates . length === 0 ) { return undefined ; } let latestCall = callStates [ 0 ] ; for ( const call of callStates . slice ( 1 ) ) { if ( ( call . endTime ?. getTime ( ) ?? 0 ) > ( latestCall . endTime ?. getTime ( ) ?? 0 ) ) { latestCall = call ; } } return latestCall ; } ; private export class AzureCommunicationCallAdapter implements CallAdapter { private callClient : StatefulCallClient ; private callAgent : CallAgent ; private deviceManager : StatefulDeviceManager ; private localStream : SDKLocalVideoStream | undefined ; private locator : TeamsMeetingLinkLocator | GroupCallLocator ; private _call ? : Call ; private context : CallContext ; private diagnosticsForwarder ? : DiagnosticsForwarder ; private handlers : CallingHandlers ; private participantSubscribers = new Map < string , ParticipantSubscriber > ( ) ; private emitter : EventEmitter = new EventEmitter ( ) ; private onClientStateChange : ( clientState : CallClientState ) => void ; private get call ( ) : Call | undefined { return this . _call ; } private set call ( newCall ) { this . resetDiagnosticsForwarder ( newCall ) ; this . _call = newCall ; } constructor ( callClient , locator , callAgent , deviceManager ) { this . bindPublicMethods ( ) ; this . callClient = callClient ; this . callAgent = callAgent ; this . locator = locator ; this . deviceManager = deviceManager ; const isTeamsMeeting = 'meetingLink' in this . locator ; this . context = new CallContext ( callClient . getState ( ) , isTeamsMeeting ) ; const onStateChange = ( clientState ) : void => { if ( ! this ) { callClient . offStateChange ( onStateChange ) ; return ; } this . context . updateClientState ( clientState ) ; } ; this . handlers = createDefaultCallingHandlers ( callClient , callAgent , deviceManager , undefined ) ; this . onClientStateChange = onStateChange ; this . callClient . onStateChange ( onStateChange ) ; } private bindPublicMethods ( ) { this . onStateChange . bind ( this ) ; this . offStateChange . bind ( this ) ; this . getState . bind ( this ) ; this . dispose . bind ( this ) ; this . joinCall . bind ( this ) ; this . leaveCall . bind ( this ) ; this . setCamera . bind ( this ) ; this . setMicrophone . bind ( this ) ; this . setSpeaker . bind ( this ) ; this . askDevicePermission . bind ( this ) ; this . queryCameras . bind ( this ) ; this . queryMicrophones . bind ( this ) ; this . querySpeakers . bind ( this ) ; this . startCamera . bind ( this ) ; this . stopCamera . bind ( this ) ; this . mute . bind ( this ) ; this . unmute . bind ( this ) ; this . startCall . bind ( this ) ; this . startScreenShare . bind ( this ) ; this . stopScreenShare . bind ( this ) ; this . removeParticipant . bind ( this ) ; this . createStreamView . bind ( this ) ; this . disposeStreamView . bind ( this ) ; this . on . bind ( this ) ; this . off . bind ( this ) ; } public dispose ( ) { this . resetDiagnosticsForwarder ( ) ; this . callClient . offStateChange ( this . onClientStateChange ) ; this . callAgent . dispose ( ) ; } public async queryCameras ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return this . deviceManager . getCameras ( ) ; } ) ; } public async queryMicrophones ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return this . deviceManager . getMicrophones ( ) ; } ) ; } public async querySpeakers ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { return this . deviceManager . isSpeakerSelectionAvailable ? this . deviceManager . getSpeakers ( ) : [ ] ; } ) ; } public async askDevicePermission ( constrain ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . deviceManager . askDevicePermission ( constrain ) ; } ) ; } public joinCall ( microphoneOn ? ) { if ( _isInCall ( this . getState ( ) . call ?. state ?? 'None' ) ) { throw new Error ( 'You are already in the call!' ) ; } else { const audioOptions = { muted : microphoneOn ?? ! this . getState ( ) . isLocalPreviewMicrophoneEnabled } ; const videoOptions = { localVideoStreams : this . localStream ? [ this . localStream ] : undefined } ; const isTeamsMeeting = ! ( 'groupId' in this . locator ) ; if ( isTeamsMeeting ) { this . call = this . callAgent . join ( this . locator as TeamsMeetingLinkLocator , { audioOptions , videoOptions } ) ; } else { this . call = this . callAgent . join ( this . locator as GroupCallLocator , { audioOptions , videoOptions } ) ; } this . context . setCallId ( this . call . id ) ; this . context . updateClientState ( this . callClient . getState ( ) ) ; this . handlers = createDefaultCallingHandlers ( this . callClient , this . callAgent , this . deviceManager , this . call ) ; this . subscribeCallEvents ( ) ; return this . call ; } } public async createStreamView ( remoteUserId ? , options ? ) { if ( remoteUserId === undefined ) { await this . handlers . onCreateLocalStreamView ( options ) ; } else { await this . handlers . onCreateRemoteStreamView ( remoteUserId , options ) ; } } public async disposeStreamView ( remoteUserId ? ) { if ( remoteUserId === undefined ) { await this . handlers . onDisposeLocalStreamView ( ) ; } else { await this . handlers . onDisposeRemoteStreamView ( remoteUserId ) ; } } public async leaveCall ( ) { const callId = this . call ?. id ; await this . handlers . onHangUp ( ) ; this . unsubscribeCallEvents ( ) ; this . call = undefined ; this . handlers = createDefaultCallingHandlers ( this . callClient , this . callAgent , this . deviceManager , undefined ) ; this . context . setCallId ( undefined ) ; this . context . updateClientState ( this . callClient . getState ( ) ) ; this . stopCamera ( ) ; this . mute ( ) ; this . emitter . emit ( 'callEnded' , { callId } ) ; } public async setCamera ( device , options ? ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onSelectCamera ( device , options ) ; } ) ; } public async setMicrophone ( device ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onSelectMicrophone ( device ) ; } ) ; } public async setSpeaker ( device ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { await this . handlers . onSelectSpeaker ( device ) ; } ) ; } public async startCamera ( options ? ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { if ( ! isCameraOn ( this . getState ( ) ) ) { await this . handlers . onToggleCamera ( options ) ; } } ) ; } public async stopCamera ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { if ( isCameraOn ( this . getState ( ) ) ) { await this . handlers . onToggleCamera ( ) ; } } ) ; } public async mute ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { this . context . setIsLocalMicrophoneEnabled ( false ) ; if ( _isInCall ( this . call ?. state ) && ! this . call ?. isMuted ) { await this . handlers . onToggleMicrophone ( ) ; } } ) ; } public async unmute ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { this . context . setIsLocalMicrophoneEnabled ( true ) ; if ( _isInCall ( this . call ?. state ) && this . call ?. isMuted ) { await this . handlers . onToggleMicrophone ( ) ; } } ) ; } public async startScreenShare ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { if ( ! this . call ?. isScreenSharingOn ) { await this . handlers . onToggleScreenShare ( ) ; } } ) ; } public async stopScreenShare ( ) { return await this . asyncTeeErrorToEventEmitter ( async ( ) => { if ( this . call ?. isScreenSharingOn ) { await this . handlers . onToggleScreenShare ( ) ; } } ) ; } public startCall ( participants ) { const idsToAdd = participants . map ( ( participant ) => { const backendId = fromFlatCommunicationIdentifier ( participant ) as CommunicationUserIdentifier ; return backendId ; } ) ; return this . handlers . onStartCall ( idsToAdd ) ; } public async removeParticipant ( userId ) { this . handlers . onRemoveParticipant ( userId ) ; } public getState ( ) { return this . context . getState ( ) ; } public onStateChange ( handler ) { this . context . onStateChange ( handler ) ; } public offStateChange ( handler ) { this . context . offStateChange ( handler ) ; } on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , listener ) ; on ( event , errorHandler ) ; public on ( event , listener ) { this . emitter . on ( event , listener ) ; } private subscribeCallEvents ( ) { this . call ?. on ( 'remoteParticipantsUpdated' , this . onRemoteParticipantsUpdated . bind ( this ) ) ; this . call ?. on ( 'isMutedChanged' , this . isMyMutedChanged . bind ( this ) ) ; this . call ?. on ( 'isScreenSharingOnChanged' , this . isScreenSharingOnChanged . bind ( this ) ) ; this . call ?. on ( 'idChanged' , this . callIdChanged . bind ( this ) ) ; } private unsubscribeCallEvents ( ) { for ( const subscriber of this . participantSubscribers . values ( ) ) { subscriber . unsubscribeAll ( ) ; } this . participantSubscribers . clear ( ) ; this . call ?. off ( 'remoteParticipantsUpdated' , this . onRemoteParticipantsUpdated . bind ( this ) ) ; this . call ?. off ( 'isMutedChanged' , this . isMyMutedChanged . bind ( this ) ) ; this . call ?. off ( 'isScreenSharingOnChanged' , this . isScreenSharingOnChanged . bind ( this ) ) ; this . call ?. off ( 'idChanged' , this . callIdChanged . bind ( this ) ) ; } private isMyMutedChanged = ( ) : void => { this . emitter . emit ( 'isMutedChanged' , { participantId : this . getState ( ) . userId , isMuted : this . call ?. isMuted } ) ; } ; private onRemoteParticipantsUpdated ( { added , removed } ) { if ( added && added . length > 0 ) { this . emitter . emit ( 'participantsJoined' , added ) ; } if ( removed && removed . length > 0 ) { this . emitter . emit ( 'participantsLeft' , removed ) ; } added . forEach ( ( participant ) => { this . participantSubscribers . set ( toFlatCommunicationIdentifier ( participant . identifier ) , new ParticipantSubscriber ( participant , this . emitter ) ) ; } ) ; removed . forEach ( ( participant ) => { const subscriber = this . participantSubscribers . get ( toFlatCommunicationIdentifier ( participant . identifier ) ) ; subscriber && subscriber . unsubscribeAll ( ) ; this . participantSubscribers . delete ( toFlatCommunicationIdentifier ( participant . identifier ) ) ; } ) ; } private isScreenSharingOnChanged ( ) { this . emitter . emit ( 'isLocalScreenSharingActiveChanged' , { isScreenSharingOn : this . call ?. isScreenSharingOn } ) ; } private callIdChanged ( ) { this . context . setCallId ( this . call ?. id ) ; this . context . updateClientState ( this . callClient . getState ( ) ) ; this . emitter . emit ( 'callIdChanged' , { callId : this . callIdChanged . bind ( this ) } ) ; } private resetDiagnosticsForwarder ( newCall ? ) { if ( this . diagnosticsForwarder ) { this . diagnosticsForwarder . unsubscribe ( ) ; } if ( newCall ) { this . diagnosticsForwarder = new DiagnosticsForwarder ( this . emitter , newCall ) ; } } off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , listener ) ; off ( event , errorHandler ) ; public off ( event , listener ) { this . emitter . off ( event , listener ) ; } private async asyncTeeErrorToEventEmitter < T > ( f ) { try { return await f ( ) ; } catch ( error ) { if ( isCallError ( error ) ) { this . emitter . emit ( 'error' , error as AdapterError ) ; } throw error ; } } } /**
 * Arguments for creating the Azure Communication Services implementation of  CallAdapter .
 *
 * Note: `displayName` can be a maximum of 256 characters.
 *
 *  public export type AzureCommunicationCallAdapterArgs = { userId : CommunicationUserIdentifier ; displayName : string ; credential : CommunicationTokenCredential ; locator : TeamsMeetingLinkLocator | GroupCallLocator ; } ; /**
 * Create a  CallAdapter backed by Azure Communication Services.
 *
 * This is the default implementation of  CallAdapter provided by this library.
 *
 * Note: `displayName` can be a maximum of 256 characters.
 *
 *  public export const createAzureCommunicationCallAdapter = async ( { userId , displayName , credential , locator } ) : Promise < CallAdapter > => { const callClient = createStatefulCallClient ( { userId } ) ; const callAgent = await callClient . createCallAgent ( credential , { displayName } ) ; const adapter = createAzureCommunicationCallAdapterFromClient ( callClient , callAgent , locator ) ; return adapter ; } ; /**
 * Create a  CallAdapter using the provided  StatefulCallClient .
 *
 * Useful if you want to keep a reference to  StatefulCallClient .
 * Consider using  createAzureCommunicationCallAdapter for a simpler API.
 *
 *  public export const createAzureCommunicationCallAdapterFromClient = async ( callClient , callAgent , locator ) : Promise < CallAdapter > => { const deviceManager = ( await callClient . getDeviceManager ( ) ) as StatefulDeviceManager ; return new AzureCommunicationCallAdapter ( callClient , locator , callAgent , deviceManager ) ; } ; const isCallError = ( e ) : e is CallError => { return e [ 'target' ] !== undefined && e [ 'innerError' ] !== undefined ; } ;