import DefaultBrowserBehavior from '../browserbehavior/DefaultBrowserBehavior' ; import Logger from '../logger/Logger' ; import SignalingClientObserver from '../signalingclientobserver/SignalingClientObserver' ; import { ISdkClientDetails , SdkAudioControlFrame , SdkClientDetails , SdkClientMetricFrame , SdkDataMessageFrame , SdkJoinFlags , SdkJoinFrame , SdkLeaveFrame , SdkPauseResumeFrame , SdkPingPongFrame , SdkRemoteVideoUpdateFrame , SdkSignalFrame , SdkStreamDescriptor , SdkStreamMediaType , SdkStreamServiceType , SdkSubscribeFrame , SdkVideoSubscriptionConfiguration , } from '../signalingprotocol/SignalingProtocol.js' ; import Versioning from '../versioning/Versioning' ; import WebSocketAdapter from '../websocketadapter/WebSocketAdapter' ; import WebSocketReadyState from '../websocketadapter/WebSocketReadyState' ; import SignalingClient from './SignalingClient' ; import SignalingClientConnectionRequest from './SignalingClientConnectionRequest' ; import SignalingClientEvent from './SignalingClientEvent' ; import SignalingClientEventType from './SignalingClientEventType' ; import SignalingClientJoin from './SignalingClientJoin' ; import SignalingClientSubscribe from './SignalingClientSubscribe' ; import SignalingClientVideoSubscriptionConfiguration from './SignalingClientVideoSubscriptionConfiguration' ; /**
 * [[DefaultSignalingClient]] implements the SignalingClient interface.
 */ export default class DefaultSignalingClient implements SignalingClient { private static FRAME_TYPE_RTC : number = 0x5 ; private observerQueue : Set < SignalingClientObserver > ; private wasOpened : boolean ; private isClosing : boolean ; private connectionRequestQueue : SignalingClientConnectionRequest [ ] ; private unloadHandler : ( ) => void | null = null ; private audioSessionId : number ; constructor ( private webSocket , private logger ) { this . observerQueue = new Set < SignalingClientObserver > ( ) ; this . connectionRequestQueue = [ ] ; this . resetConnection ( ) ; this . logger . debug ( ( ) => 'signaling client init' ) ; this . audioSessionId = this . generateNewAudioSessionId ( ) ; } registerObserver ( observer ) { this . logger . debug ( ( ) => 'registering signaling client observer' ) ; this . observerQueue . add ( observer ) ; } removeObserver ( observer ) { this . logger . debug ( ( ) => 'removing signaling client observer' ) ; this . observerQueue . delete ( observer ) ; } openConnection ( request ) { this . logger . info ( 'adding connection request to queue: ' + request . url ( ) ) ; this . connectionRequestQueue . push ( request ) ; this . closeConnection ( ) ; } pingPong ( pingPongFrame ) { this . logger . debug ( ( ) => 'sending ping' ) ; const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . PING_PONG ; message . pingPong = pingPongFrame ; this . sendMessage ( message ) ; return message . timestampMs as number ; } join ( settings ) { this . logger . info ( 'sending join' ) ; const joinFrame = SdkJoinFrame . create ( ) ; joinFrame . protocolVersion = 2 ; joinFrame . flags = SdkJoinFlags . HAS_STREAM_UPDATE ; const browserBehavior = new DefaultBrowserBehavior ( ) ; if ( browserBehavior . supportsSenderSideBandwidthEstimation ( ) ) { joinFrame . flags |= SdkJoinFlags . USE_SEND_SIDE_BWE ; } joinFrame . flags |= settings . sendBitrates ? SdkJoinFlags . SEND_BITRATES : 0 ; const sdkClientDetails = { platformName : browserBehavior . name ( ) , platformVersion : browserBehavior . version ( ) , clientSource : Versioning . sdkName , chimeSdkVersion : Versioning . sdkVersion , } ; if ( settings . applicationMetadata ) { const { appName , appVersion } = settings . applicationMetadata ; sdkClientDetails . appName = appName ; sdkClientDetails . appVersion = appVersion ; } joinFrame . clientDetails = SdkClientDetails . create ( sdkClientDetails ) ; joinFrame . audioSessionId = this . audioSessionId ; const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . JOIN ; message . join = joinFrame ; this . sendMessage ( message ) ; } subscribe ( settings ) { const subscribeFrame = SdkSubscribeFrame . create ( ) ; subscribeFrame . sendStreams = [ ] ; subscribeFrame . sdpOffer = settings . sdpOffer ; if ( settings . connectionTypeHasVideo ) { subscribeFrame . receiveStreamIds = settings . receiveStreamIds ; } if ( settings . audioHost ) { subscribeFrame . audioCheckin = settings . audioCheckin ; subscribeFrame . audioHost = settings . audioHost ; subscribeFrame . audioMuted = settings . audioMuted ; if ( ! settings . audioCheckin ) { const audioStream = SdkStreamDescriptor . create ( ) ; audioStream . mediaType = SdkStreamMediaType . AUDIO ; audioStream . trackLabel = 'AmazonChimeExpressAudio' ; audioStream . attendeeId = settings . attendeeId ; audioStream . streamId = 1 ; audioStream . groupId = 1 ; audioStream . framerate = 15 ; audioStream . maxBitrateKbps = 600 ; audioStream . avgBitrateBps = 400000 ; subscribeFrame . sendStreams . push ( audioStream ) ; } } subscribeFrame . duplex = SdkStreamServiceType . RX ; if ( settings . localVideoEnabled ) { subscribeFrame . duplex = SdkStreamServiceType . DUPLEX ; for ( let i = 0 ; i < settings . videoStreamDescriptions . length ; i ++ ) { const streamDescription = settings . videoStreamDescriptions [ i ] . clone ( ) ; streamDescription . attendeeId = settings . attendeeId ; subscribeFrame . sendStreams . push ( streamDescription . toStreamDescriptor ( ) ) ; } } const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . SUBSCRIBE ; message . sub = subscribeFrame ; this . sendMessage ( message ) ; } remoteVideoUpdate ( addedOrUpdated , removed ) { const remoteVideoUpdate = SdkRemoteVideoUpdateFrame . create ( ) ; remoteVideoUpdate . addedOrUpdatedVideoSubscriptions = addedOrUpdated . map ( this . convertVideoSubscriptionConfiguration ) ; remoteVideoUpdate . removedVideoSubscriptionMids = removed ; const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . REMOTE_VIDEO_UPDATE ; message . remoteVideoUpdate = remoteVideoUpdate ; this . sendMessage ( message ) ; } private convertVideoSubscriptionConfiguration ( config ) { const signalConfig = new SdkVideoSubscriptionConfiguration ( ) ; signalConfig . mid = config . mid ; signalConfig . attendeeId = config . attendeeId ; signalConfig . streamId = config . streamId ; return signalConfig ; } leave ( ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . LEAVE ; message . leave = SdkLeaveFrame . create ( ) ; this . sendMessage ( message ) ; this . logger . debug ( ( ) => { return 'sent leave' ; } ) ; } sendClientMetrics ( clientMetricFrame ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . CLIENT_METRIC ; message . clientMetric = clientMetricFrame ; this . sendMessage ( message ) ; } sendDataMessage ( messageFrame ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . DATA_MESSAGE ; message . dataMessage = messageFrame ; this . sendMessage ( message ) ; } closeConnection ( ) { if ( this . webSocket . readyState ( ) !== WebSocketReadyState . None && this . webSocket . readyState ( ) !== WebSocketReadyState . Closed ) { this . isClosing = true ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketClosing , null ) ) ; this . webSocket . close ( ) ; this . deactivatePageUnloadHandler ( ) ; } else { this . logger . info ( 'no existing signaling client connection needs closing' ) ; this . serviceConnectionRequestQueue ( ) ; } } ready ( ) { return ( this . webSocket . readyState ( ) === WebSocketReadyState . Open && ! this . isClosing && this . wasOpened ) ; } mute ( muted ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . AUDIO_CONTROL ; const audioControl = SdkAudioControlFrame . create ( ) ; audioControl . muted = muted ; message . audioControl = audioControl ; this . sendMessage ( message ) ; } pause ( streamIds ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . PAUSE ; message . pause = SdkPauseResumeFrame . create ( ) ; message . pause . streamIds = streamIds ; this . sendMessage ( message ) ; } resume ( streamIds ) { const message = SdkSignalFrame . create ( ) ; message . type = SdkSignalFrame . Type . RESUME ; message . pause = SdkPauseResumeFrame . create ( ) ; message . pause . streamIds = streamIds ; this . sendMessage ( message ) ; } private resetConnection ( ) { this . webSocket . destroy ( ) ; this . wasOpened = false ; } private sendMessage ( message ) { message . timestampMs = Date . now ( ) ; this . logger . debug ( ( ) => `sending: ${ JSON . stringify ( message ) }` ) ; const buffer = this . prependWithFrameTypeRTC ( SdkSignalFrame . encode ( message ) . finish ( ) ) ; if ( this . ready ( ) ) { if ( ! this . webSocket . send ( buffer ) ) { this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketSendMessageFailure , null ) ) ; return ; } this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketSentMessage , null ) ) ; } else { this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketSkippedMessage , null ) ) ; } } private receiveMessage ( inBuffer ) { let message ; try { message = SdkSignalFrame . decode ( inBuffer ) ; } catch ( e ) { this . logger . info ( `failed to decode: ${ inBuffer }` ) ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . ProtocolDecodeFailure , null ) ) ; return ; } this . logger . debug ( ( ) => `received: ${ JSON . stringify ( message ) }` ) ; if ( this . webSocket . readyState ( ) === WebSocketReadyState . Open ) { this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . ReceivedSignalFrame , message ) ) ; } else { this . logger . info ( `skipping notification of message since WebSocket is not open: ${ JSON . stringify ( message ) }` ) ; } } private stripFrameTypeRTC ( inBuffer ) { const frameType = inBuffer [ 0 ] ; if ( frameType !== DefaultSignalingClient . FRAME_TYPE_RTC && frameType !== 0x02 ) { this . logger . warn ( `expected FrameTypeRTC for message but got ${ frameType }` ) ; } return inBuffer . slice ( 1 ) ; } private prependWithFrameTypeRTC ( inBuffer ) { const outBuffer = new Uint8Array ( inBuffer . length + 1 ) ; outBuffer [ 0 ] = DefaultSignalingClient . FRAME_TYPE_RTC ; outBuffer . set ( inBuffer , 1 ) ; return outBuffer ; } private serviceConnectionRequestQueue ( ) { if ( this . connectionRequestQueue . length === 0 ) { this . logger . info ( 'no connection requests to service' ) ; return ; } const request = this . connectionRequestQueue . shift ( ) ; this . logger . info ( `opening connection to ${ request . url ( ) }` ) ; this . isClosing = false ; this . webSocket . create ( request . url ( ) , request . protocols ( ) ) ; this . setUpEventListeners ( ) ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketConnecting , null ) ) ; } private sendEvent ( event ) { switch ( event . type ) { case SignalingClientEventType . WebSocketMessage : case SignalingClientEventType . ReceivedSignalFrame : case SignalingClientEventType . WebSocketSentMessage : this . logger . debug ( ( ) => `notifying event: ${ SignalingClientEventType [ event . type ] }` ) ; break ; case SignalingClientEventType . WebSocketSkippedMessage : this . logger . debug ( ( ) => `notifying event: ${ SignalingClientEventType [ event . type ] }, websocket state=${ WebSocketReadyState [ this . webSocket . readyState ( ) ] }` ) ; break ; default : this . logger . info ( `notifying event: ${ SignalingClientEventType [ event . type ] }` ) ; break ; } for ( const observer of this . observerQueue ) { observer . handleSignalingClientEvent ( event ) ; } } private setUpEventListeners ( ) { this . webSocket . addEventListener ( 'open' , ( ) => { this . activatePageUnloadHandler ( ) ; this . wasOpened = true ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketOpen , null ) ) ; } ) ; this . webSocket . addEventListener ( 'message' , ( event ) => { this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketMessage , null ) ) ; this . receiveMessage ( this . stripFrameTypeRTC ( new Uint8Array ( event . data ) ) ) ; } ) ; this . webSocket . addEventListener ( 'close' , ( event ) => { this . deactivatePageUnloadHandler ( ) ; this . resetConnection ( ) ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketClosed , null , event . code , event . reason ) ) ; this . serviceConnectionRequestQueue ( ) ; } ) ; this . webSocket . addEventListener ( 'error' , ( ) => { if ( this . isClosing && ! this . wasOpened ) { this . logger . info ( 'ignoring error closing signaling while connecting' ) ; return ; } if ( this . wasOpened ) { this . logger . error ( 'received error while connected' ) ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketError , null ) ) ; } else { this . logger . error ( 'failed to connect' ) ; this . sendEvent ( new SignalingClientEvent ( this , SignalingClientEventType . WebSocketFailed , null ) ) ; } } ) ; } private activatePageUnloadHandler ( ) { this . unloadHandler = ( ) => { this . leave ( ) ; } ; const GlobalAny = global as any ; GlobalAny [ 'window' ] && GlobalAny [ 'window' ] [ 'addEventListener' ] && window . addEventListener ( 'unload' , this . unloadHandler ) ; } private deactivatePageUnloadHandler ( ) { const GlobalAny = global as any ; GlobalAny [ 'window' ] && GlobalAny [ 'window' ] [ 'addEventListener' ] && window . removeEventListener ( 'unload' , this . unloadHandler ) ; this . unloadHandler = null ; } private generateNewAudioSessionId ( ) { const num = new Uint32Array ( 1 ) ; const randomNum = window . crypto . getRandomValues ( num ) ; return randomNum [ 0 ] ; } }