import DataMessage from '../datamessage/DataMessage' ; import DefaultTranscriptionController from '../transcript/DefaultTranscriptionController' ; import TranscriptionController from '../transcript/TranscriptionController' ; import RealtimeAttendeePositionInFrame from './RealtimeAttendeePositionInFrame' ; import RealtimeController from './RealtimeController' ; import RealtimeState from './RealtimeState' ; import RealtimeVolumeIndicator from './RealtimeVolumeIndicator' ; import type VolumeIndicatorCallback from './VolumeIndicatorCallback' ; /**
 * [[DefaultRealtimeController]] is written to adhere to the following tenets to
 * make privacy and performance bugs significantly less likely.
 *
 * 1. Any call to the object is guaranteed to succeed from the caller's
 *    perspective to the maximum extent that this can be ensured. However, all
 *    failures of the object are reported as fatal errors. For example, if local
 *    mute fails, then that is a privacy issue and we must tear down the
 *    connection and try starting over.
 *
 * 2. State is owned by the object and is considered authoritative at all times.
 *    For example, if [[realtimeIsLocalAudioMuted]] is true then the user *is*
 *    muted.
 *
 * 3. Callbacks are fired synchronously and do their work synchronously. Any
 *    unnecessary asynchronous implementation only invites latency and
 *    increases the surface error for potential errors.
 *
 * 4. Mutation only occurs when state changes. All state-changing functions are
 *    idempotent.
 *
 * 5. Every conditional branch gets its own if statement and test coverage is
 *    100% for this object.
 *
 * 6. Function parameters and returns use primitives only (no classes or enums).
 *    This minimizes the number of dependencies that consumers have to take on
 *    and allows the object to be more easily wrapped. Values are normalized
 *    where possible.
 *
 * 7. The object takes no other non-realtime dependencies.
 *
 * 8. Interface functions begin with `realtime` to make boundaries between the
 *    RealtimeController interface and the UI or business logic explicit and
 *    auditable.
 *
 * 9. Local state overrides remote state but not vice-versa. For example, if
 *    locally muted with an active audio input and a remote state indicates the
 *    same user is unmuted because the muted state has not yet propagated, then
 *    the volume indicator update for the user would show the remote mute state
 *    as muted. However, if locally muted without an active audio input and a
 *    remote state indicates the user is unmuted (since they are dialed in), the
 *    remote state persists but does not override the local state so
 *    [[realtimeIsLocalAudioMuted]] still returns true.
 */ export default class DefaultRealtimeController implements RealtimeController { private readonly state : RealtimeState = new RealtimeState ( ) ; private readonly _transcriptionController : TranscriptionController ; constructor ( transcriptionController ? ) { this . _transcriptionController = transcriptionController || new DefaultTranscriptionController ( this ) ; } realtimeSetLocalAttendeeId ( attendeeId , externalUserId ) { this . state . localAttendeeId = attendeeId ; this . state . localExternalUserId = externalUserId ; } realtimeSetAttendeeIdPresence ( attendeeId , present , externalUserId , dropped , posInFrame ) { try { if ( present ) { this . state . attendeeIdToExternalUserId [ attendeeId ] = externalUserId ; } for ( const fn of this . state . attendeeIdChangesCallbacks ) { fn ( attendeeId , present , externalUserId , dropped , posInFrame ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToAttendeeIdPresence ( callback ) { try { this . state . attendeeIdChangesCallbacks . push ( callback ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeToAttendeeIdPresence ( callback ) { try { const index = this . state . attendeeIdChangesCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . attendeeIdChangesCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSetLocalAudioInput ( audioInput ) { try { if ( this . state . audioInput === audioInput ) { return ; } this . setAudioInputEnabled ( false ) ; this . state . audioInput = audioInput ; this . setAudioInputEnabled ( ! this . state . muted ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeSetCanUnmuteLocalAudio ( canUnmute ) { try { if ( this . state . canUnmute === canUnmute ) { return ; } this . state . canUnmute = canUnmute ; for ( const fn of this . state . setCanUnmuteLocalAudioCallbacks ) { fn ( canUnmute ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToSetCanUnmuteLocalAudio ( callback ) { try { this . state . setCanUnmuteLocalAudioCallbacks . push ( callback ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeToSetCanUnmuteLocalAudio ( callback ) { try { const index = this . state . setCanUnmuteLocalAudioCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . setCanUnmuteLocalAudioCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeCanUnmuteLocalAudio ( ) { return this . state . canUnmute ; } realtimeMuteLocalAudio ( ) { if ( this . state . muted ) { return ; } try { this . setAudioInputEnabled ( false ) ; this . state . muted = true ; this . realtimeUpdateVolumeIndicator ( this . state . localAttendeeId , null , null , null , this . state . localExternalUserId ) ; for ( const fn of this . state . muteAndUnmuteLocalAudioCallbacks ) { fn ( true ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeUnmuteLocalAudio ( ) { if ( ! this . state . muted ) { return true ; } if ( ! this . state . canUnmute ) { return false ; } try { this . setAudioInputEnabled ( true ) ; this . state . muted = false ; this . realtimeUpdateVolumeIndicator ( this . state . localAttendeeId , null , null , null , this . state . localExternalUserId ) ; for ( const fn of this . state . muteAndUnmuteLocalAudioCallbacks ) { fn ( false ) ; } return true ; } catch ( e ) { this . onError ( e ) ; return false ; } } realtimeSubscribeToMuteAndUnmuteLocalAudio ( callback ) { try { this . state . muteAndUnmuteLocalAudioCallbacks . push ( callback ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeToMuteAndUnmuteLocalAudio ( callback ) { try { const index = this . state . muteAndUnmuteLocalAudioCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . muteAndUnmuteLocalAudioCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeIsLocalAudioMuted ( ) { return this . state . muted ; } realtimeSubscribeToVolumeIndicator ( attendeeId , callback ) { try { if ( ! this . state . volumeIndicatorCallbacks . hasOwnProperty ( attendeeId ) ) { this . state . volumeIndicatorCallbacks [ attendeeId ] = [ ] ; } this . state . volumeIndicatorCallbacks [ attendeeId ] . push ( callback ) ; this . sendVolumeIndicatorChange ( attendeeId , true , true , true , this . state . attendeeIdToExternalUserId [ attendeeId ] ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeFromVolumeIndicator ( attendeeId , callback ? ) { try { if ( callback ) { const index = this . state . volumeIndicatorCallbacks [ attendeeId ] . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . volumeIndicatorCallbacks [ attendeeId ] . splice ( index , 1 ) ; } } else { delete this . state . volumeIndicatorCallbacks [ attendeeId ] ; } } catch ( e ) { this . onError ( e ) ; } } realtimeUpdateVolumeIndicator ( attendeeId , volume , muted , signalStrength , externalUserId ) { try { muted = this . applyLocalMuteOverride ( attendeeId , muted ) ; const state = this . getVolumeIndicatorState ( attendeeId ) ; let volumeUpdated = false ; let mutedUpdated = false ; let signalStrengthUpdated = false ; if ( muted !== null ) { if ( state . muted !== muted ) { state . muted = muted ; mutedUpdated = true ; if ( state . muted && state . volume !== 0.0 ) { state . volume = 0.0 ; volumeUpdated = true ; } } } if ( ! state . muted && volume !== null ) { if ( state . volume !== volume ) { state . volume = volume ; volumeUpdated = true ; } if ( state . muted === null ) { state . muted = false ; mutedUpdated = true ; } } if ( signalStrength !== null ) { if ( state . signalStrength !== signalStrength ) { state . signalStrength = signalStrength ; signalStrengthUpdated = true ; } } this . sendVolumeIndicatorChange ( attendeeId , volumeUpdated , mutedUpdated , signalStrengthUpdated , externalUserId ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToLocalSignalStrengthChange ( callback ) { try { this . state . localSignalStrengthChangeCallbacks . push ( callback ) ; if ( this . state . localAttendeeId === null ) { return ; } this . sendLocalSignalStrengthChange ( this . state . localAttendeeId , true ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeToLocalSignalStrengthChange ( callback ) { try { const index = this . state . localSignalStrengthChangeCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . localSignalStrengthChangeCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToSendDataMessage ( callback ) { try { this . state . sendDataMessageCallbacks . push ( callback ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeFromSendDataMessage ( callback ) { try { const index = this . state . sendDataMessageCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . sendDataMessageCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSendDataMessage ( topic , data , lifetimeMs ? ) { try { for ( const fn of this . state . sendDataMessageCallbacks ) { fn ( topic , data , lifetimeMs ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToReceiveDataMessage ( topic , callback ) { try { if ( this . state . receiveDataMessageCallbacks . has ( topic ) ) { this . state . receiveDataMessageCallbacks . get ( topic ) . push ( callback ) ; } else { this . state . receiveDataMessageCallbacks . set ( topic , [ callback ] ) ; } } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeFromReceiveDataMessage ( topic ) { try { this . state . receiveDataMessageCallbacks . delete ( topic ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeReceiveDataMessage ( dataMessage ) { try { if ( this . state . receiveDataMessageCallbacks . has ( dataMessage . topic ) ) { for ( const fn of this . state . receiveDataMessageCallbacks . get ( dataMessage . topic ) ) { fn ( dataMessage ) ; } } } catch ( e ) { this . onError ( e ) ; } } realtimeSubscribeToFatalError ( callback ) { try { this . state . fatalErrorCallbacks . push ( callback ) ; } catch ( e ) { this . onError ( e ) ; } } realtimeUnsubscribeToFatalError ( callback ) { try { const index = this . state . fatalErrorCallbacks . indexOf ( callback ) ; if ( index !== - 1 ) { this . state . fatalErrorCallbacks . splice ( index , 1 ) ; } } catch ( e ) { this . onError ( e ) ; } } get transcriptionController ( ) : TranscriptionController { return this . _transcriptionController ; } private setAudioInputEnabled ( enabled ) { if ( ! this . state . audioInput ) { return ; } for ( const track of this . state . audioInput . getTracks ( ) ) { if ( track . enabled === enabled ) { continue ; } track . enabled = enabled ; } } private applyLocalMuteOverride ( attendeeIdRemote , mutedRemote ) { const attendeeIdLocal = this . state . localAttendeeId ; const mutedLocal = this . state . muted ; if ( attendeeIdRemote !== attendeeIdLocal ) { return mutedRemote ; } if ( this . state . audioInput === null ) { return mutedRemote ; } return mutedLocal ; } private sendVolumeIndicatorChange ( attendeeId , volumeUpdated , mutedUpdated , signalStrengthUpdated , externalUserId ) { this . sendLocalSignalStrengthChange ( attendeeId , signalStrengthUpdated ) ; if ( ! this . state . volumeIndicatorCallbacks . hasOwnProperty ( attendeeId ) ) { return ; } const state = this . getVolumeIndicatorState ( attendeeId ) ; const updateState = new RealtimeVolumeIndicator ( ) ; if ( volumeUpdated ) { updateState . volume = state . volume ; } if ( mutedUpdated ) { updateState . muted = state . muted ; } if ( signalStrengthUpdated ) { updateState . signalStrength = state . signalStrength ; } if ( this . stateIsEmpty ( updateState ) ) { return ; } for ( const fn of this . state . volumeIndicatorCallbacks [ attendeeId ] ) { fn ( attendeeId , updateState . volume , updateState . muted , updateState . signalStrength , externalUserId ) ; } } private sendLocalSignalStrengthChange ( attendeeId , signalStrengthUpdated ) { if ( ! signalStrengthUpdated ) { return ; } if ( attendeeId !== this . state . localAttendeeId ) { return ; } const state = this . getVolumeIndicatorState ( attendeeId ) ; const signalStrength = state . signalStrength ; if ( signalStrength === null ) { return ; } for ( const fn of this . state . localSignalStrengthChangeCallbacks ) { fn ( signalStrength ) ; } } private getVolumeIndicatorState ( id ) { if ( ! this . state . volumeIndicatorState . hasOwnProperty ( id ) ) { this . state . volumeIndicatorState [ id ] = new RealtimeVolumeIndicator ( ) ; } return this . state . volumeIndicatorState [ id ] ; } private stateIsEmpty ( state ) { return state . volume === null && state . muted === null && state . signalStrength === null ; } private onError ( error ) { try { for ( const callback of this . state . fatalErrorCallbacks ) { callback ( error ) ; } } catch ( eventError ) { try { console . error ( error ) ; console . error ( eventError ) ; } catch ( consoleError ) { } } } }