html5 sipjs bridge has useful errors now

This commit is contained in:
Chad Pilkey 2019-02-20 13:58:37 -08:00
parent 77ca025891
commit 89b8189087
5 changed files with 123 additions and 83 deletions

View File

@ -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: this.baseErrorCodes.ICE_NEGOTIATION_FAILED,
bridgeError: peer,
error: 1007,
bridgeError: `ICE negotiation failed. Current state - ${peer.iceConnectionState}`,
});
};
CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.on(e, handleConnectionTerminated));
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: 1012,
bridgeError: "ICE connection closed. Current state - " + peer.iceConnectionState,
});
*/
};
['iceConnectionClosed'].forEach(e => mediaHandler.on(e, handleIceConnectionTerminated));
this.currentSession = currentSession;
});

View File

@ -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;

View File

@ -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;

View File

@ -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();
}

View File

@ -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",