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-10-26 10:57:49 +08:00
const RELAY _ONLY _ON _RECONNECT = MEDIA . relayOnlyOnReconnect ;
2019-06-13 05:01:20 +08:00
const IPV4 _FALLBACK _DOMAIN = Meteor . settings . public . app . ipv4FallbackDomain ;
2019-02-21 05:58:37 +08:00
const ICE _NEGOTIATION _FAILED = [ 'iceConnectionFailed' ] ;
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' ;
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 ;
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-05-26 01:32:24 +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 ) )
. then ( this . inviteUserAgent . bind ( this ) )
. then ( this . setupEventHandlers . 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
this . currentSession . dtmf ( 1 ) ;
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
}
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 ;
let hangup = 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
if ( this . currentSession ) {
const { mediaHandler } = this . currentSession ;
// Removing termination events to avoid triggering an error
ICE _NEGOTIATION _FAILED . forEach ( e => mediaHandler . off ( e ) ) ;
}
2017-10-27 01:14:56 +08:00
const tryHangup = ( ) => {
2020-01-10 07:50:10 +08:00
if ( ( this . currentSession && this . currentSession . endTime )
|| ( this . userAgent && this . userAgent . status === SIP . UA . C . STATUS _USER _CLOSED ) ) {
2019-02-21 05:58:37 +08:00
hangup = true ;
return resolve ( ) ;
}
2020-01-10 07:50:10 +08:00
if ( this . currentSession ) this . currentSession . bye ( ) ;
if ( this . userAgent ) 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 ) ;
}
if ( ! hangup ) return tryHangup ( ) ;
return resolve ( ) ;
} , CALL _HANGUP _TIMEOUT ) ;
} ;
2020-01-10 07:50:10 +08:00
if ( this . currentSession ) {
this . currentSession . on ( 'bye' , ( ) => {
hangup = true ;
resolve ( ) ;
} ) ;
}
2017-10-27 01:14:56 +08:00
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 ,
} = this . user ;
2019-05-22 00:48:01 +08:00
// WebView safari needs a transceiver to be added. Made it a SIP.js hack.
// Don't like the UA picking though, we should straighten everything to user
// transceivers - prlanzarin 2019/05/21
const browserUA = window . navigator . userAgent . toLocaleLowerCase ( ) ;
const isSafariWebview = ( ( browserUA . indexOf ( 'iphone' ) > - 1
2019-06-04 02:54:30 +08:00
|| browserUA . indexOf ( 'ipad' ) > - 1 ) && browserUA . indexOf ( 'safari' ) === - 1 ) ;
2019-05-22 00:48:01 +08:00
// Second UA check to get all Safari browsers to enable Unified Plan <-> PlanB
// translation
2019-05-22 00:56:57 +08:00
const isSafari = browser ( ) . name === 'safari' ;
2019-05-22 00:48:01 +08:00
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 ;
this . userAgent = new window . SIP . UA ( {
2017-10-18 03:16:42 +08:00
uri : ` sip: ${ encodeURIComponent ( callerIdName ) } @ ${ hostname } ` ,
wsServers : ` ${ ( protocol === 'https:' ? 'wss://' : 'ws://' ) } ${ hostname } /ws ` ,
displayName : callerIdName ,
2017-09-30 04:42:34 +08:00
register : false ,
traceSip : true ,
autostart : false ,
userAgentString : 'BigBlueButton' ,
stunServers : stun ,
turnServers : turn ,
2019-05-22 00:48:01 +08:00
hackPlanBUnifiedPlanTranslation : isSafari ,
hackAddAudioTransceiver : isSafariWebview ,
2019-10-26 10:57:49 +08:00
relayOnlyOnReconnect : this . reconnectAttempt && RELAY _ONLY _ON _RECONNECT ,
2019-12-06 08:25:42 +08:00
localSdpCallback ,
2019-12-19 04:49:35 +08:00
remoteSdpCallback ,
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 ) {
this . userAgent . removeAllListeners ( ) ;
this . userAgent . stop ( ) ;
}
2019-02-21 05:58:37 +08:00
let error ;
let bridgeError ;
2019-02-22 04:49:04 +08:00
2019-06-13 05:01:20 +08:00
if ( this . userRequestedHangup ) return ;
2019-02-21 05:58:37 +08:00
if ( userAgentConnected ) {
error = 1001 ;
bridgeError = 'Websocket disconnected' ;
} else {
error = 1002 ;
bridgeError = 'Websocket failed to connect' ;
}
2017-10-18 03:16:42 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
2018-06-27 21:56:03 +08:00
error ,
2019-02-21 05:58:37 +08:00
bridgeError ,
2017-11-18 03:01:52 +08:00
} ) ;
2017-10-20 18:11:51 +08:00
reject ( this . baseErrorCodes . CONNECTION _ERROR ) ;
2017-10-18 03:16:42 +08:00
} ;
2017-09-30 04:42:34 +08:00
2019-06-13 05:01:20 +08:00
this . userAgent . on ( 'connected' , handleUserAgentConnection ) ;
this . userAgent . on ( 'disconnected' , handleUserAgentDisconnection ) ;
2017-09-30 04:42:34 +08:00
2019-06-13 05:01:20 +08:00
this . userAgent . start ( ) ;
2017-09-30 04:42:34 +08:00
} ) ;
}
2017-10-18 03:16:42 +08:00
inviteUserAgent ( userAgent ) {
2020-01-10 07:50:10 +08:00
if ( this . userRequestedHangup === true ) Promise . reject ( ) ;
2017-10-18 03:16:42 +08:00
const {
hostname ,
} = this ;
const {
inputStream ,
callExtension ,
} = this . callOptions ;
const options = {
media : {
stream : inputStream ,
constraints : {
audio : true ,
video : false ,
} ,
render : {
remote : document . querySelector ( MEDIA _TAG ) ,
} ,
} ,
RTCConstraints : {
2018-12-22 01:14:05 +08:00
offerToReceiveAudio : true ,
offerToReceiveVideo : false ,
2017-10-18 03:16:42 +08:00
} ,
} ;
2017-10-11 02:03:29 +08:00
2017-10-18 03:16:42 +08:00
return userAgent . invite ( ` sip: ${ callExtension } @ ${ hostname } ` , options ) ;
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 ( ) ;
2018-04-04 00:49:45 +08:00
const { mediaHandler } = currentSession ;
2017-10-27 01:14:56 +08:00
2019-06-13 05:01:20 +08:00
let iceCompleted = false ;
let fsReady = false ;
this . currentSession = currentSession ;
2019-02-21 05:58:37 +08:00
2019-01-30 08:11:20 +08:00
let connectionCompletedEvents = [ 'iceConnectionCompleted' , 'iceConnectionConnected' ] ;
// Edge sends a connected first and then a completed, but the call isn't ready until
// the completed comes in. Due to the way that we have the listeners set up, the only
// way to ignore one status is to not listen for it.
if ( browser ( ) . name === 'edge' ) {
2019-02-21 05:58:37 +08:00
connectionCompletedEvents = [ 'iceConnectionCompleted' ] ;
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 ( ) ;
}
2019-06-13 05:01:20 +08:00
if ( iceCompleted && fsReady ) {
this . webrtcConnected = true ;
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 ) ;
2019-06-13 05:01:20 +08:00
currentSession . off ( 'accepted' , handleSessionAccepted ) ;
2019-02-21 05:58:37 +08:00
// 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 ,
2019-02-22 04:49:04 +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 ( ) ;
2019-02-21 05:58:37 +08:00
} , ICE _NEGOTIATION _TIMEOUT ) ;
}
2019-02-01 07:15:29 +08:00
} ;
currentSession . on ( 'accepted' , handleSessionAccepted ) ;
2019-03-15 05:02:51 +08:00
const handleSessionProgress = ( update ) => {
2020-06-13 05:13:49 +08:00
logger . info ( { logCode : 'sip_js_session_progress' , extraInfo : { callerIdName : this . user . callerIdName } } , 'Audio call session progress update' ) ;
2019-03-15 05:02:51 +08:00
clearTimeout ( callTimeout ) ;
currentSession . off ( 'progress' , handleSessionProgress ) ;
} ;
currentSession . on ( 'progress' , handleSessionProgress ) ;
2019-02-01 07:15:29 +08:00
const handleConnectionCompleted = ( peer ) => {
2019-06-29 05:45:50 +08:00
logger . info ( {
logCode : 'sip_js_ice_connection_success' ,
2020-06-13 05:13:49 +08:00
extraInfo : {
currentState : peer . iceConnectionState ,
callerIdName : this . user . callerIdName ,
} ,
2019-06-29 05:45:50 +08:00
} , ` ICE connection success. Current state - ${ peer . iceConnectionState } ` ) ;
2019-02-21 05:58:37 +08:00
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
2018-04-04 00:49:45 +08:00
connectionCompletedEvents . forEach ( e => mediaHandler . off ( e , handleConnectionCompleted ) ) ;
2019-06-13 05:01:20 +08:00
iceCompleted = true ;
2019-05-10 05:01:34 +08:00
2019-12-19 04:49:35 +08:00
logSelectedCandidate ( peer , this . protocolIsIpv6 ) ;
2019-05-10 05:01:34 +08:00
checkIfCallReady ( ) ;
2017-10-18 03:16:42 +08:00
} ;
2018-04-04 00:49:45 +08:00
connectionCompletedEvents . forEach ( e => mediaHandler . on ( e , handleConnectionCompleted ) ) ;
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
currentSession . off ( 'terminated' , handleSessionTerminated ) ;
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 ,
} ) ;
}
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 ,
} ) ;
} ;
currentSession . on ( 'terminated' , handleSessionTerminated ) ;
2017-10-10 04:48:10 +08:00
2019-02-21 05:58:37 +08:00
const handleIceNegotiationFailed = ( peer ) => {
2019-06-13 05:01:20 +08:00
if ( iceCompleted ) {
2020-06-13 05:13:49 +08:00
logger . error ( { logCode : 'sipjs_ice_failed_after' , extraInfo : { callerIdName : this . user . callerIdName } } , 'ICE connection failed after success' ) ;
2019-05-10 05:01:34 +08:00
} else {
2020-06-13 05:13:49 +08:00
logger . error ( { logCode : 'sipjs_ice_failed_before' , extraInfo : { callerIdName : this . user . callerIdName } } , 'ICE connection failed before success' ) ;
2019-05-10 05:01:34 +08:00
}
2019-02-21 05:58:37 +08:00
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
ICE _NEGOTIATION _FAILED . forEach ( e => mediaHandler . off ( e , handleIceNegotiationFailed ) ) ;
this . callback ( {
status : this . baseCallStates . failed ,
error : 1007 ,
bridgeError : ` ICE negotiation failed. Current state - ${ peer . iceConnectionState } ` ,
} ) ;
} ;
ICE _NEGOTIATION _FAILED . forEach ( e => mediaHandler . on ( e , handleIceNegotiationFailed ) ) ;
const handleIceConnectionTerminated = ( peer ) => {
[ 'iceConnectionClosed' ] . forEach ( e => mediaHandler . off ( e , handleIceConnectionTerminated ) ) ;
2019-02-22 04:53:39 +08:00
if ( ! this . userRequestedHangup ) {
2020-06-13 05:13:49 +08:00
logger . error ( { logCode : 'sipjs_ice_closed' , extraInfo : { callerIdName : this . user . callerIdName } } , 'ICE connection closed' ) ;
2019-02-22 04:53:39 +08:00
}
2019-02-21 05:58:37 +08:00
/ *
2018-04-04 00:49:45 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
2019-02-21 05:58:37 +08:00
error : 1012 ,
bridgeError : "ICE connection closed. Current state - " + peer . iceConnectionState ,
2018-04-04 00:49:45 +08:00
} ) ;
2019-02-21 05:58:37 +08:00
* /
2018-04-04 00:49:45 +08:00
} ;
2019-02-21 05:58:37 +08:00
[ 'iceConnectionClosed' ] . forEach ( e => mediaHandler . on ( e , handleIceConnectionTerminated ) ) ;
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 ( ) ;
}
} ,
} ) ;
} ) ;
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 ;
if ( currentSession && currentSession . mediaHandler ) {
return currentSession . mediaHandler . peerConnection ;
}
return null ;
}
2019-06-13 05:01:20 +08:00
exitAudio ( ) {
return this . activeSession . exitAudio ( ) ;
}
2017-11-02 20:10:01 +08:00
setDefaultInputDevice ( ) {
const handleMediaSuccess = ( mediaStream ) => {
const deviceLabel = mediaStream . getAudioTracks ( ) [ 0 ] . label ;
2018-04-18 22:15:17 +08:00
window . defaultInputStream = mediaStream . getTracks ( ) ;
2017-11-02 20:10:01 +08:00
return navigator . mediaDevices . enumerateDevices ( ) . then ( ( mediaDevices ) => {
const device = mediaDevices . find ( d => d . label === deviceLabel ) ;
2019-06-13 05:01:20 +08:00
return this . changeInputDevice ( device . deviceId , deviceLabel ) ;
2017-11-02 20:10:01 +08:00
} ) ;
2017-11-18 03:01:52 +08:00
} ;
2017-10-27 01:14:56 +08:00
2017-11-02 20:10:01 +08:00
return navigator . mediaDevices . getUserMedia ( { audio : true } ) . then ( handleMediaSuccess ) ;
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
}
}