2019-02-21 05:58:37 +08:00
import browser from 'browser-detect' ;
2017-10-12 22:49:50 +08:00
import BaseAudioBridge from './base' ;
2018-07-12 06:03:56 +08:00
import logger from '/imports/startup/client/logger' ;
2020-05-21 12:20:46 +08:00
import { fetchStunTurnServers , getFallbackStun } from '/imports/utils/fetchStunTurnServers' ;
2019-06-04 02:54:30 +08:00
import {
2019-12-19 04:49:35 +08:00
isUnifiedPlan ,
toUnifiedPlan ,
toPlanB ,
stripMDnsCandidates ,
analyzeSdp ,
logSelectedCandidate ,
2019-06-04 02:54:30 +08:00
} from '/imports/utils/sdpUtils' ;
2020-02-19 06:03:06 +08:00
import { Tracker } from 'meteor/tracker' ;
import VoiceCallStates from '/imports/api/voice-call-states' ;
import CallStateOptions from '/imports/api/voice-call-states/utils/callStates' ;
import Auth from '/imports/ui/services/auth' ;
2017-08-01 04:54:18 +08:00
2017-10-18 03:16:42 +08:00
const MEDIA = Meteor . settings . public . media ;
const MEDIA _TAG = MEDIA . mediaTag ;
const CALL _TRANSFER _TIMEOUT = MEDIA . callTransferTimeout ;
2017-10-27 01:14:56 +08:00
const CALL _HANGUP _TIMEOUT = MEDIA . callHangupTimeout ;
const CALL _HANGUP _MAX _RETRIES = MEDIA . callHangupMaximumRetries ;
2019-06-13 05:01:20 +08:00
const IPV4 _FALLBACK _DOMAIN = Meteor . settings . public . app . ipv4FallbackDomain ;
2019-11-23 05:48:46 +08:00
const CALL _CONNECT _TIMEOUT = 20000 ;
2019-04-13 06:23:22 +08:00
const ICE _NEGOTIATION _TIMEOUT = 20000 ;
2019-11-14 07:57:42 +08:00
const AUDIO _SESSION _NUM _KEY = 'AudioSessionNumber' ;
2020-09-26 07:11:44 +08:00
const USER _AGENT _RECONNECTION _ATTEMPTS = 3 ;
const USER _AGENT _RECONNECTION _DELAY _MS = 5000 ;
const USER _AGENT _CONNECTION _TIMEOUT _MS = 5000 ;
2019-11-14 07:57:42 +08:00
const getAudioSessionNumber = ( ) => {
let currItem = parseInt ( sessionStorage . getItem ( AUDIO _SESSION _NUM _KEY ) , 10 ) ;
if ( ! currItem ) {
currItem = 0 ;
}
currItem += 1 ;
sessionStorage . setItem ( AUDIO _SESSION _NUM _KEY , currItem ) ;
return currItem ;
} ;
2017-10-12 05:04:10 +08:00
2019-06-13 05:01:20 +08:00
class SIPSession {
2019-11-14 07:57:42 +08:00
constructor ( user , userData , protocol , hostname ,
2019-10-26 10:57:49 +08:00
baseCallStates , baseErrorCodes , reconnectAttempt ) {
2019-06-13 05:01:20 +08:00
this . user = user ;
this . userData = userData ;
this . protocol = protocol ;
this . hostname = hostname ;
this . baseCallStates = baseCallStates ;
2019-06-25 04:41:19 +08:00
this . baseErrorCodes = baseErrorCodes ;
2019-10-26 10:57:49 +08:00
this . reconnectAttempt = reconnectAttempt ;
2020-09-26 07:11:44 +08:00
this . currentSession = null ;
this . remoteStream = null ;
this . _hangupFlag = false ;
this . _reconnecting = false ;
this . _currentSessionState = null ;
2017-09-29 21:38:10 +08:00
}
2017-07-24 22:15:46 +08:00
2017-09-30 04:42:34 +08:00
joinAudio ( { isListenOnly , extension , inputStream } , managerCallback ) {
2017-10-05 04:49:11 +08:00
return new Promise ( ( resolve , reject ) => {
2017-10-12 22:49:50 +08:00
const callExtension = extension ? ` ${ extension } ${ this . userData . voiceBridge } ` : this . userData . voiceBridge ;
2017-07-24 22:15:46 +08:00
2017-09-29 21:38:10 +08:00
const callback = ( message ) => {
2019-06-13 05:01:20 +08:00
// There will sometimes we erroneous errors put out like timeouts and improper shutdowns,
// but only the first error ever matters
if ( this . alreadyErrored ) {
2019-06-29 05:45:50 +08:00
logger . info ( {
logCode : 'sip_js_absorbing_callback_message' ,
extraInfo : { message } ,
} , 'Absorbing a redundant callback message.' ) ;
2019-06-13 05:01:20 +08:00
return ;
}
if ( message . status === this . baseCallStates . failed ) {
this . alreadyErrored = true ;
}
2017-10-12 05:04:10 +08:00
managerCallback ( message ) . then ( resolve ) ;
2017-09-30 04:42:34 +08:00
} ;
2017-07-24 22:15:46 +08:00
2017-10-12 05:30:38 +08:00
this . callback = callback ;
2019-06-13 05:01:20 +08:00
// If there's an extension passed it means that we're joining the echo test first
this . inEchoTest = ! ! extension ;
2017-10-27 01:14:56 +08:00
return this . doCall ( { callExtension , isListenOnly , inputStream } )
2017-11-18 03:01:52 +08:00
. catch ( ( reason ) => {
reject ( reason ) ;
} ) ;
2017-09-30 04:42:34 +08:00
} ) ;
2017-07-24 22:15:46 +08:00
}
2020-06-13 05:13:49 +08:00
async getIceServers ( sessionToken ) {
2020-05-21 12:20:46 +08:00
try {
const iceServers = await fetchStunTurnServers ( sessionToken ) ;
return iceServers ;
} catch ( error ) {
logger . error ( {
logCode : 'sip_js_fetchstunturninfo_error' ,
extraInfo : {
errorCode : error . code ,
errorMessage : error . message ,
2020-06-13 05:13:49 +08:00
callerIdName : this . user . callerIdName ,
2020-05-21 12:20:46 +08:00
} ,
} , 'Full audio bridge failed to fetch STUN/TURN info' ) ;
return getFallbackStun ( ) ;
}
}
2017-10-20 18:11:51 +08:00
doCall ( options ) {
const {
isListenOnly ,
} = options ;
const {
userId ,
name ,
sessionToken ,
} = this . user ;
const callerIdName = [
2019-11-14 07:57:42 +08:00
` ${ userId } _ ${ getAudioSessionNumber ( ) } ` ,
2017-10-20 18:11:51 +08:00
'bbbID' ,
isListenOnly ? ` LISTENONLY- ${ name } ` : name ,
2019-02-22 04:53:39 +08:00
] . join ( '-' ) . replace ( /"/g , "'" ) ;
2017-10-20 18:11:51 +08:00
2019-02-22 04:53:39 +08:00
this . user . callerIdName = callerIdName ;
2017-10-20 18:11:51 +08:00
this . callOptions = options ;
2020-05-21 12:20:46 +08:00
return this . getIceServers ( sessionToken )
2017-11-18 03:01:52 +08:00
. then ( this . createUserAgent . bind ( this ) )
2020-09-26 07:11:44 +08:00
. then ( this . inviteUserAgent . bind ( this ) ) ;
2017-10-20 18:11:51 +08:00
}
2017-10-18 03:16:42 +08:00
transferCall ( onTransferSuccess ) {
2017-10-12 22:49:50 +08:00
return new Promise ( ( resolve , reject ) => {
2019-06-13 05:01:20 +08:00
this . inEchoTest = false ;
2020-02-19 06:03:06 +08:00
let trackerControl = null ;
const timeout = setTimeout ( ( ) => {
trackerControl . stop ( ) ;
2019-06-29 05:45:50 +08:00
logger . error ( { logCode : 'sip_js_transfer_timed_out' } , 'Timeout on transferring from echo test to conference' ) ;
2019-05-10 05:01:34 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
error : 1008 ,
bridgeError : 'Timeout on call transfer' ,
} ) ;
2019-11-23 05:48:46 +08:00
this . exitAudio ( ) ;
2019-05-10 05:01:34 +08:00
reject ( this . baseErrorCodes . REQUEST _TIMEOUT ) ;
2017-10-18 03:16:42 +08:00
} , CALL _TRANSFER _TIMEOUT ) ;
// This is is the call transfer code ask @chadpilkey
2020-09-26 07:11:44 +08:00
if ( this . sessionSupportRTPPayloadDtmf ( this . currentSession ) ) {
this . currentSession . sessionDescriptionHandler . sendDtmf ( 1 ) ;
} else {
// RFC4733 not supported , sending DTMF through INFO
logger . debug ( {
logCode : 'sip_js_rtp_payload_dtmf_not_supported' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'Browser do not support payload dtmf, using INFO instead' ) ;
this . sendDtmf ( 1 ) ;
}
2017-10-18 03:16:42 +08:00
2020-02-19 06:03:06 +08:00
Tracker . autorun ( ( c ) => {
trackerControl = c ;
const selector = { meetingId : Auth . meetingID , userId : Auth . userID } ;
const query = VoiceCallStates . find ( selector ) ;
query . observeChanges ( {
changed : ( id , fields ) => {
if ( fields . callState === CallStateOptions . IN _CONFERENCE ) {
clearTimeout ( timeout ) ;
onTransferSuccess ( ) ;
c . stop ( ) ;
resolve ( ) ;
}
} ,
} ) ;
2017-10-12 20:50:23 +08:00
} ) ;
2017-10-12 22:49:50 +08:00
} ) ;
2017-10-12 20:50:23 +08:00
}
2020-09-26 07:11:44 +08:00
/ * *
*
* sessionSupportRTPPayloadDtmf
* tells if browser support RFC4733 DTMF .
* Safari 13 doens ' t support it yet
* /
sessionSupportRTPPayloadDtmf ( session ) {
try {
const sessionDescriptionHandler = session
? session . sessionDescriptionHandler
: this . currentSession . sessionDescriptionHandler ;
const senders = sessionDescriptionHandler . peerConnection . getSenders ( ) ;
return ! ! ( senders [ 0 ] . dtmf ) ;
} catch ( error ) {
return false ;
}
}
/ * *
* sendDtmf - send DTMF Tones using INFO message
*
* same as SimpleUser ' s dtmf
* /
sendDtmf ( tone ) {
const dtmf = tone ;
const duration = 2000 ;
const body = {
contentDisposition : 'render' ,
contentType : 'application/dtmf-relay' ,
content : ` Signal= ${ dtmf } \r \n Duration= ${ duration } ` ,
} ;
const requestOptions = { body } ;
return this . currentSession . info ( { requestOptions } ) ;
}
2017-09-29 21:38:10 +08:00
exitAudio ( ) {
2017-10-27 01:14:56 +08:00
return new Promise ( ( resolve , reject ) => {
let hangupRetries = 0 ;
2020-09-26 07:11:44 +08:00
this . _hangupFlag = false ;
2018-07-03 00:36:54 +08:00
2019-02-21 05:58:37 +08:00
this . userRequestedHangup = true ;
2020-01-10 07:50:10 +08:00
2017-10-27 01:14:56 +08:00
const tryHangup = ( ) => {
2020-09-26 07:11:44 +08:00
if ( this . _hangupFlag ) {
resolve ( ) ;
}
if ( ( this . currentSession
&& ( this . currentSession . state === SIP . SessionState . Terminated ) )
|| ( this . userAgent && ( ! this . userAgent . isConnected ( ) ) ) ) {
this . _hangupFlag = true ;
2019-02-21 05:58:37 +08:00
return resolve ( ) ;
}
2020-09-26 07:11:44 +08:00
if ( this . currentSession
&& ( ( this . currentSession . state === SIP . SessionState . Establishing )
|| ( this . currentSession . state === SIP . SessionState . Established ) ) ) {
this . currentSession . bye ( ) . then ( ( ) => {
this . _hangupFlag = true ;
return resolve ( ) ;
} ) ;
}
if ( this . userAgent && this . userAgent . isConnected ( ) ) {
this . userAgent . stop ( ) ;
}
2019-06-13 05:01:20 +08:00
2017-10-27 01:14:56 +08:00
hangupRetries += 1 ;
setTimeout ( ( ) => {
if ( hangupRetries > CALL _HANGUP _MAX _RETRIES ) {
this . callback ( {
status : this . baseCallStates . failed ,
2019-02-21 05:58:37 +08:00
error : 1006 ,
2017-10-27 01:14:56 +08:00
bridgeError : 'Timeout on call hangup' ,
} ) ;
return reject ( this . baseErrorCodes . REQUEST _TIMEOUT ) ;
}
2020-09-26 07:11:44 +08:00
if ( ! this . _hangupFlag ) return tryHangup ( ) ;
2017-10-27 01:14:56 +08:00
return resolve ( ) ;
} , CALL _HANGUP _TIMEOUT ) ;
} ;
return tryHangup ( ) ;
2017-09-30 04:42:34 +08:00
} ) ;
}
2017-10-18 03:16:42 +08:00
createUserAgent ( { stun , turn } ) {
2017-09-30 04:42:34 +08:00
return new Promise ( ( resolve , reject ) => {
2020-01-10 07:50:10 +08:00
if ( this . userRequestedHangup === true ) reject ( ) ;
2017-10-18 03:16:42 +08:00
const {
hostname ,
protocol ,
} = this ;
const {
callerIdName ,
2020-07-30 00:10:17 +08:00
sessionToken ,
2017-10-18 03:16:42 +08:00
} = this . user ;
2020-06-13 05:13:49 +08:00
logger . debug ( { logCode : 'sip_js_creating_user_agent' , extraInfo : { callerIdName } } , 'Creating the user agent' ) ;
2019-02-21 05:58:37 +08:00
2019-06-13 05:01:20 +08:00
if ( this . userAgent && this . userAgent . isConnected ( ) ) {
if ( this . userAgent . configuration . hostPortParams === this . hostname ) {
2020-06-13 05:13:49 +08:00
logger . debug ( { logCode : 'sip_js_reusing_user_agent' , extraInfo : { callerIdName } } , 'Reusing the user agent' ) ;
2019-06-13 05:01:20 +08:00
resolve ( this . userAgent ) ;
return ;
}
2020-06-13 05:13:49 +08:00
logger . debug ( { logCode : 'sip_js_different_host_name' , extraInfo : { callerIdName } } , 'Different host name. need to kill' ) ;
2019-06-13 05:01:20 +08:00
}
2019-12-06 08:25:42 +08:00
const localSdpCallback = ( sdp ) => {
// For now we just need to call the utils function to parse and log the different pieces.
// In the future we're going to want to be tracking whether there were TURN candidates
// and IPv4 candidates to make informed decisions about what to do on fallbacks/reconnects.
analyzeSdp ( sdp ) ;
} ;
2019-12-19 04:49:35 +08:00
const remoteSdpCallback = ( sdp ) => {
// We have have to find the candidate that FS sends back to us to determine if the client
// is connecting with IPv4 or IPv6
const sdpInfo = analyzeSdp ( sdp , false ) ;
this . protocolIsIpv6 = sdpInfo . v6Info . found ;
} ;
2019-06-13 05:01:20 +08:00
let userAgentConnected = false ;
2020-07-30 00:10:17 +08:00
const token = ` sessionToken= ${ sessionToken } ` ;
2019-06-13 05:01:20 +08:00
2020-09-26 07:11:44 +08:00
this . userAgent = new SIP . UserAgent ( {
uri : SIP . UserAgent . makeURI ( ` sip: ${ encodeURIComponent ( callerIdName ) } @ ${ hostname } ` ) ,
transportOptions : {
server : ` ${ ( protocol === 'https:' ? 'wss://' : 'ws://' ) } ${ hostname } /ws? ${ token } ` ,
connectionTimeout : USER _AGENT _CONNECTION _TIMEOUT _MS ,
} ,
2017-10-18 03:16:42 +08:00
displayName : callerIdName ,
2017-09-30 04:42:34 +08:00
register : false ,
userAgentString : 'BigBlueButton' ,
2020-09-26 07:11:44 +08:00
iceServers : stun ? stun . concat ( turn || [ ] ) : turn ,
2017-09-30 04:42:34 +08:00
} ) ;
2017-10-18 03:16:42 +08:00
const handleUserAgentConnection = ( ) => {
2019-02-21 05:58:37 +08:00
userAgentConnected = true ;
2019-06-13 05:01:20 +08:00
resolve ( this . userAgent ) ;
2017-10-18 03:16:42 +08:00
} ;
2019-02-21 05:58:37 +08:00
const handleUserAgentDisconnection = ( ) => {
2019-06-13 05:01:20 +08:00
if ( this . userAgent ) {
2020-09-26 07:11:44 +08:00
if ( this . userRequestedHangup ) return ;
let error ;
let bridgeError ;
if ( ! this . _reconnecting ) {
if ( userAgentConnected ) {
error = 1001 ;
bridgeError = 'Websocket disconnected' ;
} else {
error = 1002 ;
bridgeError = 'Websocket failed to connect' ;
}
2019-02-21 05:58:37 +08:00
2020-09-26 07:11:44 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
error ,
bridgeError ,
} ) ;
}
2019-02-22 04:49:04 +08:00
2020-09-26 07:11:44 +08:00
logger . info ( {
logCode : 'sip_js_session_ua_disconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent disconnected: trying to reconnect...'
+ ` ${ this . userRequestedHangup } ` ) ;
2019-06-13 05:01:20 +08:00
2020-09-26 07:11:44 +08:00
logger . info ( {
logCode : 'sip_js_session_ua_reconnecting' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent failed to connect, reconnecting' ) ;
this . userAgent . reconnect ( ) . then ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_reconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent succesfully reconnected' ) ;
} ) . catch ( ( ) => {
reject ( this . baseErrorCodes . CONNECTION _ERROR ) ;
} ) ;
2019-02-21 05:58:37 +08:00
}
2020-09-26 07:11:44 +08:00
} ;
2019-02-21 05:58:37 +08:00
2020-09-26 07:11:44 +08:00
this . userAgent . transport . onConnect = handleUserAgentConnection ;
this . userAgent . transport . onDisconnect = handleUserAgentDisconnection ;
const preturn = this . userAgent . start ( ) . then ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_connected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent succesfully connected' ) ;
resolve ( ) ;
} ) . catch ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_reconnecting' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent failed to connect, reconnecting' ) ;
this . reconnect ( ) . then ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_reconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent succesfully reconnected' ) ;
resolve ( ) ;
} ) . catch ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_disconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent failed to reconnect after'
+ ` ${ USER _AGENT _RECONNECTION _ATTEMPTS } attemps ` ) ;
this . callback ( {
status : this . baseCallStates . failed ,
error : 1002 ,
bridgeError : 'Websocket failed to connect' ,
} ) ;
reject ( {
type : this . baseErrorCodes . CONNECTION _ERROR ,
} ) ;
2017-11-18 03:01:52 +08:00
} ) ;
2020-09-26 07:11:44 +08:00
} ) ;
return preturn ;
} ) ;
}
2017-09-30 04:42:34 +08:00
2020-09-26 07:11:44 +08:00
reconnect ( attempts = 1 ) {
return new Promise ( ( resolve , reject ) => {
if ( this . _reconnecting ) {
return resolve ( ) ;
}
2017-09-30 04:42:34 +08:00
2020-09-26 07:11:44 +08:00
if ( attempts > USER _AGENT _RECONNECTION _ATTEMPTS ) {
return reject ( {
type : this . baseErrorCodes . CONNECTION _ERROR ,
} ) ;
}
this . _reconnecting = true ;
setTimeout ( ( ) => {
this . userAgent . reconnect ( ) . then ( ( ) => {
this . _reconnecting = false ;
resolve ( ) ;
} ) . catch ( ( ) => {
this . _reconnecting = false ;
this . reconnect ( ++ attempts ) . then ( ( ) => {
resolve ( ) ;
} ) . catch ( ( error ) => {
reject ( error ) ;
} ) ;
} ) ;
} , USER _AGENT _RECONNECTION _DELAY _MS ) ;
2017-09-30 04:42:34 +08:00
} ) ;
}
2017-10-18 03:16:42 +08:00
inviteUserAgent ( userAgent ) {
2020-09-26 07:11:44 +08:00
return new Promise ( ( resolve , reject ) => {
if ( this . userRequestedHangup === true ) reject ( ) ;
const {
hostname ,
} = this ;
2020-01-10 07:50:10 +08:00
2020-09-26 07:11:44 +08:00
const {
callExtension ,
isListenOnly ,
} = this . callOptions ;
2017-10-18 03:16:42 +08:00
2020-09-26 07:11:44 +08:00
const target = SIP . UserAgent . makeURI ( ` sip: ${ callExtension } @ ${ hostname } ` ) ;
const inviterOptions = {
sessionDescriptionHandlerOptions : {
constraints : {
audio : ! isListenOnly ,
video : false ,
} ,
2017-10-18 03:16:42 +08:00
} ,
2020-09-26 07:11:44 +08:00
sessionDescriptionHandlerModifiersPostICEGathering :
[ stripMDnsCandidates ] ,
} ;
2017-10-11 02:03:29 +08:00
2020-09-26 07:11:44 +08:00
if ( isListenOnly ) {
inviterOptions . sessionDescriptionHandlerOptions . offerOptions = {
offerToReceiveAudio : true ,
} ;
}
const inviter = new SIP . Inviter ( userAgent , target , inviterOptions ) ;
this . currentSession = inviter ;
this . setupEventHandlers ( inviter ) . then ( ( ) => {
inviter . invite ( ) . then ( ( ) => {
resolve ( ) ;
} ) . catch ( e => reject ( e ) ) ;
} ) ;
} ) ;
2017-09-30 04:42:34 +08:00
}
2017-10-18 03:16:42 +08:00
setupEventHandlers ( currentSession ) {
2020-01-10 07:50:10 +08:00
return new Promise ( ( resolve , reject ) => {
if ( this . userRequestedHangup === true ) reject ( ) ;
2019-06-13 05:01:20 +08:00
let iceCompleted = false ;
let fsReady = false ;
2020-09-26 07:11:44 +08:00
const setupRemoteMedia = ( ) => {
const mediaElement = document . querySelector ( MEDIA _TAG ) ;
2019-02-21 05:58:37 +08:00
2020-09-26 07:11:44 +08:00
this . remoteStream = new MediaStream ( ) ;
this . currentSession . sessionDescriptionHandler
. peerConnection . getReceivers ( ) . forEach ( ( receiver ) => {
if ( receiver . track ) {
this . remoteStream . addTrack ( receiver . track ) ;
}
} ) ;
logger . info ( {
logCode : 'sip_js_session_playing_remote_media' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'Audio call - playing remote media' ) ;
mediaElement . srcObject = this . remoteStream ;
mediaElement . play ( ) ;
2019-01-30 08:11:20 +08:00
}
2019-05-10 05:01:34 +08:00
const checkIfCallReady = ( ) => {
2020-01-10 07:50:10 +08:00
if ( this . userRequestedHangup === true ) {
this . exitAudio ( ) ;
resolve ( ) ;
}
2020-09-26 07:11:44 +08:00
logger . info ( {
logCode : 'sip_js_session_check_if_call_ready' ,
extraInfo : {
iceCompleted ,
fsReady ,
} ,
} , 'Audio call - check if ICE is finished and FreeSWITCH is ready' ) ;
2019-06-13 05:01:20 +08:00
if ( iceCompleted && fsReady ) {
this . webrtcConnected = true ;
2020-09-26 07:11:44 +08:00
setupRemoteMedia ( ) ;
const { sdp } = this . currentSession . sessionDescriptionHandler
. peerConnection . remoteDescription ;
logger . info ( {
logCode : 'sip_js_session_setup_remote_media' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
sdp ,
} ,
} , 'Audio call - setup remote media' ) ;
2019-05-10 05:01:34 +08:00
this . callback ( { status : this . baseCallStates . started } ) ;
resolve ( ) ;
}
} ;
2019-02-22 04:49:04 +08:00
// Sometimes FreeSWITCH just won't respond with anything and hangs. This timeout is to
2019-02-21 05:58:37 +08:00
// avoid that issue
const callTimeout = setTimeout ( ( ) => {
this . callback ( {
status : this . baseCallStates . failed ,
error : 1006 ,
2019-02-22 04:49:04 +08:00
bridgeError : ` Call timed out on start after ${ CALL _CONNECT _TIMEOUT / 1000 } s ` ,
2019-02-21 05:58:37 +08:00
} ) ;
2019-11-23 05:48:46 +08:00
this . exitAudio ( ) ;
2019-02-21 05:58:37 +08:00
} , CALL _CONNECT _TIMEOUT ) ;
let iceNegotiationTimeout ;
2019-02-01 07:15:29 +08:00
const handleSessionAccepted = ( ) => {
2020-06-13 05:13:49 +08:00
logger . info ( { logCode : 'sip_js_session_accepted' , extraInfo : { callerIdName : this . user . callerIdName } } , 'Audio call session accepted' ) ;
2019-02-21 05:58:37 +08:00
clearTimeout ( callTimeout ) ;
// If ICE isn't connected yet then start timeout waiting for ICE to finish
2019-06-13 05:01:20 +08:00
if ( ! iceCompleted ) {
2019-02-21 05:58:37 +08:00
iceNegotiationTimeout = setTimeout ( ( ) => {
this . callback ( {
status : this . baseCallStates . failed ,
error : 1010 ,
2020-09-26 07:11:44 +08:00
bridgeError : 'ICE negotiation timeout after '
+ ` ${ ICE _NEGOTIATION _TIMEOUT / 1000 } s ` ,
2019-02-21 05:58:37 +08:00
} ) ;
2019-11-23 05:48:46 +08:00
this . exitAudio ( ) ;
2020-09-26 07:11:44 +08:00
reject ( {
type : this . baseErrorCodes . CONNECTION _ERROR
} ) ;
2019-02-21 05:58:37 +08:00
} , ICE _NEGOTIATION _TIMEOUT ) ;
}
2020-09-26 07:11:44 +08:00
checkIfCallReady ( ) ;
2019-02-01 07:15:29 +08:00
} ;
2020-09-26 07:11:44 +08:00
const handleIceNegotiationFailed = ( peer ) => {
if ( iceCompleted ) {
logger . error ( {
logCode : 'sipjs_ice_failed_after' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'ICE connection failed after success' ) ;
} else {
logger . error ( {
logCode : 'sipjs_ice_failed_before' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'ICE connection failed before success' ) ;
}
2019-03-15 05:02:51 +08:00
clearTimeout ( callTimeout ) ;
2020-09-26 07:11:44 +08:00
clearTimeout ( iceNegotiationTimeout ) ;
this . callback ( {
status : this . baseCallStates . failed ,
error : 1007 ,
bridgeError : 'ICE negotiation failed. Current state '
+ ` - ${ peer . iceConnectionState } ` ,
} ) ;
2019-03-15 05:02:51 +08:00
} ;
2020-09-26 07:11:44 +08:00
const handleIceConnectionTerminated = ( peer ) => {
if ( ! this . userRequestedHangup ) {
logger . error ( {
logCode : 'sipjs_ice_closed' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'ICE connection closed' ) ;
}
this . callback ( {
status : this . baseCallStates . failed ,
error : 1012 ,
bridgeError : 'ICE connection closed. Current state -'
+ ` ${ peer . iceConnectionState } ` ,
} ) ;
} ;
const handleSessionProgress = ( update ) => {
2019-06-29 05:45:50 +08:00
logger . info ( {
2020-09-26 07:11:44 +08:00
logCode : 'sip_js_session_progress' ,
2020-06-13 05:13:49 +08:00
extraInfo : {
callerIdName : this . user . callerIdName ,
2020-09-26 07:11:44 +08:00
update ,
2020-06-13 05:13:49 +08:00
} ,
2020-09-26 07:11:44 +08:00
} , 'Audio call session progress update' ) ;
this . currentSession . sessionDescriptionHandler . peerConnectionDelegate = {
onconnectionstatechange : ( event ) => {
const peer = event . target ;
switch ( peer . connectionState ) {
case 'connected' :
logger . info ( {
logCode : 'sip_js_ice_connection_success' ,
extraInfo : {
currentState : peer . connectionState ,
callerIdName : this . user . callerIdName ,
} ,
} , 'ICE connection success. Current state - '
+ ` ${ peer . iceConnectionState } ` ) ;
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
iceCompleted = true ;
logSelectedCandidate ( peer , this . protocolIsIpv6 ) ;
checkIfCallReady ( ) ;
break ;
case 'failed' :
handleIceNegotiationFailed ( peer ) ;
break ;
case 'closed' :
handleIceConnectionTerminated ( peer ) ;
break ;
default :
break ;
}
} ,
} ;
2017-10-18 03:16:42 +08:00
} ;
const handleSessionTerminated = ( message , cause ) => {
2019-02-21 05:58:37 +08:00
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
2019-06-13 05:01:20 +08:00
2019-02-21 05:58:37 +08:00
if ( ! message && ! cause && ! ! this . userRequestedHangup ) {
2017-10-18 03:16:42 +08:00
return this . callback ( {
status : this . baseCallStates . ended ,
} ) ;
}
2020-09-26 07:11:44 +08:00
// if session hasn't even started, we let audio-modal to handle
// any possile errors
if ( ! this . _currentSessionState ) return false ;
2019-06-29 05:45:50 +08:00
logger . error ( {
logCode : 'sip_js_call_terminated' ,
2020-06-13 05:13:49 +08:00
extraInfo : { cause , callerIdName : this . user . callerIdName } ,
2019-06-29 05:45:50 +08:00
} , ` Audio call terminated. cause= ${ cause } ` ) ;
2019-02-01 07:15:29 +08:00
2019-02-21 05:58:37 +08:00
let mappedCause ;
2019-06-13 05:01:20 +08:00
if ( ! iceCompleted ) {
2019-02-21 05:58:37 +08:00
mappedCause = '1004' ;
} else {
mappedCause = '1005' ;
}
2017-10-20 18:11:51 +08:00
2017-10-18 03:16:42 +08:00
return this . callback ( {
status : this . baseCallStates . failed ,
error : mappedCause ,
bridgeError : cause ,
} ) ;
} ;
2017-10-10 04:48:10 +08:00
2020-09-26 07:11:44 +08:00
currentSession . stateChange . addListener ( ( state ) => {
switch ( state ) {
case SIP . SessionState . Initial :
break ;
case SIP . SessionState . Establishing :
handleSessionProgress ( ) ;
break ;
case SIP . SessionState . Established :
handleSessionAccepted ( ) ;
break ;
case SIP . SessionState . Terminating :
break ;
case SIP . SessionState . Terminated :
handleSessionTerminated ( ) ;
break ;
default :
logger . error ( {
logCode : 'sipjs_ice_session_unknown_state' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'SIP.js unknown session state' ) ;
break ;
2019-05-10 05:01:34 +08:00
}
2020-09-26 07:11:44 +08:00
this . _currentSessionState = state ;
} ) ;
2018-04-03 21:50:18 +08:00
2020-02-19 06:03:06 +08:00
Tracker . autorun ( ( c ) => {
const selector = { meetingId : Auth . meetingID , userId : Auth . userID } ;
const query = VoiceCallStates . find ( selector ) ;
query . observeChanges ( {
changed : ( id , fields ) => {
2020-03-05 02:25:54 +08:00
if ( ( this . inEchoTest && fields . callState === CallStateOptions . IN _ECHO _TEST )
|| ( ! this . inEchoTest && fields . callState === CallStateOptions . IN _CONFERENCE ) ) {
2020-02-19 06:03:06 +08:00
fsReady = true ;
checkIfCallReady ( ) ;
c . stop ( ) ;
}
} ,
} ) ;
} ) ;
2020-09-26 07:11:44 +08:00
resolve ( ) ;
2019-06-13 05:01:20 +08:00
} ) ;
}
}
2019-05-10 05:01:34 +08:00
2019-06-13 05:01:20 +08:00
export default class SIPBridge extends BaseAudioBridge {
constructor ( userData ) {
super ( userData ) ;
const {
userId ,
username ,
sessionToken ,
} = userData ;
this . user = {
userId ,
sessionToken ,
name : username ,
} ;
this . media = {
inputDevice : { } ,
} ;
this . protocol = window . document . location . protocol ;
this . hostname = window . document . location . hostname ;
// SDP conversion utilitary methods to be used inside SIP.js
window . isUnifiedPlan = isUnifiedPlan ;
window . toUnifiedPlan = toUnifiedPlan ;
window . toPlanB = toPlanB ;
window . stripMDnsCandidates = stripMDnsCandidates ;
2020-02-29 08:38:30 +08:00
// No easy way to expose the client logger to sip.js code so we need to attach it globally
window . clientLogger = logger ;
2019-06-13 05:01:20 +08:00
}
joinAudio ( { isListenOnly , extension , inputStream } , managerCallback ) {
const hasFallbackDomain = typeof IPV4 _FALLBACK _DOMAIN === 'string' && IPV4 _FALLBACK _DOMAIN !== '' ;
return new Promise ( ( resolve , reject ) => {
let { hostname } = this ;
2019-06-26 03:13:31 +08:00
this . activeSession = new SIPSession ( this . user , this . userData , this . protocol ,
2019-10-26 10:57:49 +08:00
hostname , this . baseCallStates , this . baseErrorCodes , false ) ;
2019-06-13 05:01:20 +08:00
const callback = ( message ) => {
if ( message . status === this . baseCallStates . failed ) {
let shouldTryReconnect = false ;
// Try and get the call to clean up and end on an error
this . activeSession . exitAudio ( ) . catch ( ( ) => { } ) ;
if ( this . activeSession . webrtcConnected ) {
// webrtc was able to connect so just try again
message . silenceNotifications = true ;
callback ( { status : this . baseCallStates . reconnecting } ) ;
shouldTryReconnect = true ;
} else if ( hasFallbackDomain === true && hostname !== IPV4 _FALLBACK _DOMAIN ) {
message . silenceNotifications = true ;
2020-06-13 05:13:49 +08:00
logger . info ( { logCode : 'sip_js_attempt_ipv4_fallback' , extraInfo : { callerIdName : this . user . callerIdName } } , 'Attempting to fallback to IPv4 domain for audio' ) ;
2019-06-13 05:01:20 +08:00
hostname = IPV4 _FALLBACK _DOMAIN ;
shouldTryReconnect = true ;
}
if ( shouldTryReconnect ) {
const fallbackExtension = this . activeSession . inEchoTest ? extension : undefined ;
2019-06-26 03:13:31 +08:00
this . activeSession = new SIPSession ( this . user , this . userData , this . protocol ,
2019-10-26 10:57:49 +08:00
hostname , this . baseCallStates , this . baseErrorCodes , true ) ;
2019-06-13 05:01:20 +08:00
this . activeSession . joinAudio ( { isListenOnly , extension : fallbackExtension , inputStream } , callback )
. then ( ( value ) => {
resolve ( value ) ;
} ) . catch ( ( reason ) => {
reject ( reason ) ;
} ) ;
}
}
return managerCallback ( message ) ;
} ;
this . activeSession . joinAudio ( { isListenOnly , extension , inputStream } , callback )
. then ( ( value ) => {
resolve ( value ) ;
} ) . catch ( ( reason ) => {
reject ( reason ) ;
} ) ;
2017-10-12 05:04:10 +08:00
} ) ;
}
2019-06-13 05:01:20 +08:00
transferCall ( onTransferSuccess ) {
return this . activeSession . transferCall ( onTransferSuccess ) ;
}
2019-11-30 05:48:04 +08:00
getPeerConnection ( ) {
const { currentSession } = this . activeSession ;
2020-09-26 07:11:44 +08:00
if ( currentSession && currentSession . sessionDescriptionHandler ) {
return currentSession . sessionDescriptionHandler . peerConnection ;
2019-11-30 05:48:04 +08:00
}
return null ;
}
2019-06-13 05:01:20 +08:00
exitAudio ( ) {
return this . activeSession . exitAudio ( ) ;
}
2017-11-02 20:10:01 +08:00
setDefaultInputDevice ( ) {
2020-09-26 07:11:44 +08:00
// kept for compatibility
return Promise . resolve ( ) ;
2017-10-27 01:14:56 +08:00
}
2019-06-13 05:01:20 +08:00
changeInputDevice ( deviceId , deviceLabel ) {
2017-10-18 03:16:42 +08:00
const {
media ,
} = this ;
if ( media . inputDevice . audioContext ) {
2017-11-02 20:10:01 +08:00
const handleAudioContextCloseSuccess = ( ) => {
2017-10-18 03:16:42 +08:00
media . inputDevice . audioContext = null ;
media . inputDevice . scriptProcessor = null ;
media . inputDevice . source = null ;
2019-06-13 05:01:20 +08:00
return this . changeInputDevice ( deviceId ) ;
2017-11-18 03:01:52 +08:00
} ;
2017-11-02 20:10:01 +08:00
return media . inputDevice . audioContext . close ( ) . then ( handleAudioContextCloseSuccess ) ;
2017-10-18 03:16:42 +08:00
}
2017-10-12 05:04:10 +08:00
2017-10-18 03:16:42 +08:00
if ( 'AudioContext' in window ) {
media . inputDevice . audioContext = new window . AudioContext ( ) ;
} else {
media . inputDevice . audioContext = new window . webkitAudioContext ( ) ;
2017-10-12 05:04:10 +08:00
}
2017-11-02 20:10:01 +08:00
2019-06-13 05:01:20 +08:00
media . inputDevice . id = deviceId ;
media . inputDevice . label = deviceLabel ;
2017-10-18 03:16:42 +08:00
media . inputDevice . scriptProcessor = media . inputDevice . audioContext
2017-11-18 03:01:52 +08:00
. createScriptProcessor ( 2048 , 1 , 1 ) ;
2017-10-18 03:16:42 +08:00
media . inputDevice . source = null ;
const constraints = {
audio : {
2019-06-13 05:01:20 +08:00
deviceId ,
2017-10-18 03:16:42 +08:00
} ,
} ;
2017-10-12 05:04:10 +08:00
2017-11-02 20:10:01 +08:00
const handleMediaSuccess = ( mediaStream ) => {
media . inputDevice . stream = mediaStream ;
2017-11-18 03:01:52 +08:00
media . inputDevice . source = media . inputDevice . audioContext
. createMediaStreamSource ( mediaStream ) ;
2017-11-02 20:10:01 +08:00
media . inputDevice . source . connect ( media . inputDevice . scriptProcessor ) ;
media . inputDevice . scriptProcessor . connect ( media . inputDevice . audioContext . destination ) ;
return this . media . inputDevice ;
2017-11-18 03:01:52 +08:00
} ;
2017-10-18 03:16:42 +08:00
2017-11-02 20:10:01 +08:00
return navigator . mediaDevices . getUserMedia ( constraints ) . then ( handleMediaSuccess ) ;
2017-10-18 03:16:42 +08:00
}
2017-10-27 01:14:56 +08:00
async changeOutputDevice ( value ) {
2017-10-18 03:16:42 +08:00
const audioContext = document . querySelector ( MEDIA _TAG ) ;
if ( audioContext . setSinkId ) {
2017-10-27 01:14:56 +08:00
try {
2018-10-25 04:26:20 +08:00
audioContext . srcObject = null ;
2017-10-27 01:14:56 +08:00
await audioContext . setSinkId ( value ) ;
this . media . outputDeviceId = value ;
} catch ( err ) {
2019-06-29 05:45:50 +08:00
logger . error ( {
2019-07-03 04:50:25 +08:00
logCode : 'audio_sip_changeoutputdevice_error' ,
2020-06-13 05:13:49 +08:00
extraInfo : { error : err , callerIdName : this . user . callerIdName } ,
2019-06-29 05:45:50 +08:00
} , 'Change Output Device error' ) ;
2017-10-27 01:14:56 +08:00
throw new Error ( this . baseErrorCodes . MEDIA _ERROR ) ;
}
2017-10-18 03:16:42 +08:00
}
2017-10-19 03:40:01 +08:00
2017-11-17 19:52:48 +08:00
return this . media . outputDeviceId || value ;
2017-07-24 22:15:46 +08:00
}
}