diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 577578047f39a3c4b0fd9280383937a53fe9a322..a4caa2a5cf6ca861521c740bdec8cea79ee134a1 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -1,18 +1,19 @@ -import _ from 'lodash'; import VoiceUsers from '/imports/api/voice-users'; import { Tracker } from 'meteor/tracker'; +import browser from 'browser-detect'; import BaseAudioBridge from './base'; import logger from '/imports/startup/client/logger'; import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers'; -import browser from 'browser-detect'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout; const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout; const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries; -const CONNECTION_TERMINATED_EVENTS = ['iceConnectionFailed', 'iceConnectionClosed']; +const ICE_NEGOTIATION_FAILED = ['iceConnectionFailed']; +const CALL_CONNECT_TIMEOUT = 10000; const CALL_CONNECT_NOTIFICATION_TIMEOUT = 500; +const ICE_NEGOTIATION_TIMEOUT = 10000; export default class SIPBridge extends BaseAudioBridge { constructor(userData) { @@ -36,30 +37,6 @@ export default class SIPBridge extends BaseAudioBridge { this.protocol = window.document.location.protocol; this.hostname = window.document.location.hostname; - - const { - causes, - } = window.SIP.C; - - this.errorCodes = { - [causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT, - [causes.INVALID_TARGET]: this.baseErrorCodes.INVALID_TARGET, - [causes.CONNECTION_ERROR]: this.baseErrorCodes.CONNECTION_ERROR, - [causes.WEBRTC_NOT_SUPPORTED]: this.baseErrorCodes.WEBRTC_NOT_SUPPORTED, - }; - this.webRtcError = { - 1001: '1001', - 1002: '1002', - 1003: '1003', - 1004: '1004', - 1005: '1005', - 1006: '1006', - 1007: '1007', - 1008: '1008', - 1009: '1009', - 1010: '1010', - 1011: '1011', - }; } joinAudio({ isListenOnly, extension, inputStream }, managerCallback) { @@ -74,11 +51,6 @@ export default class SIPBridge extends BaseAudioBridge { return this.doCall({ callExtension, isListenOnly, inputStream }) .catch((reason) => { - callback({ - status: this.baseCallStates.failed, - error: this.baseErrorCodes.GENERIC_ERROR, - bridgeError: reason, - }); reject(reason); }); }); @@ -117,7 +89,7 @@ export default class SIPBridge extends BaseAudioBridge { const timeout = setTimeout(() => { clearTimeout(timeout); trackerControl.stop(); - logger.error({logCode: "sip_js_transfer_timed_out"}, "Timeout on transfering from echo test to conference") + logger.error({ logCode: 'sip_js_transfer_timed_out' }, 'Timeout on transfering from echo test to conference'); this.callback({ status: this.baseCallStates.failed, error: 1008, @@ -154,9 +126,15 @@ export default class SIPBridge extends BaseAudioBridge { let hangup = false; const { mediaHandler } = this.currentSession; + this.userRequestedHangup = true; // Removing termination events to avoid triggering an error - CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.off(e)); + ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.off(e)); const tryHangup = () => { + if (!!this.currentSession.endTime) { + hangup = true; + return resolve(); + } + this.currentSession.bye(); hangupRetries += 1; @@ -164,7 +142,7 @@ export default class SIPBridge extends BaseAudioBridge { if (hangupRetries > CALL_HANGUP_MAX_RETRIES) { this.callback({ status: this.baseCallStates.failed, - error: this.baseErrorCodes.REQUEST_TIMEOUT, + error: 1006, bridgeError: 'Timeout on call hangup', }); return reject(this.baseErrorCodes.REQUEST_TIMEOUT); @@ -195,6 +173,10 @@ export default class SIPBridge extends BaseAudioBridge { callerIdName, } = this.user; + let userAgentConnected = false; + + logger.debug('Creating the user agent'); + let userAgent = new window.SIP.UA({ uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`, wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`, @@ -211,19 +193,29 @@ export default class SIPBridge extends BaseAudioBridge { userAgent.removeAllListeners('disconnected'); const handleUserAgentConnection = () => { + userAgentConnected = true; resolve(userAgent); }; - const handleUserAgentDisconnection = (event) => { + const handleUserAgentDisconnection = () => { userAgent.stop(); userAgent = null; - const { lastTransportError } = event.transport; - const errorCode = lastTransportError.code; - const error = this.webRtcError[errorCode] || this.baseErrorCodes.CONNECTION_ERROR; + + let error; + let bridgeError; + + if (userAgentConnected) { + error = 1001; + bridgeError = 'Websocket disconnected'; + } else { + error = 1002; + bridgeError = 'Websocket failed to connect'; + } + this.callback({ status: this.baseCallStates.failed, error, - bridgeError: 'User Agent Disconnected', + bridgeError, }); reject(this.baseErrorCodes.CONNECTION_ERROR); }; @@ -269,46 +261,79 @@ export default class SIPBridge extends BaseAudioBridge { return new Promise((resolve) => { const { mediaHandler } = currentSession; + this.connectionCompleted = false; + let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected']; // Edge sends a connected first and then a completed, but the call isn't ready until // the completed comes in. Due to the way that we have the listeners set up, the only // way to ignore one status is to not listen for it. if (browser().name === 'edge') { - connectionCompletedEvents = ['iceConnectionCompleted']; + connectionCompletedEvents = ['iceConnectionCompleted']; } + // Sometimes FreeSWITCH just won't respond with anything and hangs. This timeout is to + // avoid that issue + const callTimeout = setTimeout(() => { + this.callback({ + status: this.baseCallStates.failed, + error: 1006, + bridgeError: 'Call timed out on start after ' + CALL_CONNECT_TIMEOUT/1000 + 's', + }); + }, CALL_CONNECT_TIMEOUT); + + let iceNegotiationTimeout; + const handleSessionAccepted = () => { - logger.info({logCode: "sip_js_session_accepted"}, "Audio call session accepted"); + logger.info({ logCode: 'sip_js_session_accepted' }, 'Audio call session accepted'); + clearTimeout(callTimeout); + + // If ICE isn't connected yet then start timeout waiting for ICE to finish + if (!this.connectionCompleted) { + iceNegotiationTimeout = setTimeout(() => { + this.callback({ + status: this.baseCallStates.failed, + error: 1010, + bridgeError: 'ICE negotiation timeout after ' + ICE_NEGOTIATION_TIMEOUT/1000 + 's', + }); + }, ICE_NEGOTIATION_TIMEOUT); + } }; currentSession.on('accepted', handleSessionAccepted); const handleConnectionCompleted = (peer) => { - logger.info({logCode: "sip_js_ice_connection_success"}, "ICE connection success. Current state - " + peer.iceConnectionState); + logger.info({ logCode: 'sip_js_ice_connection_success' }, `ICE connection success. Current state - ${peer.iceConnectionState}`); + clearTimeout(callTimeout); + clearTimeout(iceNegotiationTimeout); connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted)); + this.connectionCompleted = true; // We have to delay notifying that the call is connected because it is sometimes not // actually ready and if the user says "Yes they can hear themselves" too quickly the // B-leg transfer will fail const that = this; setTimeout(() => { that.callback({ status: that.baseCallStates.started }); - that.connectionCompleted = true; resolve(); }, CALL_CONNECT_NOTIFICATION_TIMEOUT); }; connectionCompletedEvents.forEach(e => mediaHandler.on(e, handleConnectionCompleted)); const handleSessionTerminated = (message, cause) => { - if (!message && !cause) { + clearTimeout(callTimeout); + clearTimeout(iceNegotiationTimeout); + if (!message && !cause && !!this.userRequestedHangup) { return this.callback({ status: this.baseCallStates.ended, }); } - logger.error({logCode: "sip_js_call_terminated"}, "Audio call terminated. message=" + message + ", cause=" + cause); + logger.error({ logCode: 'sip_js_call_terminated' }, `Audio call terminated. cause=${cause}`); - const mappedCause = cause in this.errorCodes - ? this.errorCodes[cause] - : this.baseErrorCodes.GENERIC_ERROR; + let mappedCause; + if (!this.connectionCompleted) { + mappedCause = '1004'; + } else { + mappedCause = '1005'; + } return this.callback({ status: this.baseCallStates.failed, @@ -318,16 +343,30 @@ export default class SIPBridge extends BaseAudioBridge { }; currentSession.on('terminated', handleSessionTerminated); - const handleConnectionTerminated = (peer) => { - logger.error({logCode: "sip_js_ice_connection_error"}, "ICE connection error. Current state - " + peer.iceConnectionState); - CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.off(e, handleConnectionTerminated)); + const handleIceNegotiationFailed = (peer) => { + clearTimeout(callTimeout); + clearTimeout(iceNegotiationTimeout); + ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.off(e, handleIceNegotiationFailed)); + this.callback({ + status: this.baseCallStates.failed, + error: 1007, + bridgeError: `ICE negotiation failed. Current state - ${peer.iceConnectionState}`, + }); + }; + ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.on(e, handleIceNegotiationFailed)); + + const handleIceConnectionTerminated = (peer) => { + ['iceConnectionClosed'].forEach(e => mediaHandler.off(e, handleIceConnectionTerminated)); + logger.error({ logCode: 'sipjs_ice_closed' }, 'ICE connection closed'); + /* this.callback({ status: this.baseCallStates.failed, - error: this.baseErrorCodes.ICE_NEGOTIATION_FAILED, - bridgeError: peer, + error: 1012, + bridgeError: "ICE connection closed. Current state - " + peer.iceConnectionState, }); + */ }; - CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.on(e, handleConnectionTerminated)); + ['iceConnectionClosed'].forEach(e => mediaHandler.on(e, handleIceConnectionTerminated)); this.currentSession = currentSession; }); diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx index a7a75ebd2e271dd5e58d74e983637ec839ea9d8d..aca05aa168605054bc5e74900900d93a16a0a210 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx @@ -47,10 +47,6 @@ const intlMessages = defineMessages({ id: 'app.audioNotification.audioFailedError1003', description: 'browser not supported error messsage', }, - iceNegotiationError: { - id: 'app.audioNotification.audioFailedError1007', - description: 'ice negociation error messsage', - }, reconectingAsListener: { id: 'app.audioNotificaion.reconnectingAsListenOnly', description: 'ice negociation error messsage', @@ -106,33 +102,32 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) => }, }); - const webRtcError = _.range(1001, 1012) + const webRtcError = _.range(1001, 1011) .reduce((acc, value) => ({ ...acc, - [value]: intl.formatMessage({ id: `app.audioNotification.audioFailedError${value}` }), + [value]: { id: `app.audioNotification.audioFailedError${value}` }, }), {}); const messages = { info: { - JOINED_AUDIO: intl.formatMessage(intlMessages.joinedAudio), - JOINED_ECHO: intl.formatMessage(intlMessages.joinedEcho), - LEFT_AUDIO: intl.formatMessage(intlMessages.leftAudio), + JOINED_AUDIO: intlMessages.joinedAudio, + JOINED_ECHO: intlMessages.joinedEcho, + LEFT_AUDIO: intlMessages.leftAudio, }, error: { - GENERIC_ERROR: intl.formatMessage(intlMessages.genericError), - CONNECTION_ERROR: intl.formatMessage(intlMessages.connectionError), - REQUEST_TIMEOUT: intl.formatMessage(intlMessages.requestTimeout), - INVALID_TARGET: intl.formatMessage(intlMessages.invalidTarget), - MEDIA_ERROR: intl.formatMessage(intlMessages.mediaError), - WEBRTC_NOT_SUPPORTED: intl.formatMessage(intlMessages.BrowserNotSupported), - ICE_NEGOTIATION_FAILED: intl.formatMessage(intlMessages.iceNegotiationError), + GENERIC_ERROR: intlMessages.genericError, + CONNECTION_ERROR: intlMessages.connectionError, + REQUEST_TIMEOUT: intlMessages.requestTimeout, + INVALID_TARGET: intlMessages.invalidTarget, + MEDIA_ERROR: intlMessages.mediaError, + WEBRTC_NOT_SUPPORTED: intlMessages.BrowserNotSupported, ...webRtcError, }, }; return { init: () => { - Service.init(messages); + Service.init(messages, intl); Service.changeOutputDevice(document.querySelector('#remote-media').sinkId); if (!autoJoin || didMountAutoJoin) return; diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index 0be7337cd7fb3d9e9514ed781cf0a586fd5d0ac6..027b83f8bd7d25bda2a8b37ca0f9e4d4cc58959f 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -4,8 +4,8 @@ import AudioManager from '/imports/ui/services/audio-manager'; import Meetings from '/imports/api/meetings'; import mapUser from '/imports/ui/services/user/mapUser'; -const init = (messages) => { - AudioManager.setAudioMessages(messages); +const init = (messages, intl) => { + AudioManager.setAudioMessages(messages, intl); if (AudioManager.initialized) return; const meetingId = Auth.meetingID; const userId = Auth.userID; diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 7348982e0c23c357a4c67b7ccc20fccf3683db86..2c39b2c0d896c99acb2aecb1c1aba1668d5d4844 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -56,8 +56,9 @@ class AudioManager { this.initialized = true; } - setAudioMessages(messages) { + setAudioMessages(messages, intl) { this.messages = messages; + this.intl = intl; } defineProperties(obj) { @@ -153,14 +154,14 @@ class AudioManager { try { await tryGenerateIceCandidates(); } catch (e) { - this.notify(this.messages.error.ICE_NEGOTIATION_FAILED); + this.notify(this.intl.formatMessage(this.messages.error.ICE_NEGOTIATION_FAILED)); } } // 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) - && userAgent.indexOf('safari') == -1) { + && userAgent.indexOf('safari') === -1) { iosWebviewAudioPolyfills(); } @@ -265,7 +266,8 @@ class AudioManager { if (!this.isEchoTest) { window.parent.postMessage({ response: 'joinedAudio' }, '*'); - this.notify(this.messages.info.JOINED_AUDIO); + this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO)); + logger.info({ logCode: 'audio_joined' }, 'Audio Joined'); } } @@ -287,7 +289,7 @@ class AudioManager { } if (!this.error && !this.isEchoTest) { - this.notify(this.messages.info.LEFT_AUDIO); + this.notify(this.intl.formatMessage(this.messages.info.LEFT_AUDIO)); } window.parent.postMessage({ response: 'notInAudio' }, '*'); } @@ -310,11 +312,14 @@ class AudioManager { this.onAudioJoin(); resolve(STARTED); } else if (status === ENDED) { + logger.debug({ logCode: 'audio_ended' }, 'Audio ended without issue'); this.onAudioExit(); } else if (status === FAILED) { - this.error = error; - this.notify(this.messages.error[error] || this.messages.error.GENERIC_ERROR, true); - logger.error({ logCode: 'audiomanager_audio_error' }, 'Audio Error:', error, bridgeError); + const errorKey = this.messages.error[error] || this.messages.error.GENERIC_ERROR; + const errorMsg = this.intl.formatMessage(errorKey, { 0: bridgeError }); + this.error = !!error; + this.notify(errorMsg, true); + logger.error({ logCode: 'audio_failure', error, cause: bridgeError }, 'Audio Error:', error, bridgeError); this.exitAudio(); this.onAudioExit(); } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 76fadde7ec1d92a0fcdaf09647ab918f59684c0f..76fee2335d88f0ae214f07c4521a56759179814a 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -287,7 +287,7 @@ "app.audioNotification.audioFailedError1001": "Error 1001: WebSocket disconnected", "app.audioNotification.audioFailedError1002": "Error 1002: Could not make a WebSocket connection", "app.audioNotification.audioFailedError1003": "Error 1003: Browser version not supported", - "app.audioNotification.audioFailedError1004": "Error 1004: Failure on call", + "app.audioNotification.audioFailedError1004": "Error 1004: Failure on call (reason={0})", "app.audioNotification.audioFailedError1005": "Error 1005: Call ended unexpectedly", "app.audioNotification.audioFailedError1006": "Error 1006: Call timed out", "app.audioNotification.audioFailedError1007": "Error 1007: ICE negotiation failed", @@ -295,6 +295,7 @@ "app.audioNotification.audioFailedError1009": "Error 1009: Could not fetch STUN/TURN server information", "app.audioNotification.audioFailedError1010": "Error 1010: ICE negotiation timeout", "app.audioNotification.audioFailedError1011": "Error 1011: ICE gathering timeout", + "app.audioNotification.audioFailedError1012": "Error 1012: ICE connection closed", "app.audioNotification.audioFailedMessage": "Your audio connection failed to connect", "app.audioNotification.mediaFailedMessage": "getUserMicMedia failed as only secure origins are allowed", "app.audioNotification.closeLabel": "Close",