import { v4 as uuidv4 } from 'uuid' ; import logger from '../util/logger' ; import BrowserSession from '../BrowserSession' ; import BaseMessage from '../messages/BaseMessage' ; import { Invite , Answer , Attach , Bye , Modify , Info } from '../messages/Verto' ; import Peer from './Peer' ; import { SwEvent } from '../util/constants' ; import { INotificationEventData } from '../util/interfaces' ; import { State , DEFAULT_CALL_OPTIONS , ConferenceAction , Role , PeerType , VertoMethod , NOTIFICATION_TYPE , Direction , } from './constants' ; import { trigger , register , deRegister } from '../services/Handler' ; import { sdpStereoHack , sdpMediaOrderHack , checkSubscribeResponse , enableAudioTracks , disableAudioTracks , toggleAudioTracks , enableVideoTracks , disableVideoTracks , toggleVideoTracks , createAudio , playAudio , stopAudio , } from './helpers' ; import { objEmpty , mutateLiveArrayData , isFunction } from '../util/helpers' ; import { IVertoCallOptions , IWebRTCCall , IAudio , IStatsBinding } from './interfaces' ; import { attachMediaStream , detachMediaStream , sdpToJsonHack , stopStream , getUserMedia , setMediaElementSinkId , } from '../util/webrtc' ; import { MCULayoutEventHandler } from './LayoutHandler' ; import Call from './Call' ; import pkg from '../../../../package.json' ; const SDK_VERSION = pkg . version ; ignore export default abstract class BaseCall implements IWebRTCCall { /**
     * The call identifier.
     */ public id : string = '' ; /**
     * The `state` of the call.
     *
     * | Value | Description |
     * |---|---|
     * | `new` | New call has been created in the client. |
     * | `trying` | It's attempting to call someone. |
     * | `requesting` | The outbound call is being sent to the server. |
     * | `recovering` | The previous call is recovering after the page refreshes. If the user refreshes the page during a call, it will automatically join the latest call. |
     * | `ringing` | Someone is attempting to call you. |
     * | `answering` | You are attempting to answer this inbound call. |
     * | `early` | It receives the media before the call has been answered. |
     * | `active` | Call has become active. |
     * | `held` | Call has been held. |
     * | `hangup` | Call has ended. |
     * | `destroy` | Call has been destroyed. |
     * | `purge` | Call has been purged. |
     *
     */ public state : string = State [ State . New ] ; /**
     * The previous state of the call.
     * See `Call.state` for all possible values.
     */ public prevState : string = '' ; /**
     * The direction of the call.
     * Can be either `inbound` or `outbound`.
     */ public direction : Direction ; public peer : Peer ; public options : IVertoCallOptions ; public cause : string ; public causeCode : number ; public sipReason : string ; public sipCode : number ; public sipCallId : string ; public channels : string [ ] = [ ] ; public role : string = Role . Participant ; public extension : string = null ; private _state : State = State . New ; private _prevState : State = State . New ; private gotAnswer : boolean = false ; private gotEarly : boolean = false ; private _lastSerno : number = 0 ; private _targetNodeId : string = null ; private _iceTimeout = null ; private _iceDone : boolean = false ; private _ringtone : IAudio ; private _ringback : IAudio ; private _statsBindings : IStatsBinding [ ] = [ ] ; private _statsIntervalId : any = null ; constructor ( protected session , opts ? ) { const { iceServers , speaker : speakerId , micId , micLabel , camId , camLabel , localElement , remoteElement , mediaConstraints : { audio , video } , ringtoneFile , ringbackFile , } = session ; this . options = Object . assign ( { } , DEFAULT_CALL_OPTIONS , { audio , video , iceServers , localElement , remoteElement , micId , micLabel , camId , camLabel , speakerId , ringtoneFile , ringbackFile , } , opts ) ; this . _onMediaError = this . _onMediaError . bind ( this ) ; this . _init ( ) ; if ( this . options ) { this . _ringtone = createAudio ( this . options . ringtoneFile , '_ringtone' ) ; this . _ringback = createAudio ( this . options . ringbackFile , '_ringback' ) ; } } get nodeId ( ) : string { return this . _targetNodeId ; } set nodeId ( what ) { this . _targetNodeId = what ; } examples get telnyxIDs ( ) { return { telnyxCallControlId : this . options . telnyxCallControlId , telnyxSessionId : this . options . telnyxSessionId , telnyxLegId : this . options . telnyxLegId , } ; } examples get localStream ( ) { return this . options . localStream ; } examples get remoteStream ( ) { return this . options . remoteStream ; } get memberChannel ( ) { return `conference-member.${ this . id }` ; } invite ( ) { this . direction = Direction . Outbound ; this . peer = new Peer ( PeerType . Offer , this . options ) ; this . _registerPeerEvents ( ) ; } examples answer ( ) { this . stopRingtone ( ) ; this . direction = Direction . Inbound ; this . peer = new Peer ( PeerType . Answer , this . options ) ; this . _registerPeerEvents ( ) ; } playRingtone ( ) { playAudio ( this . _ringtone ) ; } stopRingtone ( ) { stopAudio ( this . _ringtone ) ; } playRingback ( ) { playAudio ( this . _ringback ) ; } stopRingback ( ) { stopAudio ( this . _ringback ) ; } examples hangup ( ) ; internal hangup ( hangupParams , hangupExecute ) ; internal param hangupParams param hangupExecute hangup ( hangupParams ? , hangupExecute ? ) { let params = hangupParams || { } ; let execute = hangupExecute === false ? false : true ; this . cause = params . cause || 'NORMAL_CLEARING' ; this . causeCode = params . causeCode || 16 ; this . sipCode = params . sipCode || null ; this . sipReason = params . sipReason || null ; this . sipCallId = params . sip_call_id || null ; this . setState ( State . Hangup ) ; const _close = ( ) => { this . peer ? this . peer . instance . close ( ) : null ; this . setState ( State . Destroy ) ; } ; this . stopRingtone ( ) ; if ( execute ) { const bye = new Bye ( { sessid : this . session . sessionid , dialogParams : this . options , cause : 'USER_BUSY' , causeCode : 17 , } ) ; this . _execute ( bye ) . catch ( ( error ) => { logger . error ( 'telnyl_rtc.bye failed!' , error ) ; trigger ( SwEvent . Error , error , this . session . uuid ) ; } ) . then ( _close . bind ( this ) ) ; } else { _close ( ) ; } } ignore param destination transfer ( destination ) { console . warn ( 'The call.transfer method is not currently implemented.' ) ; const msg = new Modify ( { sessid : this . session . sessionid , action : 'transfer' , destination , dialogParams : this . options , } ) ; this . _execute ( msg ) ; } replace ( replaceCallID ) { const msg = new Modify ( { sessid : this . session . sessionid , action : 'replace' , replaceCallID , dialogParams : this . options , } ) ; this . _execute ( msg ) ; } returns examples hold ( ) { const msg = new Modify ( { sessid : this . session . sessionid , action : 'hold' , dialogParams : this . options , } ) ; return this . _execute ( msg ) . then ( this . _handleChangeHoldStateSuccess . bind ( this ) ) . catch ( this . _handleChangeHoldStateError . bind ( this ) ) ; } returns examples unhold ( ) { const msg = new Modify ( { sessid : this . session . sessionid , action : 'unhold' , dialogParams : this . options , } ) ; return this . _execute ( msg ) . then ( this . _handleChangeHoldStateSuccess . bind ( this ) ) . catch ( this . _handleChangeHoldStateError . bind ( this ) ) ; } returns examples toggleHold ( ) { const msg = new Modify ( { sessid : this . session . sessionid , action : 'toggleHold' , dialogParams : this . options , } ) ; return this . _execute ( msg ) . then ( this . _handleChangeHoldStateSuccess . bind ( this ) ) . catch ( this . _handleChangeHoldStateError . bind ( this ) ) ; } param dtmf examples dtmf ( dtmf ) { const msg = new Info ( { sessid : this . session . sessionid , dtmf , dialogParams : this . options , } ) ; this . _execute ( msg ) ; } message ( to , body ) { const msg = { from : this . session . options . login , to , body } ; const info = new Info ( { sessid : this . session . sessionid , msg , dialogParams : this . options , } ) ; this . _execute ( info ) ; } examples muteAudio ( ) { disableAudioTracks ( this . options . localStream ) ; } examples unmuteAudio ( ) { enableAudioTracks ( this . options . localStream ) ; } examples toggleAudioMute ( ) { toggleAudioTracks ( this . options . localStream ) ; } examples param deviceId returns async setAudioInDevice ( deviceId ) { const { instance } = this . peer ; const sender = instance . getSenders ( ) . find ( ( { track : { kind } } ) => kind === 'audio' ) ; if ( sender ) { const newStream = await getUserMedia ( { audio : { deviceId : { exact : deviceId } } , } ) ; const audioTrack = newStream . getAudioTracks ( ) [ 0 ] ; sender . replaceTrack ( audioTrack ) ; this . options . micId = deviceId ; const { localStream } = this . options ; localStream . getAudioTracks ( ) . forEach ( ( t ) => t . stop ( ) ) ; localStream . getVideoTracks ( ) . forEach ( ( t ) => newStream . addTrack ( t ) ) ; this . options . localStream = newStream ; } } examples muteVideo ( ) { disableVideoTracks ( this . options . localStream ) ; } examples unmuteVideo ( ) { enableVideoTracks ( this . options . localStream ) ; } examples toggleVideoMute ( ) { toggleVideoTracks ( this . options . localStream ) ; } examples param deviceId returns async setVideoDevice ( deviceId ) { const { instance } = this . peer ; const sender = instance . getSenders ( ) . find ( ( { track : { kind } } ) => kind === 'video' ) ; if ( sender ) { const newStream = await getUserMedia ( { video : { deviceId : { exact : deviceId } } , } ) ; const videoTrack = newStream . getVideoTracks ( ) [ 0 ] ; sender . replaceTrack ( videoTrack ) ; const { localElement , localStream } = this . options ; attachMediaStream ( localElement , newStream ) ; this . options . camId = deviceId ; localStream . getAudioTracks ( ) . forEach ( ( t ) => newStream . addTrack ( t ) ) ; localStream . getVideoTracks ( ) . forEach ( ( t ) => t . stop ( ) ) ; this . options . localStream = newStream ; } } examples deaf ( ) { disableAudioTracks ( this . options . remoteStream ) ; } examples undeaf ( ) { enableAudioTracks ( this . options . remoteStream ) ; } examples toggleDeaf ( ) { toggleAudioTracks ( this . options . remoteStream ) ; } async setBandwidthEncodingsMaxBps ( max , _kind ) { if ( ! this || ! this . peer ) { logger . error ( 'Could not set bandwidth (reason: no peer connection). Dynamic bandwidth can only be set when there is a call running - is there any call running?)' ) ; return ; } const { instance } = this . peer ; const senders = instance . getSenders ( ) ; if ( ! senders ) { logger . error ( 'Could not set bandwidth (reason: no senders). Dynamic bandwidth can only be set when there is a call running - is there any call running?)' ) ; return ; } const sender = senders . find ( ( { track : { kind } } ) => kind === _kind ) ; if ( sender ) { let p = sender . getParameters ( ) ; const parameters = p as RTCRtpSendParameters ; if ( ! parameters . encodings ) { parameters . encodings = [ { rid : 'h' } ] ; } logger . info ( 'Parameters: ' , parameters ) ; logger . info ( 'Setting max ' , _kind === 'audio' ? 'audio' : 'video' , ' bandwidth to: ' , max , ' [bps]' ) ; parameters . encodings [ 0 ] . maxBitrate = max ; await sender . setParameters ( parameters ) . then ( ( ) => { logger . info ( _kind === 'audio' ? 'New audio' : 'New video' , ' bandwidth settings in use: ' , sender . getParameters ( ) ) ; } ) . catch ( ( e ) => console . error ( e ) ) ; } else { logger . error ( 'Could not set bandwidth (reason: no ' + _kind + ' sender). Dynamic bandwidth can only be set when there is a call running - is there any call running?)' ) ; } } setAudioBandwidthEncodingsMaxBps ( max ) { this . setBandwidthEncodingsMaxBps ( max , 'audio' ) ; } setVideoBandwidthEncodingsMaxBps ( max ) { this . setBandwidthEncodingsMaxBps ( max , 'video' ) ; } param callback param constraints returns getStats ( callback , constraints ) { if ( ! callback ) { return ; } const binding = { callback : callback , constraints : constraints } ; this . _statsBindings . push ( binding ) ; if ( ! this . _statsIntervalId ) { const STATS_INTERVAL = 2000 ; this . _startStats ( STATS_INTERVAL ) ; } } setState ( state ) { this . _prevState = this . _state ; this . _state = state ; this . state = State [ this . _state ] . toLowerCase ( ) ; this . prevState = State [ this . _prevState ] . toLowerCase ( ) ; logger . info ( `Call ${ this . id } state change from ${ this . prevState } to ${ this . state }` ) ; this . _dispatchNotification ( { type : NOTIFICATION_TYPE . callUpdate , call : this , } ) ; switch ( state ) { case State . Purge : this . hangup ( { cause : 'PURGE' , causeCode : '01' } , false ) ; break ; case State . Active : { setTimeout ( ( ) => { const { remoteElement , speakerId } = this . options ; if ( remoteElement && speakerId ) { setMediaElementSinkId ( remoteElement , speakerId ) ; } } , 0 ) ; break ; } case State . Destroy : this . _finalize ( ) ; break ; } } handleMessage ( msg ) { const { method , params } = msg ; switch ( method ) { case VertoMethod . Answer : { this . gotAnswer = true ; if ( this . _state >= State . Active ) { return ; } if ( this . _state >= State . Early ) { this . setState ( State . Active ) ; } if ( ! this . gotEarly ) { this . _onRemoteSdp ( params . sdp ) ; } this . stopRingback ( ) ; this . stopRingtone ( ) ; break ; } case VertoMethod . Media : { if ( this . _state >= State . Early ) { return ; } this . gotEarly = true ; this . _onRemoteSdp ( params . sdp ) ; break ; } case VertoMethod . Display : case VertoMethod . Attach : { const { display_name : displayName , display_number : displayNumber , display_direction , } = params ; this . extension = displayNumber ; const displayDirection = display_direction === Direction . Inbound ? Direction . Outbound : Direction . Inbound ; const notification = { type : NOTIFICATION_TYPE [ method ] , call : this , displayName , displayNumber , displayDirection , } ; if ( ! trigger ( SwEvent . Notification , notification , this . id ) ) { trigger ( SwEvent . Notification , notification , this . session . uuid ) ; } break ; } case VertoMethod . Info : case VertoMethod . Event : { const notification = { ... params , type : NOTIFICATION_TYPE . generic , call : this , } ; if ( ! trigger ( SwEvent . Notification , notification , this . id ) ) { trigger ( SwEvent . Notification , notification , this . session . uuid ) ; } break ; } case VertoMethod . Ringing : { this . playRingback ( ) ; break ; } case VertoMethod . Bye : this . stopRingback ( ) ; this . stopRingtone ( ) ; this . hangup ( params , false ) ; break ; } } async handleConferenceUpdate ( packet , initialPvtData ) { if ( ! this . _checkConferenceSerno ( packet . wireSerno ) && packet . name !== initialPvtData . laName ) { logger . error ( 'ConferenceUpdate invalid wireSerno or packet name:' , packet ) ; return 'INVALID_PACKET' ; } const { action , data , hashKey : callId = String ( this . _lastSerno ) , arrIndex : index , } = packet ; switch ( action ) { case 'bootObj' : { this . _lastSerno = 0 ; const { chatChannel , infoChannel , modChannel , laName , conferenceMemberID , role , } = initialPvtData ; this . _dispatchConferenceUpdate ( { action : ConferenceAction . Join , conferenceName : laName , participantId : Number ( conferenceMemberID ) , role , } ) ; if ( chatChannel ) { await this . _subscribeConferenceChat ( chatChannel ) ; } if ( infoChannel ) { await this . _subscribeConferenceInfo ( infoChannel ) ; } if ( modChannel && role === Role . Moderator ) { await this . _subscribeConferenceModerator ( modChannel ) ; } const participants = [ ] ; for ( const i in data ) { participants . push ( { callId : data [ i ] [ 0 ] , index : Number ( i ) , ... mutateLiveArrayData ( data [ i ] [ 1 ] ) , } ) ; } this . _dispatchConferenceUpdate ( { action : ConferenceAction . Bootstrap , participants , } ) ; break ; } case 'add' : { this . _dispatchConferenceUpdate ( { action : ConferenceAction . Add , callId , index , ... mutateLiveArrayData ( data ) , } ) ; break ; } case 'modify' : this . _dispatchConferenceUpdate ( { action : ConferenceAction . Modify , callId , index , ... mutateLiveArrayData ( data ) , } ) ; break ; case 'del' : this . _dispatchConferenceUpdate ( { action : ConferenceAction . Delete , callId , index , ... mutateLiveArrayData ( data ) , } ) ; break ; case 'clear' : this . _dispatchConferenceUpdate ( { action : ConferenceAction . Clear } ) ; break ; default : this . _dispatchConferenceUpdate ( { action , data , callId , index } ) ; break ; } } _addChannel ( channel ) { if ( ! this . channels . includes ( channel ) ) { this . channels . push ( channel ) ; } const protocol = this . session . relayProtocol ; if ( this . session . _existsSubscription ( protocol , channel ) ) { this . session . subscriptions [ protocol ] [ channel ] = { ... this . session . subscriptions [ protocol ] [ channel ] , callId : this . id , } ; } } private async _subscribeConferenceChat ( channel ) { const tmp = { nodeId : this . nodeId , channels : [ channel ] , handler : ( params ) => { const { direction , from : participantNumber , fromDisplay : participantName , message : messageText , type : messageType , } = params . data ; this . _dispatchConferenceUpdate ( { action : ConferenceAction . ChatMessage , direction , participantNumber , participantName , messageText , messageType , messageId : params . eventSerno , } ) ; } , } ; const response = await this . session . vertoSubscribe ( tmp ) . catch ( ( error ) => { logger . error ( 'ConfChat subscription error:' , error ) ; } ) ; if ( checkSubscribeResponse ( response , channel ) ) { this . _addChannel ( channel ) ; Object . defineProperties ( this , { sendChatMessage : { configurable : true , value : ( message , type ) => { this . session . vertoBroadcast ( { nodeId : this . nodeId , channel , data : { action : 'send' , message , type } , } ) ; } , } , } ) ; } } private async _subscribeConferenceInfo ( channel ) { const tmp = { nodeId : this . nodeId , channels : [ channel ] , handler : ( params ) => { const { eventData } = params ; switch ( eventData . contentType ) { case 'layout-info' : eventData . callID = this . id ; MCULayoutEventHandler ( this . session , eventData ) ; break ; default : logger . error ( 'Conference-Info unknown contentType' , params ) ; } } , } ; const response = await this . session . vertoSubscribe ( tmp ) . catch ( ( error ) => { logger . error ( 'ConfInfo subscription error:' , error ) ; } ) ; if ( checkSubscribeResponse ( response , channel ) ) { this . _addChannel ( channel ) ; } } private _confControl ( channel , params = { } ) { const data = { application : 'conf-control' , callID : this . id , value : null , ... params , } ; this . session . vertoBroadcast ( { nodeId : this . nodeId , channel , data } ) ; } private async _subscribeConferenceModerator ( channel ) { const _modCommand = ( command , memberID = null , value = null ) : void => { const id = parseInt ( memberID ) || null ; this . _confControl ( channel , { command , id , value } ) ; } ; const _videoRequired = ( ) : void => { const { video } = this . options ; if ( ( typeof video === 'boolean' && ! video ) || ( typeof video === 'object' && objEmpty ( video ) ) ) { throw `Conference ${ this . id } has no video!` ; } } ; const tmp = { nodeId : this . nodeId , channels : [ channel ] , handler : ( params ) => { const { data } = params ; switch ( data [ 'conf-command' ] ) { case 'list-videoLayouts' : if ( data . responseData ) { const tmp = JSON . stringify ( data . responseData ) . replace ( /IDS"/g , 'Ids"' ) ; this . _dispatchConferenceUpdate ( { action : ConferenceAction . LayoutList , layouts : JSON . parse ( tmp ) , } ) ; } break ; default : this . _dispatchConferenceUpdate ( { action : ConferenceAction . ModCmdResponse , command : data [ 'conf-command' ] , response : data . response , } ) ; } } , } ; const response = await this . session . vertoSubscribe ( tmp ) . catch ( ( error ) => { logger . error ( 'ConfMod subscription error:' , error ) ; } ) ; if ( checkSubscribeResponse ( response , channel ) ) { this . role = Role . Moderator ; this . _addChannel ( channel ) ; Object . defineProperties ( this , { listVideoLayouts : { configurable : true , value : ( ) => { _modCommand ( 'list-videoLayouts' ) ; } , } , playMedia : { configurable : true , value : ( file ) => { _modCommand ( 'play' , null , file ) ; } , } , stopMedia : { configurable : true , value : ( ) => { _modCommand ( 'stop' , null , 'all' ) ; } , } , deaf : { configurable : true , value : ( memberID ) => { _modCommand ( 'deaf' , memberID ) ; } , } , undeaf : { configurable : true , value : ( memberID ) => { _modCommand ( 'undeaf' , memberID ) ; } , } , startRecord : { configurable : true , value : ( file ) => { _modCommand ( 'recording' , null , [ 'start' , file ] ) ; } , } , stopRecord : { configurable : true , value : ( ) => { _modCommand ( 'recording' , null , [ 'stop' , 'all' ] ) ; } , } , snapshot : { configurable : true , value : ( file ) => { _videoRequired ( ) ; _modCommand ( 'vid-write-png' , null , file ) ; } , } , setVideoLayout : { configurable : true , value : ( layout , canvasID ) => { _videoRequired ( ) ; const value = canvasID ? [ layout , canvasID ] : layout ; _modCommand ( 'vid-layout' , null , value ) ; } , } , kick : { configurable : true , value : ( memberID ) => { _modCommand ( 'kick' , memberID ) ; } , } , muteMic : { configurable : true , value : ( memberID ) => { _modCommand ( 'tmute' , memberID ) ; } , } , muteVideo : { configurable : true , value : ( memberID ) => { _videoRequired ( ) ; _modCommand ( 'tvmute' , memberID ) ; } , } , presenter : { configurable : true , value : ( memberID ) => { _videoRequired ( ) ; _modCommand ( 'vid-res-id' , memberID , 'presenter' ) ; } , } , videoFloor : { configurable : true , value : ( memberID ) => { _videoRequired ( ) ; _modCommand ( 'vid-floor' , memberID , 'force' ) ; } , } , banner : { configurable : true , value : ( memberID , text ) => { _videoRequired ( ) ; _modCommand ( 'vid-banner' , memberID , encodeURI ( text ) ) ; } , } , volumeDown : { configurable : true , value : ( memberID ) => { _modCommand ( 'volume_out' , memberID , 'down' ) ; } , } , volumeUp : { configurable : true , value : ( memberID ) => { _modCommand ( 'volume_out' , memberID , 'up' ) ; } , } , gainDown : { configurable : true , value : ( memberID ) => { _modCommand ( 'volume_in' , memberID , 'down' ) ; } , } , gainUp : { configurable : true , value : ( memberID ) => { _modCommand ( 'volume_in' , memberID , 'up' ) ; } , } , transfer : { configurable : true , value : ( memberID , exten ) => { _modCommand ( 'transfer' , memberID , exten ) ; } , } , } ) ; } } private _handleChangeHoldStateSuccess ( response ) { response . holdState === 'active' ? this . setState ( State . Active ) : this . setState ( State . Held ) ; return true ; } private _handleChangeHoldStateError ( error ) { logger . error ( `Failed to ${ error . action } on call ${ this . id }` ) ; return false ; } private _onRemoteSdp ( remoteSdp ) { let sdp = sdpMediaOrderHack ( remoteSdp , this . peer . instance . localDescription . sdp ) ; if ( this . options . useStereo ) { sdp = sdpStereoHack ( sdp ) ; } const sessionDescr = sdpToJsonHack ( { sdp , type : PeerType . Answer , } ) ; this . peer . instance . setRemoteDescription ( sessionDescr ) . then ( ( ) => { if ( this . gotEarly ) { this . setState ( State . Early ) ; } if ( this . gotAnswer ) { this . setState ( State . Active ) ; } } ) . catch ( ( error ) => { logger . error ( 'Call setRemoteDescription Error: ' , error ) ; this . hangup ( ) ; } ) ; } private _requestAnotherLocalDescription ( ) { if ( isFunction ( this . peer . onSdpReadyTwice ) ) { trigger ( SwEvent . Error , new Error ( 'SDP without candidates for the second time!' ) , this . session . uuid ) ; return ; } Object . defineProperty ( this . peer , 'onSdpReadyTwice' , { value : this . _onIceSdp . bind ( this ) , } ) ; this . _iceDone = false ; this . peer . startNegotiation ( ) ; } private _onIceSdp ( data ) { if ( this . _iceTimeout ) { clearTimeout ( this . _iceTimeout ) ; } this . _iceTimeout = null ; this . _iceDone = true ; const { sdp , type } = data ; if ( sdp . indexOf ( 'candidate' ) === - 1 ) { logger . info ( 'No candidate - retry \n' ) ; this . _requestAnotherLocalDescription ( ) ; return ; } this . peer . instance . removeEventListener ( 'icecandidate' , this . _onIce ) ; let msg = null ; const tmpParams = { sessid : this . session . sessionid , sdp , dialogParams : this . options , 'User-Agent' : `Web-${ SDK_VERSION }` , } ; switch ( type ) { case PeerType . Offer : this . setState ( State . Requesting ) ; msg = new Invite ( tmpParams ) ; break ; case PeerType . Answer : this . setState ( State . Answering ) ; msg = this . options . attach === true ? new Attach ( tmpParams ) : new Answer ( tmpParams ) ; break ; default : logger . error ( `${ this . id } - Unknown local SDP type:` , data ) ; return this . hangup ( { } , false ) ; } this . _execute ( msg ) . then ( ( response ) => { const { node_id = null } = response ; this . _targetNodeId = node_id ; type === PeerType . Offer ? this . setState ( State . Trying ) : this . setState ( State . Active ) ; } ) . catch ( ( error ) => { logger . error ( `${ this . id } - Sending ${ type } error:` , error ) ; this . hangup ( ) ; } ) ; } private _onIce ( event ) { const { instance } = this . peer ; if ( this . _iceTimeout === null ) { this . _iceTimeout = setTimeout ( ( ) => this . _onIceSdp ( instance . localDescription ) , 1000 ) ; } if ( event . candidate ) { logger . debug ( 'RTCPeer Candidate:' , event . candidate ) ; } else { this . _onIceSdp ( instance . localDescription ) ; } } private _registerPeerEvents ( ) { const { instance } = this . peer ; this . _iceDone = false ; instance . onicecandidate = ( event ) => { if ( this . _iceDone ) { return ; } this . _onIce ( event ) ; } ; instance . addEventListener ( 'addstream' , ( event ) => { this . options . remoteStream = event . stream ; } ) ; instance . addEventListener ( 'track' , ( event ) => { this . options . remoteStream = event . streams [ 0 ] ; const { remoteElement , remoteStream , screenShare } = this . options ; if ( screenShare === false ) { attachMediaStream ( remoteElement , remoteStream ) ; } } ) ; } private _checkConferenceSerno = ( serno ) => { const check = serno < 0 || ! this . _lastSerno || ( this . _lastSerno && serno === this . _lastSerno + 1 ) ; if ( check && serno >= 0 ) { this . _lastSerno = serno ; } return check ; } ; private _onMediaError ( error ) { this . _dispatchNotification ( { type : NOTIFICATION_TYPE . userMediaError , error , } ) ; this . hangup ( { } , false ) ; } private _dispatchConferenceUpdate ( params ) { this . _dispatchNotification ( { type : NOTIFICATION_TYPE . conferenceUpdate , call : this , ... params , } ) ; } private _dispatchNotification ( notification ) { if ( this . options . screenShare === true ) { return ; } if ( ! trigger ( SwEvent . Notification , notification , this . id , false ) ) { trigger ( SwEvent . Notification , notification , this . session . uuid ) ; } } private _execute ( msg ) { if ( this . nodeId ) { msg . targetNodeId = this . nodeId ; } return this . session . execute ( msg ) ; } private _init ( ) { const { id , userVariables , remoteCallerNumber , onNotification , } = this . options ; if ( ! id ) { this . options . id = uuidv4 ( ) ; } this . id = this . options . id ; if ( ! userVariables || objEmpty ( userVariables ) ) { this . options . userVariables = this . session . options . userVariables || { } ; } if ( ! remoteCallerNumber ) { this . options . remoteCallerNumber = this . options . destinationNumber ; } this . session . calls [ this . id ] = this ; register ( SwEvent . MediaError , this . _onMediaError , this . id ) ; if ( isFunction ( onNotification ) ) { register ( SwEvent . Notification , onNotification . bind ( this ) , this . id ) ; } this . setState ( State . New ) ; logger . info ( 'New Call with Options:' , this . options ) ; } protected _finalize ( ) { this . _stopStats ( ) ; if ( this . peer && this . peer . instance ) { this . peer . instance . close ( ) ; this . peer = null ; } const { remoteStream , localStream } = this . options ; stopStream ( remoteStream ) ; stopStream ( localStream ) ; deRegister ( SwEvent . MediaError , null , this . id ) ; this . session . calls [ this . id ] = null ; delete this . session . calls [ this . id ] ; } private _startStats ( interval ) { this . _statsIntervalId = setInterval ( this . _doStats , interval ) ; logger . info ( 'Stats started' ) ; } private _stopStats ( ) { if ( this . _statsIntervalId ) { clearInterval ( this . _statsIntervalId ) ; this . _statsIntervalId = null ; } logger . info ( 'Stats stopped' ) ; } private _doStats = ( ) => { if ( ! this . peer || ! this . peer . instance ) { return ; } if ( this . _statsBindings . length === 0 ) { return ; } this . peer . instance . getStats ( ) . then ( ( res ) => { res . forEach ( ( report ) => { this . _statsBindings . forEach ( ( binding ) => { if ( ! binding . callback ) { return ; } if ( binding . constraints ) { for ( var key in binding . constraints ) { if ( binding . constraints . hasOwnProperty ( key ) && ( binding . constraints [ key ] !== report [ key ] ) ) { return ; } } } binding . callback ( report ) ; } ) ; } ) ; } ) ; } ; static setStateTelnyx = ( call ) => { if ( ! call ) { return ; } switch ( call . _state ) { case State . Requesting : case State . Recovering : case State . Trying : case State . Early : call . state = 'connecting' ; break ; case State . Active : call . state = 'active' ; break ; case State . Held : call . state = 'held' ; break ; case State . Hangup : case State . Destroy : call . state = 'done' ; break ; case State . Answering : call . state = 'ringing' ; break ; case State . New : call . state = 'new' ; break ; default : break ; } return call ; } ; }