2017-09-20 01:47:57 +08:00
import { Tracker } from 'meteor/tracker' ;
2018-05-02 23:49:57 +08:00
import KurentoBridge from '/imports/api/audio/client/bridge/kurento' ;
2020-12-02 02:24:24 +08:00
2017-11-17 19:52:48 +08:00
import Auth from '/imports/ui/services/auth' ;
2017-11-18 03:01:52 +08:00
import VoiceUsers from '/imports/api/voice-users' ;
2017-10-13 03:22:10 +08:00
import SIPBridge from '/imports/api/audio/client/bridge/sip' ;
2018-07-12 06:03:56 +08:00
import logger from '/imports/startup/client/logger' ;
2017-10-24 21:19:58 +08:00
import { notify } from '/imports/ui/services/notification' ;
2019-09-30 22:54:34 +08:00
import playAndRetry from '/imports/utils/mediaElementPlayRetry' ;
2019-11-15 03:07:35 +08:00
import iosWebviewAudioPolyfills from '/imports/utils/ios-webview-audio-polyfills' ;
import { tryGenerateIceCandidates } from '/imports/utils/safari-webrtc' ;
2019-11-30 05:48:04 +08:00
import { monitorAudioConnection } from '/imports/utils/stats' ;
2019-09-30 22:54:34 +08:00
import AudioErrors from './error-codes' ;
2020-12-01 00:09:35 +08:00
import { Meteor } from "meteor/meteor" ;
2017-09-20 01:47:57 +08:00
2020-01-28 21:07:21 +08:00
const STATS = Meteor . settings . public . stats ;
2017-10-18 03:16:42 +08:00
const MEDIA = Meteor . settings . public . media ;
2018-08-30 03:12:34 +08:00
const MEDIA _TAG = MEDIA . mediaTag ;
2017-10-18 03:16:42 +08:00
const ECHO _TEST _NUMBER = MEDIA . echoTestNumber ;
2018-09-12 01:09:29 +08:00
const MAX _LISTEN _ONLY _RETRIES = 1 ;
2020-09-26 07:11:44 +08:00
const LISTEN _ONLY _CALL _TIMEOUT _MS = MEDIA . listenOnlyCallTimeout || 25000 ;
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
const DEFAULT _INPUT _DEVICE _ID = 'default' ;
const DEFAULT _OUTPUT _DEVICE _ID = 'default' ;
2017-10-10 04:48:10 +08:00
const CALL _STATES = {
2017-10-05 04:49:11 +08:00
STARTED : 'started' ,
ENDED : 'ended' ,
FAILED : 'failed' ,
2019-06-13 05:01:20 +08:00
RECONNECTING : 'reconnecting' ,
2019-08-03 05:32:42 +08:00
AUTOPLAY _BLOCKED : 'autoplayBlocked' ,
2017-10-05 04:49:11 +08:00
} ;
2017-09-20 01:47:57 +08:00
class AudioManager {
constructor ( ) {
2017-09-30 04:42:34 +08:00
this . _inputDevice = {
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
value : DEFAULT _INPUT _DEVICE _ID ,
2017-10-11 02:03:29 +08:00
tracker : new Tracker . Dependency ( ) ,
2017-09-30 04:42:34 +08:00
} ;
2017-09-20 01:47:57 +08:00
this . defineProperties ( {
isMuted : false ,
isConnected : false ,
isConnecting : false ,
2017-10-27 01:14:56 +08:00
isHangingUp : false ,
2017-09-29 21:38:10 +08:00
isListenOnly : false ,
isEchoTest : false ,
2018-06-20 23:36:26 +08:00
isTalking : false ,
2017-11-09 02:41:15 +08:00
isWaitingPermissions : false ,
2017-09-20 01:47:57 +08:00
error : null ,
2017-10-10 04:48:10 +08:00
outputDeviceId : null ,
2018-01-16 23:56:31 +08:00
muteHandle : null ,
2019-08-03 05:32:42 +08:00
autoplayBlocked : false ,
2017-09-20 01:47:57 +08:00
} ) ;
2018-09-12 01:09:29 +08:00
this . useKurento = Meteor . settings . public . kurento . enableListenOnly ;
2019-08-03 05:32:42 +08:00
this . failedMediaElements = [ ] ;
this . handlePlayElementFailed = this . handlePlayElementFailed . bind ( this ) ;
2019-11-30 05:48:04 +08:00
this . monitor = this . monitor . bind ( this ) ;
2017-09-20 01:47:57 +08:00
}
2018-04-20 03:57:54 +08:00
init ( userData ) {
2019-03-09 03:41:19 +08:00
this . bridge = new SIPBridge ( userData ) ; // no alternative as of 2019-03-08
2018-09-12 01:09:29 +08:00
if ( this . useKurento ) {
2018-05-02 23:49:57 +08:00
this . listenOnlyBridge = new KurentoBridge ( userData ) ;
}
2017-10-18 03:16:42 +08:00
this . userData = userData ;
2017-11-18 03:01:52 +08:00
this . initialized = true ;
2017-10-18 03:16:42 +08:00
}
2018-12-22 01:14:05 +08:00
2019-02-21 05:58:37 +08:00
setAudioMessages ( messages , intl ) {
2018-04-20 03:57:54 +08:00
this . messages = messages ;
2019-02-21 05:58:37 +08:00
this . intl = intl ;
2018-04-20 03:57:54 +08:00
}
2017-10-18 03:16:42 +08:00
2017-09-20 01:47:57 +08:00
defineProperties ( obj ) {
2017-09-29 21:38:10 +08:00
Object . keys ( obj ) . forEach ( ( key ) => {
const privateKey = ` _ ${ key } ` ;
this [ privateKey ] = {
2017-09-20 01:47:57 +08:00
value : obj [ key ] ,
2017-10-11 02:03:29 +08:00
tracker : new Tracker . Dependency ( ) ,
2017-09-29 21:38:10 +08:00
} ;
2017-09-20 01:47:57 +08:00
Object . defineProperty ( this , key , {
set : ( value ) => {
2017-09-29 21:38:10 +08:00
this [ privateKey ] . value = value ;
this [ privateKey ] . tracker . changed ( ) ;
2017-09-20 01:47:57 +08:00
} ,
get : ( ) => {
2017-09-29 21:38:10 +08:00
this [ privateKey ] . tracker . depend ( ) ;
return this [ privateKey ] . value ;
} ,
} ) ;
} ) ;
2017-09-20 01:47:57 +08:00
}
2018-03-16 02:57:25 +08:00
joinMicrophone ( ) {
this . isListenOnly = false ;
this . isEchoTest = false ;
2020-10-15 05:12:09 +08:00
return this . onAudioJoining . bind ( this ) ( )
2019-06-13 05:01:20 +08:00
. then ( ( ) => {
const callOptions = {
isListenOnly : false ,
extension : null ,
inputStream : this . inputStream ,
} ;
2020-09-26 07:11:44 +08:00
return this . joinAudio ( callOptions , this . callStateCallback . bind ( this ) ) ;
2019-06-13 05:01:20 +08:00
} ) ;
2018-03-16 02:57:25 +08:00
}
joinEchoTest ( ) {
this . isListenOnly = false ;
this . isEchoTest = true ;
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 this . onAudioJoining . bind ( this ) ( )
2019-06-13 05:01:20 +08:00
. then ( ( ) => {
const callOptions = {
isListenOnly : false ,
extension : ECHO _TEST _NUMBER ,
inputStream : this . inputStream ,
} ;
2019-07-26 02:41:24 +08:00
logger . info ( { logCode : 'audiomanager_join_echotest' , extraInfo : { logType : 'user_action' } } , 'User requested to join audio conference with mic' ) ;
2020-09-26 07:11:44 +08:00
return this . joinAudio ( callOptions , this . callStateCallback . bind ( this ) ) ;
2019-06-13 05:01:20 +08:00
} ) ;
2018-03-16 02:57:25 +08:00
}
2020-09-26 07:11:44 +08:00
joinAudio ( callOptions , callStateCallback ) {
return this . bridge . joinAudio ( callOptions ,
callStateCallback . bind ( this ) ) . catch ( ( error ) => {
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
const { name } = error ;
2020-09-26 07:11:44 +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
if ( ! name ) {
2020-09-26 07:11:44 +08:00
throw error ;
}
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
switch ( name ) {
case 'NotAllowedError' :
logger . error ( {
logCode : 'audiomanager_error_getting_device' ,
extraInfo : {
errorName : error . name ,
errorMessage : error . message ,
} ,
} , ` Error getting microphone - { ${ error . name } : ${ error . message } } ` ) ;
break ;
case 'NotFoundError' :
logger . error ( {
logCode : 'audiomanager_error_device_not_found' ,
extraInfo : {
errorName : error . name ,
errorMessage : error . message ,
} ,
} , ` Error getting microphone - { ${ error . name } : ${ error . message } } ` ) ;
break ;
default :
break ;
}
this . isConnecting = false ;
this . isWaitingPermissions = false ;
throw {
type : 'MEDIA_ERROR' ,
} ;
2020-09-26 07:11:44 +08:00
} ) ;
}
2019-07-26 02:41:24 +08:00
async joinListenOnly ( r = 0 ) {
let retries = r ;
2018-03-16 02:57:25 +08:00
this . isListenOnly = true ;
this . isEchoTest = false ;
2019-06-13 05:01:20 +08:00
2018-05-02 23:49:57 +08:00
// The kurento bridge isn't a full audio bridge yet, so we have to differ it
2018-11-14 01:14:30 +08:00
const bridge = this . useKurento ? this . listenOnlyBridge : this . bridge ;
2018-03-16 02:57:25 +08:00
const callOptions = {
isListenOnly : true ,
extension : null ,
inputStream : this . createListenOnlyStream ( ) ,
} ;
2019-11-15 00:35:56 +08:00
// WebRTC restrictions may need a capture device permission to release
// useful ICE candidates on recvonly/no-gUM peers
try {
await tryGenerateIceCandidates ( ) ;
} catch ( error ) {
logger . error ( {
logCode : 'listenonly_no_valid_candidate_gum_failure' ,
extraInfo : {
errorName : error . name ,
errorMessage : error . message ,
2019-11-15 03:07:35 +08:00
} ,
2019-11-15 00:35:56 +08:00
} , ` Forced gUM to release additional ICE candidates failed due to ${ error . name } . ` ) ;
2018-10-04 02:39:55 +08:00
}
2018-12-22 01:14:05 +08:00
// Call polyfills for webrtc client if navigator is "iOS Webview"
const userAgent = window . navigator . userAgent . toLocaleLowerCase ( ) ;
if ( ( userAgent . indexOf ( 'iphone' ) > - 1 || userAgent . indexOf ( 'ipad' ) > - 1 )
2019-02-21 05:58:37 +08:00
&& userAgent . indexOf ( 'safari' ) === - 1 ) {
2018-12-22 01:14:05 +08:00
iosWebviewAudioPolyfills ( ) ;
}
2018-03-16 02:57:25 +08:00
// We need this until we upgrade to SIP 9x. See #4690
2019-12-03 06:15:46 +08:00
const listenOnlyCallTimeoutErr = this . useKurento ? 'KURENTO_CALL_TIMEOUT' : 'SIP_CALL_TIMEOUT' ;
2018-03-16 02:57:25 +08:00
const iceGatheringTimeout = new Promise ( ( resolve , reject ) => {
2019-12-03 06:15:46 +08:00
setTimeout ( reject , LISTEN _ONLY _CALL _TIMEOUT _MS , listenOnlyCallTimeoutErr ) ;
2018-03-16 02:57:25 +08:00
} ) ;
2019-12-03 06:15:46 +08:00
const exitKurentoAudio = ( ) => {
if ( this . useKurento ) {
2020-12-02 02:24:24 +08:00
bridge . exitAudio ( ) ;
2019-12-03 06:15:46 +08:00
const audio = document . querySelector ( MEDIA _TAG ) ;
audio . muted = false ;
}
} ;
2019-07-26 02:41:24 +08:00
2019-12-03 06:15:46 +08:00
const handleListenOnlyError = ( err ) => {
2018-09-12 01:09:29 +08:00
if ( iceGatheringTimeout ) {
clearTimeout ( iceGatheringTimeout ) ;
}
2019-12-03 06:15:46 +08:00
const errorReason = ( typeof err === 'string' ? err : undefined ) || err . errorReason || err . errorMessage ;
const bridgeInUse = ( this . useKurento ? 'Kurento' : 'SIP' ) ;
2019-06-29 05:45:50 +08:00
logger . error ( {
logCode : 'audiomanager_listenonly_error' ,
extraInfo : {
2019-12-03 06:15:46 +08:00
errorReason ,
audioBridge : bridgeInUse ,
2019-06-29 05:45:50 +08:00
retries ,
} ,
2020-12-02 02:24:24 +08:00
} , ` Listen only error - ${ errorReason } - bridge: ${ bridgeInUse } ` ) ;
2018-11-14 01:14:30 +08:00
} ;
2018-09-12 01:09:29 +08:00
2019-07-26 02:41:24 +08:00
logger . info ( { logCode : 'audiomanager_join_listenonly' , extraInfo : { logType : 'user_action' } } , 'user requested to connect to audio conference as listen only' ) ;
2019-08-03 05:32:42 +08:00
window . addEventListener ( 'audioPlayFailed' , this . handlePlayElementFailed ) ;
2018-03-16 02:57:25 +08:00
return this . onAudioJoining ( )
. then ( ( ) => Promise . race ( [
2018-05-02 23:49:57 +08:00
bridge . joinAudio ( callOptions , this . callStateCallback . bind ( this ) ) ,
2018-03-16 02:57:25 +08:00
iceGatheringTimeout ,
] ) )
2018-09-12 01:09:29 +08:00
. catch ( async ( err ) => {
2019-12-03 06:15:46 +08:00
handleListenOnlyError ( err ) ;
2018-10-04 02:39:55 +08:00
if ( retries < MAX _LISTEN _ONLY _RETRIES ) {
2018-09-12 01:09:29 +08:00
// Fallback to SIP.js listen only in case of failure
if ( this . useKurento ) {
2019-12-03 06:15:46 +08:00
exitKurentoAudio ( ) ;
2018-09-12 01:09:29 +08:00
this . useKurento = false ;
2018-10-04 02:39:55 +08:00
2019-12-03 06:15:46 +08:00
const errorReason = ( typeof err === 'string' ? err : undefined ) || err . errorReason || err . errorMessage ;
logger . info ( {
logCode : 'audiomanager_listenonly_fallback' ,
extraInfo : {
logType : 'fallback' ,
errorReason ,
} ,
} , ` Falling back to FreeSWITCH listenOnly - cause: ${ errorReason } ` ) ;
2018-09-12 01:09:29 +08:00
}
2019-12-03 06:15:46 +08:00
retries += 1 ;
this . joinListenOnly ( retries ) ;
2018-03-16 02:57:25 +08:00
}
2019-07-26 02:41:24 +08:00
return null ;
2017-11-02 20:10:01 +08:00
} ) ;
2017-09-20 01:47:57 +08:00
}
2018-03-16 02:57:25 +08:00
onAudioJoining ( ) {
this . isConnecting = true ;
this . isMuted = false ;
this . error = false ;
return Promise . resolve ( ) ;
}
2017-09-20 01:47:57 +08:00
exitAudio ( ) {
2017-11-02 20:10:01 +08:00
if ( ! this . isConnected ) return Promise . resolve ( ) ;
2018-11-14 01:14:30 +08:00
const bridge = ( this . useKurento && this . isListenOnly ) ? this . listenOnlyBridge : this . bridge ;
2018-05-02 23:49:57 +08:00
2017-10-27 01:14:56 +08:00
this . isHangingUp = true ;
2018-06-28 01:44:11 +08:00
2018-05-02 23:49:57 +08:00
return bridge . exitAudio ( ) ;
2017-09-20 01:47:57 +08:00
}
2017-10-12 20:50:23 +08:00
transferCall ( ) {
2017-10-18 03:16:42 +08:00
this . onTransferStart ( ) ;
return this . bridge . transferCall ( this . onAudioJoin . bind ( this ) ) ;
2017-10-12 20:50:23 +08:00
}
2020-08-27 05:23:01 +08:00
onVoiceUserChanges ( fields ) {
if ( fields . muted !== undefined && fields . muted !== this . isMuted ) {
2020-09-10 01:03:27 +08:00
let muteState ;
2020-08-27 05:23:01 +08:00
this . isMuted = fields . muted ;
2020-09-10 01:03:27 +08:00
if ( this . isMuted ) {
muteState = 'selfMuted' ;
this . mute ( ) ;
} else {
muteState = 'selfUnmuted' ;
this . unmute ( ) ;
}
2020-08-27 05:23:01 +08:00
window . parent . postMessage ( { response : muteState } , '*' ) ;
}
if ( fields . talking !== undefined && fields . talking !== this . isTalking ) {
this . isTalking = fields . talking ;
}
if ( this . isMuted ) {
this . isTalking = false ;
}
}
2017-09-20 01:47:57 +08:00
onAudioJoin ( ) {
this . isConnecting = false ;
2017-10-19 03:40:01 +08:00
this . isConnected = true ;
2017-10-20 18:11:51 +08:00
2018-01-16 05:01:57 +08:00
// listen to the VoiceUsers changes and update the flag
2018-03-16 02:57:25 +08:00
if ( ! this . muteHandle ) {
2019-08-22 20:05:06 +08:00
const query = VoiceUsers . find ( { intId : Auth . userID } , { fields : { muted : 1 , talking : 1 } } ) ;
2018-01-16 05:13:18 +08:00
this . muteHandle = query . observeChanges ( {
2020-08-27 05:23:01 +08:00
added : ( id , fields ) => this . onVoiceUserChanges ( fields ) ,
changed : ( id , fields ) => this . onVoiceUserChanges ( fields ) ,
2018-01-16 05:01:57 +08:00
} ) ;
}
2017-10-20 18:11:51 +08:00
if ( ! this . isEchoTest ) {
2018-11-29 05:42:24 +08:00
window . parent . postMessage ( { response : 'joinedAudio' } , '*' ) ;
2019-02-21 05:58:37 +08:00
this . notify ( this . intl . formatMessage ( this . messages . info . JOINED _AUDIO ) ) ;
logger . info ( { logCode : 'audio_joined' } , 'Audio Joined' ) ;
2020-01-28 21:07:21 +08:00
if ( STATS . enabled ) this . monitor ( ) ;
2017-10-20 18:11:51 +08:00
}
2017-09-20 01:47:57 +08:00
}
2017-10-12 20:50:23 +08:00
onTransferStart ( ) {
this . isEchoTest = false ;
this . isConnecting = true ;
}
2017-09-20 01:47:57 +08:00
onAudioExit ( ) {
this . isConnected = false ;
2017-10-05 04:49:11 +08:00
this . isConnecting = false ;
2017-10-27 01:14:56 +08:00
this . isHangingUp = false ;
2019-08-03 05:32:42 +08:00
this . autoplayBlocked = false ;
this . failedMediaElements = [ ] ;
2017-09-29 21:38:10 +08:00
2018-04-18 01:09:05 +08:00
if ( this . inputStream ) {
2018-04-18 22:15:17 +08:00
window . defaultInputStream . forEach ( track => track . stop ( ) ) ;
2018-04-18 01:09:05 +08:00
this . inputStream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
this . inputDevice = { id : 'default' } ;
}
2018-04-13 20:39:26 +08:00
2017-10-20 18:11:51 +08:00
if ( ! this . error && ! this . isEchoTest ) {
2019-04-12 06:53:57 +08:00
this . notify ( this . intl . formatMessage ( this . messages . info . LEFT _AUDIO ) , false , 'audio_off' ) ;
2017-09-29 21:38:10 +08:00
}
2019-06-13 05:01:20 +08:00
if ( ! this . isEchoTest ) {
this . playHangUpSound ( ) ;
}
2018-11-29 05:42:24 +08:00
window . parent . postMessage ( { response : 'notInAudio' } , '*' ) ;
2019-08-03 05:32:42 +08:00
window . removeEventListener ( 'audioPlayFailed' , this . handlePlayElementFailed ) ;
2017-09-20 01:47:57 +08:00
}
2017-10-05 04:49:11 +08:00
callStateCallback ( response ) {
2017-09-29 21:38:10 +08:00
return new Promise ( ( resolve ) => {
const {
2017-10-05 04:49:11 +08:00
STARTED ,
ENDED ,
FAILED ,
2019-06-13 05:01:20 +08:00
RECONNECTING ,
2019-08-03 05:32:42 +08:00
AUTOPLAY _BLOCKED ,
2017-10-05 04:49:11 +08:00
} = CALL _STATES ;
2017-09-29 21:38:10 +08:00
2017-10-05 04:49:11 +08:00
const {
status ,
error ,
2017-10-23 20:41:09 +08:00
bridgeError ,
2019-06-13 05:01:20 +08:00
silenceNotifications ,
2020-12-02 02:24:24 +08:00
bridge ,
2017-10-05 04:49:11 +08:00
} = response ;
if ( status === STARTED ) {
2017-09-29 21:38:10 +08:00
this . onAudioJoin ( ) ;
2017-10-05 04:49:11 +08:00
resolve ( STARTED ) ;
} else if ( status === ENDED ) {
2019-06-05 02:16:43 +08:00
logger . info ( { logCode : 'audio_ended' } , 'Audio ended without issue' ) ;
2017-09-29 21:38:10 +08:00
this . onAudioExit ( ) ;
2017-10-05 04:49:11 +08:00
} else if ( status === FAILED ) {
2019-02-21 05:58:37 +08:00
const errorKey = this . messages . error [ error ] || this . messages . error . GENERIC _ERROR ;
const errorMsg = this . intl . formatMessage ( errorKey , { 0 : bridgeError } ) ;
this . error = ! ! error ;
2019-06-29 05:45:50 +08:00
logger . error ( {
logCode : 'audio_failure' ,
extraInfo : {
2019-09-05 02:16:14 +08:00
errorCode : error ,
2019-06-29 05:45:50 +08:00
cause : bridgeError ,
2020-12-02 02:24:24 +08:00
bridge ,
2019-06-29 05:45:50 +08:00
} ,
2019-12-03 06:15:46 +08:00
} , ` Audio error - errorCode= ${ error } , cause= ${ bridgeError } ` ) ;
2019-06-13 05:01:20 +08:00
if ( silenceNotifications !== true ) {
this . notify ( errorMsg , true ) ;
this . exitAudio ( ) ;
this . onAudioExit ( ) ;
}
} else if ( status === RECONNECTING ) {
logger . info ( { logCode : 'audio_reconnecting' } , 'Attempting to reconnect audio' ) ;
this . notify ( this . intl . formatMessage ( this . messages . info . RECONNECTING _AUDIO ) , true ) ;
this . playHangUpSound ( ) ;
2019-08-03 05:32:42 +08:00
} else if ( status === AUTOPLAY _BLOCKED ) {
this . autoplayBlocked = true ;
this . onAudioJoin ( ) ;
resolve ( AUTOPLAY _BLOCKED ) ;
2017-09-29 21:38:10 +08:00
}
2017-10-11 20:05:57 +08:00
} ) ;
2017-09-29 21:38:10 +08:00
}
2017-10-05 04:49:11 +08:00
createListenOnlyStream ( ) {
2018-11-14 01:14:30 +08:00
const audio = document . querySelector ( MEDIA _TAG ) ;
2018-08-30 03:12:34 +08:00
// Play bogus silent audio to try to circumvent autoplay policy on Safari
2020-09-26 07:11:44 +08:00
if ( ! audio . src ) {
2020-12-01 00:09:35 +08:00
audio . src = ` ${ Meteor . settings . public . app . cdn
2021-01-06 23:55:01 +08:00
+ Meteor . settings . public . app . basename + Meteor . settings . public . app . instanceId } ` + '/resources/sounds/silence.mp3';
2020-09-26 07:11:44 +08:00
}
2018-08-30 03:12:34 +08:00
2018-11-14 01:14:30 +08:00
audio . play ( ) . catch ( ( e ) => {
2020-09-26 07:11:44 +08:00
if ( e . name === 'AbortError' ) {
return ;
}
2019-07-03 00:54:10 +08:00
logger . warn ( {
2019-06-29 05:45:50 +08:00
logCode : 'audiomanager_error_test_audio' ,
extraInfo : { error : e } ,
} , 'Error on playing test audio' ) ;
2018-08-30 03:12:34 +08:00
} ) ;
2018-08-30 03:59:33 +08:00
2020-09-26 07:11:44 +08:00
return { } ;
2017-10-05 04:49:11 +08:00
}
2018-06-20 23:36:26 +08:00
isUsingAudio ( ) {
2018-12-22 01:14:05 +08:00
return this . isConnected || this . isConnecting
|| this . isHangingUp || this . isEchoTest ;
2018-06-20 23:36:26 +08:00
}
2017-10-27 01:14:56 +08:00
setDefaultInputDevice ( ) {
2017-11-02 20:10:01 +08:00
return this . changeInputDevice ( ) ;
2017-10-27 01:14:56 +08:00
}
2017-11-02 20:10:01 +08:00
setDefaultOutputDevice ( ) {
return this . changeOutputDevice ( 'default' ) ;
}
changeInputDevice ( 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
if ( ! deviceId ) {
return Promise . resolve ( ) ;
}
const handleChangeInputDeviceSuccess = ( inputDeviceId ) => {
this . inputDevice . id = inputDeviceId ;
return Promise . resolve ( inputDeviceId ) ;
2017-11-02 20:10:01 +08:00
} ;
2019-09-07 00:35:55 +08:00
const handleChangeInputDeviceError = ( error ) => {
logger . error ( {
logCode : 'audiomanager_error_getting_device' ,
extraInfo : {
errorName : error . name ,
errorMessage : error . message ,
} ,
} , ` Error getting microphone - { ${ error . name } : ${ error . message } } ` ) ;
2019-09-27 21:52:29 +08:00
2019-09-30 22:54:34 +08:00
const { MIC _ERROR } = AudioErrors ;
2019-09-27 21:52:29 +08:00
const disabledSysSetting = error . message . includes ( 'Permission denied by system' ) ;
2019-09-30 22:54:34 +08:00
const isMac = navigator . platform . indexOf ( 'Mac' ) !== - 1 ;
2019-09-27 21:52:29 +08:00
2019-09-30 22:54:34 +08:00
let code = MIC _ERROR . NO _PERMISSION ;
if ( isMac && disabledSysSetting ) code = MIC _ERROR . MAC _OS _BLOCK ;
2019-09-27 21:52:29 +08:00
2019-09-07 00:35:55 +08:00
return Promise . reject ( {
2019-07-26 02:41:24 +08:00
type : 'MEDIA_ERROR' ,
message : this . messages . error . MEDIA _ERROR ,
2019-09-27 21:52:29 +08:00
code ,
2019-07-26 02:41:24 +08:00
} ) ;
2019-09-07 00:35:55 +08:00
} ;
2017-11-02 20:10:01 +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
return this . bridge . changeInputDeviceId ( deviceId )
2017-11-18 03:01:52 +08:00
. then ( handleChangeInputDeviceSuccess )
. catch ( handleChangeInputDeviceError ) ;
2017-10-18 03:16:42 +08:00
}
2017-09-30 04:42:34 +08:00
2017-10-18 03:16:42 +08:00
async changeOutputDevice ( 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
this . outputDeviceId = await this
. bridge
. changeOutputDevice ( deviceId || DEFAULT _OUTPUT _DEVICE _ID ) ;
2017-09-30 04:42:34 +08:00
}
2017-10-18 03:16:42 +08:00
set inputDevice ( value ) {
2018-04-13 20:39:26 +08:00
this . _inputDevice . value = value ;
2017-10-18 03:16:42 +08:00
this . _inputDevice . tracker . changed ( ) ;
2017-10-10 04:48:10 +08:00
}
2017-10-11 02:03:29 +08:00
get inputStream ( ) {
2018-04-13 20:39:26 +08:00
this . _inputDevice . tracker . depend ( ) ;
return this . _inputDevice . value . stream ;
2017-09-30 04:42:34 +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
get inputDevice ( ) {
return this . _inputDevice ;
}
2017-10-11 02:03:29 +08:00
get 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 ( this . bridge && this . bridge . inputDeviceId )
? this . bridge . inputDeviceId : DEFAULT _INPUT _DEVICE _ID ;
2017-09-30 04:42:34 +08:00
}
2017-10-18 03:16:42 +08:00
set userData ( value ) {
this . _userData = value ;
}
get userData ( ) {
return this . _userData ;
}
2017-10-23 20:41:09 +08:00
2019-06-13 05:01:20 +08:00
playHangUpSound ( ) {
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 . playAlertSound ( ` ${ Meteor . settings . public . app . cdn
2020-12-01 00:09:35 +08:00
+ Meteor . settings . public . app . basename + Meteor . settings . public . app . instanceId } `
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
+ '/resources/sounds/LeftCall.mp3' ) ;
2019-06-13 05:01:20 +08:00
}
2019-04-12 21:55:14 +08:00
notify ( message , error = false , icon = 'unmute' ) {
const audioIcon = this . isListenOnly ? 'listen' : icon ;
2019-04-12 06:53:57 +08:00
2017-11-18 03:01:52 +08:00
notify (
message ,
2018-03-16 02:57:25 +08:00
error ? 'error' : 'info' ,
2019-04-12 21:55:14 +08:00
audioIcon ,
2017-11-18 03:01:52 +08:00
) ;
2017-10-23 20:41:09 +08:00
}
2019-08-03 05:32:42 +08:00
2019-11-30 05:48:04 +08:00
monitor ( ) {
const bridge = ( this . useKurento && this . isListenOnly ) ? this . listenOnlyBridge : this . bridge ;
const peer = bridge . getPeerConnection ( ) ;
monitorAudioConnection ( peer ) ;
}
2019-08-03 05:32:42 +08:00
handleAllowAutoplay ( ) {
window . removeEventListener ( 'audioPlayFailed' , this . handlePlayElementFailed ) ;
2019-09-07 02:58:22 +08:00
logger . info ( {
logCode : 'audiomanager_autoplay_allowed' ,
} , 'Listen only autoplay allowed by the user' ) ;
2019-08-03 05:32:42 +08:00
while ( this . failedMediaElements . length ) {
const mediaElement = this . failedMediaElements . shift ( ) ;
if ( mediaElement ) {
2019-09-07 02:58:22 +08:00
playAndRetry ( mediaElement ) . then ( ( played ) => {
if ( ! played ) {
logger . error ( {
logCode : 'audiomanager_autoplay_handling_failed' ,
} , 'Listen only autoplay handling failed to play media' ) ;
} else {
// logCode is listenonly_* to make it consistent with the other tag play log
logger . info ( {
logCode : 'listenonly_media_play_success' ,
} , 'Listen only media played successfully' ) ;
}
2019-08-03 05:32:42 +08:00
} ) ;
}
}
this . autoplayBlocked = false ;
}
handlePlayElementFailed ( e ) {
const { mediaElement } = e . detail ;
e . stopPropagation ( ) ;
this . failedMediaElements . push ( mediaElement ) ;
if ( ! this . autoplayBlocked ) {
2019-09-07 02:58:22 +08:00
logger . info ( {
logCode : 'audiomanager_autoplay_prompt' ,
} , 'Prompting user for action to play listen only media' ) ;
2019-08-03 05:32:42 +08:00
this . autoplayBlocked = true ;
}
}
2020-09-10 01:03:27 +08:00
2020-09-17 22:37:28 +08:00
setSenderTrackEnabled ( shouldEnable ) {
2020-09-12 07:06:56 +08:00
// If the bridge is set to listen only mode, nothing to do here. This method
// is solely for muting outbound tracks.
if ( this . isListenOnly ) return ;
2020-09-10 22:50:50 +08:00
// Bridge -> SIP.js bridge, the only full audio capable one right now
const peer = this . bridge . getPeerConnection ( ) ;
2020-09-26 07:11:44 +08:00
if ( ! peer ) {
return ;
}
2020-09-10 01:03:27 +08:00
peer . getSenders ( ) . forEach ( sender => {
const { track } = sender ;
if ( track && track . kind === 'audio' ) {
2020-09-10 22:50:50 +08:00
track . enabled = shouldEnable ;
2020-09-10 01:03:27 +08:00
}
} ) ;
}
2020-09-17 22:37:28 +08:00
mute ( ) {
2020-09-10 22:50:50 +08:00
this . setSenderTrackEnabled ( false ) ;
}
2020-09-17 22:37:28 +08:00
unmute ( ) {
2020-09-10 22:50:50 +08:00
this . setSenderTrackEnabled ( true ) ;
2020-09-10 01:03:27 +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
playAlertSound ( url ) {
if ( ! url ) {
return Promise . resolve ( ) ;
}
const audioAlert = new Audio ( url ) ;
if ( this . outputDeviceId && ( typeof audioAlert . setSinkId === 'function' ) ) {
return audioAlert
. setSinkId ( this . outputDeviceId )
. then ( ( ) => audioAlert . play ( ) ) ;
}
return audioAlert . play ( ) ;
}
2017-09-20 01:47:57 +08:00
}
const audioManager = new AudioManager ( ) ;
export default audioManager ;