html5 sipjs bridge has useful errors now
This commit is contained in:
parent
77ca025891
commit
89b8189087
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user