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-10-01 21:16:48 +08:00
import {
fetchWebRTCMappedStunTurnServers ,
2020-10-03 03:15:54 +08:00
getMappedFallbackStun ,
2020-10-01 21:16:48 +08:00
} 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 ,
2021-06-30 01:47:59 +08:00
filterValidIceCandidates ,
2019-12-19 04:49:35 +08:00
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' ;
2021-01-23 03:30:42 +08:00
import Settings from '/imports/ui/services/settings' ;
2021-03-17 22:30:07 +08:00
import Storage from '/imports/ui/services/storage/session' ;
2021-04-01 19:14:24 +08:00
import browserInfo from '/imports/utils/browserInfo' ;
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 ;
2020-11-10 20:41:48 +08:00
const SIPJS _HACK _VIA _WS = MEDIA . sipjsHackViaWs ;
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-11-06 20:22:12 +08:00
const USER _AGENT _RECONNECTION _ATTEMPTS = 3 ;
2020-09-26 07:11:44 +08:00
const USER _AGENT _RECONNECTION _DELAY _MS = 5000 ;
const USER _AGENT _CONNECTION _TIMEOUT _MS = 5000 ;
2020-10-24 19:58:25 +08:00
const ICE _GATHERING _TIMEOUT = MEDIA . iceGatheringTimeout || 5000 ;
2020-12-02 02:23:14 +08:00
const BRIDGE _NAME = 'sip' ;
2020-12-11 10:48:01 +08:00
const WEBSOCKET _KEEP _ALIVE _INTERVAL = MEDIA . websocketKeepAliveInterval || 0 ;
const WEBSOCKET _KEEP _ALIVE _DEBOUNCE = MEDIA . websocketKeepAliveDebounce || 10 ;
2020-12-11 11:31:10 +08:00
const TRACE _SIP = MEDIA . traceSip || false ;
2021-01-23 03:30:42 +08:00
const AUDIO _MICROPHONE _CONSTRAINTS = Meteor . settings . public . app . defaultSettings
2021-01-27 01:12:18 +08:00
. application . microphoneConstraints ;
2021-03-12 11:04:55 +08:00
const SDP _SEMANTICS = MEDIA . sdpSemantics ;
2019-11-14 07:57:42 +08:00
2021-03-17 22:30:07 +08:00
const DEFAULT _INPUT _DEVICE _ID = 'default' ;
const DEFAULT _OUTPUT _DEVICE _ID = 'default' ;
const INPUT _DEVICE _ID _KEY = 'audioInputDeviceId' ;
const OUTPUT _DEVICE _ID _KEY = 'audioOutputDeviceId' ;
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
2020-03-15 11:50:39 +08:00
/ * *
* Get error code from SIP . js websocket messages .
* /
const getErrorCode = ( error ) => {
try {
if ( ! error ) return error ;
const match = error . message . match ( /code: \d+/g ) ;
const _codeArray = match [ 0 ] . split ( ':' ) ;
return parseInt ( _codeArray [ 1 ] . trim ( ) , 10 ) ;
} catch ( e ) {
return 0 ;
}
} ;
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 ;
2021-04-02 02:53:43 +08:00
this . _inputDeviceId = null ;
2021-03-17 22:30:07 +08:00
this . _outputDeviceId = null ;
2020-09-26 07:11:44 +08:00
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
2021-01-30 06:05:51 +08:00
get inputStream ( ) {
if ( this . currentSession && this . currentSession . sessionDescriptionHandler ) {
return this . currentSession . sessionDescriptionHandler . localMediaStream ;
}
return null ;
}
2021-03-17 22:30:07 +08:00
getAudioConstraints ( ) {
const userSettingsConstraints = Settings . application . microphoneConstraints ;
const audioDeviceConstraints = userSettingsConstraints
|| AUDIO _MICROPHONE _CONSTRAINTS || { } ;
const matchConstraints = this . filterSupportedConstraints (
audioDeviceConstraints ,
) ;
if ( this . inputDeviceId ) {
2021-04-27 09:02:17 +08:00
matchConstraints . deviceId = { exact : this . inputDeviceId } ;
2021-03-17 22:30:07 +08:00
}
return matchConstraints ;
}
2021-04-16 21:45:40 +08:00
/ * *
* Set the input stream for the peer that represents the current session .
* Internally , this will call the sender ' s replaceTrack function .
* @ param { MediaStream } stream The MediaStream object to be used as input
* stream
* @ return { Promise } A Promise that is resolved with the
* MediaStream object that was set .
* /
async setInputStream ( stream ) {
try {
if ( ! this . currentSession
|| ! this . currentSession . sessionDescriptionHandler
) return null ;
await this . currentSession . sessionDescriptionHandler
. setLocalMediaStream ( stream ) ;
return stream ;
} catch ( error ) {
logger . warn ( {
logCode : 'sip_js_setinputstream_error' ,
extraInfo : {
errorCode : error . code ,
errorMessage : error . message ,
callerIdName : this . user . callerIdName ,
} ,
} , 'Failed to set input stream (mic)' ) ;
return null ;
}
2021-03-17 22:30:07 +08:00
}
2021-04-16 21:45:40 +08:00
/ * *
* Change the input device with the given deviceId , without renegotiating
* peer .
* A new MediaStream object is created for the given deviceId . This object
* is returned by the resolved promise .
* @ param { String } deviceId The id of the device to be set as input
* @ return { Promise } A promise that is resolved with the MediaStream
* object after changing the input device .
* /
async liveChangeInputDevice ( deviceId ) {
try {
this . inputDeviceId = deviceId ;
const constraints = {
audio : this . getAudioConstraints ( ) ,
} ;
this . inputStream . getAudioTracks ( ) . forEach ( ( t ) => t . stop ( ) ) ;
return await navigator . mediaDevices . getUserMedia ( constraints )
. then ( this . setInputStream . bind ( this ) ) ;
} catch ( error ) {
logger . warn ( {
logCode : 'sip_js_livechangeinputdevice_error' ,
extraInfo : {
errorCode : error . code ,
errorMessage : error . message ,
callerIdName : this . user . callerIdName ,
} ,
} , 'Failed to change input device (mic)' ) ;
return null ;
}
2021-03-17 22:30:07 +08:00
}
get inputDeviceId ( ) {
if ( ! this . _inputDeviceId ) {
const stream = this . inputStream ;
if ( stream ) {
const track = stream . getAudioTracks ( ) . find (
2021-04-16 21:45:40 +08:00
( t ) => t . getSettings ( ) . deviceId ,
2021-03-17 22:30:07 +08:00
) ;
if ( track && ( typeof track . getSettings === 'function' ) ) {
const { deviceId } = track . getSettings ( ) ;
2021-04-27 09:02:17 +08:00
this . _inputDeviceId = deviceId ;
2021-03-17 22:30:07 +08:00
}
}
}
2021-04-27 09:02:17 +08:00
return this . _inputDeviceId ;
2021-03-17 22:30:07 +08:00
}
set inputDeviceId ( deviceId ) {
this . _inputDeviceId = deviceId ;
}
get outputDeviceId ( ) {
if ( ! this . _outputDeviceId ) {
const audioElement = document . querySelector ( MEDIA _TAG ) ;
2021-04-02 02:53:43 +08:00
if ( audioElement ) {
2021-04-27 09:02:17 +08:00
this . _outputDeviceId = audioElement . sinkId ;
2021-03-17 22:30:07 +08:00
}
}
2021-04-27 09:02:17 +08:00
return this . _outputDeviceId ;
2021-03-17 22:30:07 +08:00
}
set outputDeviceId ( deviceId ) {
this . _outputDeviceId = deviceId ;
}
2021-04-23 02:03:43 +08:00
joinAudio ( {
isListenOnly ,
extension ,
inputDeviceId ,
outputDeviceId ,
2021-06-30 01:47:59 +08:00
validIceCandidates ,
2021-04-23 02:03:43 +08:00
} , 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 ;
2021-06-30 01:47:59 +08:00
this . validIceCandidates = validIceCandidates ;
2021-04-23 02:03:43 +08:00
return this . doCall ( {
callExtension ,
isListenOnly ,
inputDeviceId ,
outputDeviceId ,
} ) . 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 {
2020-10-01 21:16:48 +08:00
const iceServers = await fetchWebRTCMappedStunTurnServers ( sessionToken ) ;
2020-05-21 12:20:46 +08:00
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' ) ;
2020-10-03 03:15:54 +08:00
return getMappedFallbackStun ( ) ;
2020-05-21 12:20:46 +08:00
}
}
2017-10-20 18:11:51 +08:00
doCall ( options ) {
const {
isListenOnly ,
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
inputDeviceId ,
2021-04-23 02:03:43 +08:00
outputDeviceId ,
2017-10-20 18:11:51 +08:00
} = options ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
this . inputDeviceId = inputDeviceId ;
2021-04-23 02:03:43 +08:00
this . outputDeviceId = outputDeviceId ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
2017-10-20 18:11:51 +08:00
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' ,
2020-11-20 10:11:39 +08:00
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' ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2019-05-10 05:01:34 +08:00
} ) ;
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-11-26 05:18:56 +08:00
logger . debug ( {
2020-11-26 11:01:58 +08:00
logCode : 'sip_js_rtp_payload_send_dtmf' ,
2020-11-26 05:18:56 +08:00
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'Sending DTMF INFO to transfer user' ) ;
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
2020-03-15 11:50:39 +08:00
&& ( ( this . currentSession . state === SIP . SessionState . Establishing ) ) ) {
this . currentSession . cancel ( ) . then ( ( ) => {
this . _hangupFlag = true ;
return resolve ( ) ;
} ) ;
}
if ( this . currentSession
&& ( ( this . currentSession . state === SIP . SessionState . Established ) ) ) {
2020-09-26 07:11:44 +08:00
this . currentSession . bye ( ) . then ( ( ) => {
this . _hangupFlag = true ;
return resolve ( ) ;
} ) ;
}
if ( this . userAgent && this . userAgent . isConnected ( ) ) {
this . userAgent . stop ( ) ;
2020-03-15 11:50:39 +08:00
window . removeEventListener ( 'beforeunload' , this . onBeforeUnload ) ;
2020-09-26 07:11:44 +08:00
}
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' ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2017-10-27 01:14:56 +08:00
} ) ;
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
} ) ;
}
2020-10-29 01:38:33 +08:00
stopUserAgent ( ) {
if ( this . userAgent && ( typeof this . userAgent . stop === 'function' ) ) {
2020-03-15 11:50:39 +08:00
return this . userAgent . stop ( ) ;
}
return Promise . resolve ( ) ;
}
2020-10-29 01:38:33 +08:00
onBeforeUnload ( ) {
this . userRequestedHangup = true ;
return this . stopUserAgent ( ) ;
}
2020-10-01 21:16:48 +08:00
createUserAgent ( iceServers ) {
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 ,
2020-12-11 10:48:01 +08:00
keepAliveInterval : WEBSOCKET _KEEP _ALIVE _INTERVAL ,
keepAliveDebounce : WEBSOCKET _KEEP _ALIVE _DEBOUNCE ,
2020-12-11 11:31:10 +08:00
traceSip : TRACE _SIP ,
2020-09-26 07:11:44 +08:00
} ,
2020-10-01 21:16:48 +08:00
sessionDescriptionHandlerFactoryOptions : {
peerConnectionConfiguration : {
iceServers ,
2021-03-12 11:04:55 +08:00
sdpSemantics : SDP _SEMANTICS ,
2020-10-01 21:16:48 +08:00
} ,
} ,
2017-10-18 03:16:42 +08:00
displayName : callerIdName ,
2017-09-30 04:42:34 +08:00
register : false ,
userAgentString : 'BigBlueButton' ,
2020-11-10 20:41:48 +08:00
hackViaWs : SIPJS _HACK _VIA _WS ,
2017-09-30 04:42:34 +08:00
} ) ;
2017-10-18 03:16:42 +08:00
const handleUserAgentConnection = ( ) => {
2020-03-15 11:50:39 +08:00
if ( ! userAgentConnected ) {
userAgentConnected = true ;
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-03-15 11:50:39 +08:00
if ( this . userRequestedHangup ) {
userAgentConnected = false ;
return ;
}
2020-09-26 07:11:44 +08:00
let error ;
let bridgeError ;
if ( ! this . _reconnecting ) {
2020-03-15 11:50:39 +08:00
logger . info ( {
logCode : 'sip_js_session_ua_disconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent disconnected: trying to reconnect...'
2021-04-02 02:11:13 +08:00
+ ` (userHangup = ${ ! ! this . userRequestedHangup } ) ` ) ;
2020-09-26 07:11:44 +08:00
logger . info ( {
2020-03-15 11:50:39 +08:00
logCode : 'sip_js_session_ua_reconnecting' ,
2020-09-26 07:11:44 +08:00
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
2020-03-15 11:50:39 +08:00
} , 'User agent disconnected, reconnecting' ) ;
this . reconnect ( ) . then ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_reconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent succesfully reconnected' ) ;
} ) . catch ( ( ) => {
if ( userAgentConnected ) {
error = 1001 ;
bridgeError = 'Websocket disconnected' ;
} else {
error = 1002 ;
bridgeError = 'Websocket failed to connect' ;
}
2020-10-29 01:38:33 +08:00
this . stopUserAgent ( ) ;
2020-03-15 11:50:39 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
error ,
bridgeError ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2020-03-15 11:50:39 +08:00
} ) ;
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' ) ;
2020-03-15 11:50:39 +08:00
window . addEventListener ( 'beforeunload' , this . onBeforeUnload . bind ( this ) ) ;
2020-09-26 07:11:44 +08:00
resolve ( ) ;
2020-03-15 11:50:39 +08:00
} ) . catch ( ( error ) => {
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' ) ;
2020-03-15 11:50:39 +08:00
const code = getErrorCode ( error ) ;
2020-12-02 04:02:50 +08:00
// Websocket's 1006 is currently mapped to BBB's 1002
2020-03-15 11:50:39 +08:00
if ( code === 1006 ) {
2020-10-29 01:38:33 +08:00
this . stopUserAgent ( ) ;
2020-03-15 11:50:39 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
2020-11-06 20:22:12 +08:00
error : 1002 ,
2020-03-15 11:50:39 +08:00
bridgeError : 'Websocket failed to connect' ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2020-03-15 11:50:39 +08:00
} ) ;
return reject ( {
type : this . baseErrorCodes . CONNECTION _ERROR ,
} ) ;
}
2020-09-26 07:11:44 +08:00
this . reconnect ( ) . then ( ( ) => {
logger . info ( {
logCode : 'sip_js_session_ua_reconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent succesfully reconnected' ) ;
resolve ( ) ;
} ) . catch ( ( ) => {
2020-10-29 01:38:33 +08:00
this . stopUserAgent ( ) ;
2020-09-26 07:11:44 +08:00
logger . info ( {
logCode : 'sip_js_session_ua_disconnected' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'User agent failed to reconnect after'
2021-04-02 02:11:13 +08:00
+ ` ${ USER _AGENT _RECONNECTION _ATTEMPTS } attemps ` ) ;
2020-09-26 07:11:44 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
error : 1002 ,
bridgeError : 'Websocket failed to connect' ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2020-09-26 07:11:44 +08:00
} ) ;
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 ;
2020-10-29 01:38:33 +08:00
logger . info ( {
logCode : 'sip_js_session_ua_reconnection_attempt' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , ` User agent reconnection attempt ${ attempts } ` ) ;
2020-11-23 23:40:38 +08:00
this . userAgent . reconnect ( ) . then ( ( ) => {
this . _reconnecting = false ;
resolve ( ) ;
} ) . catch ( ( ) => {
setTimeout ( ( ) => {
2020-09-26 07:11:44 +08:00
this . _reconnecting = false ;
this . reconnect ( ++ attempts ) . then ( ( ) => {
resolve ( ) ;
} ) . catch ( ( error ) => {
reject ( error ) ;
} ) ;
2020-11-23 23:40:38 +08:00
} , USER _AGENT _RECONNECTION _DELAY _MS ) ;
} ) ;
2017-09-30 04:42:34 +08:00
} ) ;
}
2021-06-30 01:47:59 +08:00
isValidIceCandidate ( event ) {
return event . candidate
&& this . validIceCandidates
&& this . validIceCandidates . find ( ( validCandidate ) => (
( validCandidate . address === event . candidate . address )
|| ( validCandidate . relatedAddress === event . candidate . address ) )
&& ( validCandidate . protocol === event . candidate . protocol ) ) ;
}
onIceGatheringStateChange ( event ) {
const iceGatheringState = event . target
? event . target . iceGatheringState
: null ;
2021-07-15 21:46:49 +08:00
if ( ( iceGatheringState === 'gathering' ) && ( ! this . _iceGatheringStartTime ) ) {
this . _iceGatheringStartTime = new Date ( ) ;
}
2021-06-30 01:47:59 +08:00
if ( iceGatheringState === 'complete' ) {
2021-07-15 21:46:49 +08:00
const secondsToGatherIce = ( new Date ( )
- ( this . _iceGatheringStartTime || this . _sessionStartTime ) ) / 1000 ;
2021-06-30 01:47:59 +08:00
logger . info ( {
logCode : 'sip_js_ice_gathering_time' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
2021-07-16 22:07:52 +08:00
secondsToGatherIce ,
2021-06-30 01:47:59 +08:00
} ,
} , ` ICE gathering candidates took (s): ${ secondsToGatherIce } ` ) ;
}
}
onIceCandidate ( sessionDescriptionHandler , event ) {
if ( this . isValidIceCandidate ( event ) ) {
logger . info ( {
logCode : 'sip_js_found_valid_candidate_from_trickle_ice' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'Found a valid candidate from trickle ICE, finishing gathering' ) ;
if ( sessionDescriptionHandler . iceGatheringCompleteResolve ) {
sessionDescriptionHandler . iceGatheringCompleteResolve ( ) ;
}
}
}
initSessionDescriptionHandler ( sessionDescriptionHandler ) {
/* eslint-disable no-param-reassign */
sessionDescriptionHandler . peerConnectionDelegate = {
onicecandidate :
this . onIceCandidate . bind ( this , sessionDescriptionHandler ) ,
onicegatheringstatechange :
this . onIceGatheringStateChange . bind ( this ) ,
} ;
/* eslint-enable no-param-reassign */
}
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
2021-06-30 01:47:59 +08:00
this . _sessionStartTime = new Date ( ) ;
2020-09-26 07:11:44 +08:00
const target = SIP . UserAgent . makeURI ( ` sip: ${ callExtension } @ ${ hostname } ` ) ;
2021-03-17 22:30:07 +08:00
const matchConstraints = this . getAudioConstraints ( ) ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
2020-09-26 07:11:44 +08:00
const inviterOptions = {
sessionDescriptionHandlerOptions : {
constraints : {
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
audio : isListenOnly
? false
2021-01-23 03:30:42 +08:00
: matchConstraints ,
2020-09-26 07:11:44 +08:00
video : false ,
} ,
2020-10-23 22:21:20 +08:00
iceGatheringTimeout : ICE _GATHERING _TIMEOUT ,
2017-10-18 03:16:42 +08:00
} ,
2021-06-30 01:47:59 +08:00
sessionDescriptionHandlerModifiersPostICEGathering : [
stripMDnsCandidates ,
filterValidIceCandidates . bind ( this , this . validIceCandidates ) ,
] ,
delegate : {
onSessionDescriptionHandler :
this . initSessionDescriptionHandler . bind ( this ) ,
} ,
2020-09-26 07:11:44 +08:00
} ;
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 ( ) ;
2020-10-01 21:16:48 +08:00
} ;
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' ) ;
2020-12-02 02:23:14 +08:00
this . callback ( { status : this . baseCallStates . started , bridge : BRIDGE _NAME } ) ;
2019-05-10 05:01:34 +08:00
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 ` ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
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 ` ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
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 ( {
2020-10-01 21:16:48 +08:00
type : this . baseErrorCodes . CONNECTION _ERROR ,
2020-09-26 07:11:44 +08:00
} ) ;
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 } ` ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2020-09-26 07:11:44 +08:00
} ) ;
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' ) ;
2020-11-20 01:33:01 +08:00
} else return ;
2020-09-26 07:11:44 +08:00
this . callback ( {
status : this . baseCallStates . failed ,
error : 1012 ,
bridgeError : 'ICE connection closed. Current state -'
+ ` ${ peer . iceConnectionState } ` ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2020-09-26 07:11:44 +08:00
} ) ;
} ;
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' ) ;
2021-06-30 01:47:59 +08:00
this . currentSession . sessionDescriptionHandler . peerConnectionDelegate
. onconnectionstatechange = ( event ) => {
2020-11-06 20:37:58 +08:00
const peer = event . target ;
logger . info ( {
2020-11-07 03:48:49 +08:00
logCode : 'sip_js_connection_state_change' ,
2020-11-06 20:37:58 +08:00
extraInfo : {
2020-11-07 03:48:49 +08:00
connectionStateChange : peer . connectionState ,
2020-11-06 20:37:58 +08:00
callerIdName : this . user . callerIdName ,
} ,
2020-11-07 03:48:49 +08:00
} , 'ICE connection state change - Current connection state - '
2021-04-02 02:11:13 +08:00
+ ` ${ peer . connectionState } ` ) ;
2020-11-07 03:48:49 +08:00
switch ( peer . connectionState ) {
case 'failed' :
// Chrome triggers 'failed' for connectionState event, only
handleIceNegotiationFailed ( peer ) ;
break ;
default :
break ;
}
2021-06-30 01:47:59 +08:00
} ;
this . currentSession . sessionDescriptionHandler . peerConnectionDelegate
. oniceconnectionstatechange = ( event ) => {
2020-09-26 07:11:44 +08:00
const peer = event . target ;
2020-11-07 03:48:49 +08:00
switch ( peer . iceConnectionState ) {
case 'completed' :
2020-09-26 07:11:44 +08:00
case 'connected' :
2020-10-23 22:16:09 +08:00
if ( iceCompleted ) {
logger . info ( {
logCode : 'sip_js_ice_connection_success_after_success' ,
extraInfo : {
currentState : peer . connectionState ,
callerIdName : this . user . callerIdName ,
} ,
} , 'ICE connection success, but user is already connected'
2021-04-02 02:11:13 +08:00
+ 'ignoring it...'
+ ` ${ peer . iceConnectionState } ` ) ;
2020-10-23 22:16:09 +08:00
return ;
}
2020-09-26 07:11:44 +08:00
logger . info ( {
logCode : 'sip_js_ice_connection_success' ,
extraInfo : {
currentState : peer . connectionState ,
callerIdName : this . user . callerIdName ,
} ,
2020-11-06 20:37:58 +08:00
} , 'ICE connection success. Current ICE Connection state - '
2021-04-02 02:11:13 +08:00
+ ` ${ peer . iceConnectionState } ` ) ;
2020-09-26 07:11:44 +08:00
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
iceCompleted = true ;
logSelectedCandidate ( peer , this . protocolIsIpv6 ) ;
checkIfCallReady ( ) ;
break ;
case 'failed' :
handleIceNegotiationFailed ( peer ) ;
break ;
case 'closed' :
handleIceConnectionTerminated ( peer ) ;
break ;
default :
break ;
}
2021-06-30 01:47:59 +08:00
} ;
2017-10-18 03:16:42 +08:00
} ;
2020-10-23 22:21:20 +08:00
const handleSessionTerminated = ( message ) => {
2019-02-21 05:58:37 +08:00
clearTimeout ( callTimeout ) ;
clearTimeout ( iceNegotiationTimeout ) ;
2019-06-13 05:01:20 +08:00
2020-10-23 22:21:20 +08:00
if ( ! message && ! ! this . userRequestedHangup ) {
2017-10-18 03:16:42 +08:00
return this . callback ( {
status : this . baseCallStates . ended ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2017-10-18 03:16:42 +08:00
} ) ;
}
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-02-01 07:15:29 +08:00
2019-02-21 05:58:37 +08:00
let mappedCause ;
2020-10-23 22:21:20 +08:00
let cause ;
2019-06-13 05:01:20 +08:00
if ( ! iceCompleted ) {
2019-02-21 05:58:37 +08:00
mappedCause = '1004' ;
2020-10-23 22:21:20 +08:00
cause = 'ICE error' ;
2019-02-21 05:58:37 +08:00
} else {
2020-10-23 22:21:20 +08:00
cause = 'Audio Conference Error' ;
2019-02-21 05:58:37 +08:00
mappedCause = '1005' ;
}
2017-10-20 18:11:51 +08:00
2020-10-23 22:21:20 +08:00
logger . error ( {
logCode : 'sip_js_call_terminated' ,
extraInfo : { cause , callerIdName : this . user . callerIdName } ,
} , ` Audio call terminated. cause= ${ cause } ` ) ;
2017-10-18 03:16:42 +08:00
return this . callback ( {
status : this . baseCallStates . failed ,
error : mappedCause ,
bridgeError : cause ,
2020-12-02 02:23:14 +08:00
bridge : BRIDGE _NAME ,
2017-10-18 03:16:42 +08:00
} ) ;
} ;
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
} ) ;
}
2021-01-23 03:30:42 +08:00
/ * *
* Filter constraints set in audioDeviceConstraints , based on
* constants supported by browser . This avoids setting a constraint
* unsupported by browser . In currently safari version ( 13 + ) , for example ,
* setting an unsupported constraint crashes the audio .
* @ param { Object } audioDeviceConstraints Constraints to be set
* see : https : //developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
* @ return { Object } A new Object of the same type as
* input , containing only the supported constraints .
* /
filterSupportedConstraints ( audioDeviceConstraints ) {
try {
const matchConstraints = { } ;
const supportedConstraints = navigator
. mediaDevices . getSupportedConstraints ( ) || { } ;
Object . entries ( audioDeviceConstraints ) . forEach (
( [ constraintName , constraintValue ] ) => {
if ( supportedConstraints [ constraintName ] ) {
matchConstraints [ constraintName ] = constraintValue ;
}
}
) ;
return matchConstraints ;
} catch ( error ) {
logger . error ( {
logCode : 'sipjs_unsupported_audio_constraint_error' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'SIP.js unsupported constraint error' ) ;
return { } ;
}
}
/ * *
* Update audio constraints for current local MediaStream ( microphone )
* @ param { Object } constraints MediaTrackConstraints object . See :
* https : //developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
* @ return { Promise } A Promise for this process
* /
async updateAudioConstraints ( constraints ) {
try {
2021-01-26 10:45:27 +08:00
if ( typeof constraints !== 'object' ) return ;
2021-01-23 03:30:42 +08:00
logger . info ( {
logCode : 'sipjs_update_audio_constraint' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'SIP.js updating audio constraint' ) ;
const matchConstraints = this . filterSupportedConstraints ( constraints ) ;
//Chromium bug - see: https://bugs.chromium.org/p/chromium/issues/detail?id=796964&q=applyConstraints&can=2
2021-04-01 19:14:24 +08:00
const { isChrome } = browserInfo ;
if ( isChrome ) {
2021-01-23 03:30:42 +08:00
matchConstraints . deviceId = this . inputDeviceId ;
const stream = await navigator . mediaDevices . getUserMedia (
{ audio : matchConstraints } ,
) ;
this . currentSession . sessionDescriptionHandler
. setLocalMediaStream ( stream ) ;
} else {
const { localMediaStream } = this . currentSession
. sessionDescriptionHandler ;
localMediaStream . getAudioTracks ( ) . forEach (
track => track . applyConstraints ( matchConstraints ) ,
) ;
}
} catch ( error ) {
logger . error ( {
logCode : 'sipjs_audio_constraint_error' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'SIP.js failed to update audio constraint' ) ;
}
}
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
}
2020-12-02 04:02:50 +08:00
get inputDeviceId ( ) {
2021-03-17 22:30:07 +08:00
const sessionInputDeviceId = Storage . getItem ( INPUT _DEVICE _ID _KEY ) ;
if ( sessionInputDeviceId ) {
return sessionInputDeviceId ;
}
if ( this . media . inputDeviceId ) {
return this . media . inputDeviceId ;
}
if ( this . activeSession ) {
return this . activeSession . inputDeviceId ;
}
return DEFAULT _INPUT _DEVICE _ID ;
}
set inputDeviceId ( deviceId ) {
Storage . setItem ( INPUT _DEVICE _ID _KEY , deviceId ) ;
this . media . inputDeviceId = deviceId ;
if ( this . activeSession ) {
this . activeSession . inputDeviceId = deviceId ;
}
}
get outputDeviceId ( ) {
const sessionOutputDeviceId = Storage . getItem ( OUTPUT _DEVICE _ID _KEY ) ;
if ( sessionOutputDeviceId ) {
return sessionOutputDeviceId ;
}
if ( this . media . outputDeviceId ) {
return this . media . outputDeviceId ;
}
if ( this . activeSession ) {
return this . activeSession . outputDeviceId ;
}
return DEFAULT _OUTPUT _DEVICE _ID ;
}
set outputDeviceId ( deviceId ) {
Storage . setItem ( OUTPUT _DEVICE _ID _KEY , deviceId ) ;
this . media . outputDeviceId = deviceId ;
if ( this . activeSession ) {
this . activeSession . outputDeviceId = deviceId ;
}
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
}
2021-01-30 06:05:51 +08:00
get inputStream ( ) {
return this . activeSession ? this . activeSession . inputStream : null ;
}
2021-06-30 01:47:59 +08:00
joinAudio ( { isListenOnly , extension , validIceCandidates } , managerCallback ) {
2019-06-13 05:01:20 +08:00
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
2021-04-02 02:11:13 +08:00
this . activeSession . exitAudio ( ) . catch ( ( ) => { } ) ;
2019-06-13 05:01:20 +08:00
if ( this . activeSession . webrtcConnected ) {
// webrtc was able to connect so just try again
message . silenceNotifications = true ;
2020-12-02 02:23:14 +08:00
callback ( { status : this . baseCallStates . reconnecting , bridge : BRIDGE _NAME , } ) ;
2019-06-13 05:01:20 +08:00
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 ) ;
2021-04-23 02:03:43 +08:00
const { inputDeviceId , outputDeviceId } = this ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
this . activeSession . joinAudio ( {
isListenOnly ,
extension : fallbackExtension ,
inputDeviceId ,
2021-04-23 02:03:43 +08:00
outputDeviceId ,
2021-06-30 01:47:59 +08:00
validIceCandidates ,
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
} , callback )
2019-06-13 05:01:20 +08:00
. then ( ( value ) => {
2021-04-23 02:03:43 +08:00
this . changeOutputDevice ( outputDeviceId , true ) ;
2019-06-13 05:01:20 +08:00
resolve ( value ) ;
} ) . catch ( ( reason ) => {
reject ( reason ) ;
} ) ;
}
}
return managerCallback ( message ) ;
} ;
2021-04-23 02:03:43 +08:00
const { inputDeviceId , outputDeviceId } = this ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
this . activeSession . joinAudio ( {
isListenOnly ,
extension ,
inputDeviceId ,
2021-04-23 02:03:43 +08:00
outputDeviceId ,
2021-06-30 01:47:59 +08:00
validIceCandidates ,
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
} , callback )
2019-06-13 05:01:20 +08:00
. then ( ( value ) => {
2021-04-23 02:03:43 +08:00
this . changeOutputDevice ( outputDeviceId , true ) ;
2019-06-13 05:01:20 +08:00
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 ( ) {
2021-03-17 22:30:07 +08:00
this . inputDeviceId = DEFAULT _INPUT _DEVICE _ID ;
2017-10-27 01:14:56 +08:00
}
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
async changeInputDeviceId ( inputDeviceId ) {
if ( ! inputDeviceId ) {
throw new Error ( ) ;
2017-10-18 03:16:42 +08:00
}
2017-10-12 05:04:10 +08:00
2021-03-17 22:30:07 +08:00
this . inputDeviceId = inputDeviceId ;
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
return inputDeviceId ;
2017-10-18 03:16:42 +08:00
}
2020-07-28 03:49:26 +08:00
liveChangeInputDevice ( deviceId ) {
2021-03-17 22:30:07 +08:00
this . inputDeviceId = deviceId ;
return this . activeSession . liveChangeInputDevice ( deviceId ) ;
2020-07-28 03:49:26 +08:00
}
2021-04-23 02:03:43 +08:00
reloadAudioElement ( audioElement ) {
if ( audioElement && ( audioElement . readyState > 0 ) ) {
logger . debug ( {
logCode : 'sip_js_reload_audio_element' ,
extraInfo : {
callerIdName : this . user . callerIdName ,
} ,
} , 'Reloading audio element after changing output device' ) ;
audioElement . load ( ) ;
}
}
2021-03-17 22:30:07 +08:00
async changeOutputDevice ( value , isLive ) {
2021-04-23 02:03:43 +08:00
const audioElement = document . querySelector ( MEDIA _TAG ) ;
2017-10-18 03:16:42 +08:00
2021-04-23 02:03:43 +08:00
if ( audioElement . setSinkId ) {
2017-10-27 01:14:56 +08:00
try {
2021-03-17 22:30:07 +08:00
if ( ! isLive ) {
2021-04-23 02:03:43 +08:00
audioElement . srcObject = null ;
2021-03-17 22:30:07 +08:00
}
2021-04-23 02:03:43 +08:00
await audioElement . setSinkId ( value ) ;
this . reloadAudioElement ( audioElement ) ;
2021-03-17 22:30:07 +08:00
this . outputDeviceId = value ;
2017-10-27 01:14:56 +08:00
} 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
2021-03-17 22:30:07 +08:00
return this . outputDeviceId ;
2017-07-24 22:15:46 +08:00
}
2021-01-23 03:30:42 +08:00
async updateAudioConstraints ( constraints ) {
return this . activeSession . updateAudioConstraints ( constraints ) ;
}
2017-07-24 22:15:46 +08:00
}