Merge remote-tracking branch 'upstream/master' into parameterize-network-monitoring-threshold
This commit is contained in:
commit
63a2df07cb
@ -282,3 +282,6 @@
|
||||
.icon-bbb-popout_window:before {
|
||||
content: "\e956";
|
||||
}
|
||||
.icon-bbb-closed_caption:before {
|
||||
content: "\e957";
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ export default class BaseAudioBridge {
|
||||
started: 'started',
|
||||
ended: 'ended',
|
||||
failed: 'failed',
|
||||
reconnecting: 'reconnecting',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,38 +11,18 @@ 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 IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
|
||||
const ICE_NEGOTIATION_FAILED = ['iceConnectionFailed'];
|
||||
const CALL_CONNECT_TIMEOUT = 15000;
|
||||
const ICE_NEGOTIATION_TIMEOUT = 20000;
|
||||
|
||||
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;
|
||||
class SIPSession {
|
||||
constructor(user, userData, protocol, hostname, baseCallStates) {
|
||||
this.user = user;
|
||||
this.userData = userData;
|
||||
this.protocol = protocol;
|
||||
this.hostname = hostname;
|
||||
this.baseCallStates = baseCallStates;
|
||||
}
|
||||
|
||||
static parseDTMF(message) {
|
||||
@ -58,11 +38,25 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
|
||||
|
||||
const callback = (message) => {
|
||||
// There will sometimes we erroneous errors put out like timeouts and improper shutdowns,
|
||||
// but only the first error ever matters
|
||||
if (this.alreadyErrored) {
|
||||
logger.info({ logCode: 'sip_js_absorbing_callback_message' }, `Absorbing a redundant callback message. ${JSON.stringify(message)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.status === this.baseCallStates.failed) {
|
||||
this.alreadyErrored = true;
|
||||
}
|
||||
|
||||
managerCallback(message).then(resolve);
|
||||
};
|
||||
|
||||
this.callback = callback;
|
||||
|
||||
// If there's an extension passed it means that we're joining the echo test first
|
||||
this.inEchoTest = !!extension;
|
||||
|
||||
return this.doCall({ callExtension, isListenOnly, inputStream })
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
@ -98,6 +92,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
|
||||
transferCall(onTransferSuccess) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.inEchoTest = false;
|
||||
|
||||
const timeout = setInterval(() => {
|
||||
clearInterval(timeout);
|
||||
logger.error({ logCode: 'sip_js_transfer_timed_out' }, 'Timeout on transfering from echo test to conference');
|
||||
@ -114,7 +110,7 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
|
||||
this.currentSession.on('dtmf', (event) => {
|
||||
if (event.body && (typeof event.body === 'string')) {
|
||||
const key = SIPBridge.parseDTMF(event.body);
|
||||
const key = SIPSession.parseDTMF(event.body);
|
||||
if (key === '7') {
|
||||
clearInterval(timeout);
|
||||
onTransferSuccess();
|
||||
@ -141,6 +137,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
}
|
||||
|
||||
this.currentSession.bye();
|
||||
this.userAgent.stop();
|
||||
|
||||
hangupRetries += 1;
|
||||
|
||||
setTimeout(() => {
|
||||
@ -178,8 +176,6 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
callerIdName,
|
||||
} = this.user;
|
||||
|
||||
let userAgentConnected = false;
|
||||
|
||||
// WebView safari needs a transceiver to be added. Made it a SIP.js hack.
|
||||
// Don't like the UA picking though, we should straighten everything to user
|
||||
// transceivers - prlanzarin 2019/05/21
|
||||
@ -193,7 +189,18 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
|
||||
logger.debug('Creating the user agent');
|
||||
|
||||
let userAgent = new window.SIP.UA({
|
||||
if (this.userAgent && this.userAgent.isConnected()) {
|
||||
if (this.userAgent.configuration.hostPortParams === this.hostname) {
|
||||
logger.debug('Reusing the user agent');
|
||||
resolve(this.userAgent);
|
||||
return;
|
||||
}
|
||||
logger.debug('different host name. need to kill');
|
||||
}
|
||||
|
||||
let userAgentConnected = false;
|
||||
|
||||
this.userAgent = new window.SIP.UA({
|
||||
uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
|
||||
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
|
||||
displayName: callerIdName,
|
||||
@ -207,21 +214,23 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
hackAddAudioTransceiver: isSafariWebview,
|
||||
});
|
||||
|
||||
userAgent.removeAllListeners('connected');
|
||||
userAgent.removeAllListeners('disconnected');
|
||||
|
||||
const handleUserAgentConnection = () => {
|
||||
userAgentConnected = true;
|
||||
resolve(userAgent);
|
||||
resolve(this.userAgent);
|
||||
};
|
||||
|
||||
const handleUserAgentDisconnection = () => {
|
||||
userAgent.stop();
|
||||
userAgent = null;
|
||||
if (this.userAgent) {
|
||||
this.userAgent.removeAllListeners();
|
||||
this.userAgent.stop();
|
||||
this.userAgent = null;
|
||||
}
|
||||
|
||||
let error;
|
||||
let bridgeError;
|
||||
|
||||
if (this.userRequestedHangup) return;
|
||||
|
||||
if (userAgentConnected) {
|
||||
error = 1001;
|
||||
bridgeError = 'Websocket disconnected';
|
||||
@ -238,10 +247,10 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
reject(this.baseErrorCodes.CONNECTION_ERROR);
|
||||
};
|
||||
|
||||
userAgent.on('connected', handleUserAgentConnection);
|
||||
userAgent.on('disconnected', handleUserAgentDisconnection);
|
||||
this.userAgent.on('connected', handleUserAgentConnection);
|
||||
this.userAgent.on('disconnected', handleUserAgentDisconnection);
|
||||
|
||||
userAgent.start();
|
||||
this.userAgent.start();
|
||||
});
|
||||
}
|
||||
|
||||
@ -279,8 +288,10 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return new Promise((resolve) => {
|
||||
const { mediaHandler } = currentSession;
|
||||
|
||||
this.connectionCompleted = false;
|
||||
this.inEcho = false;
|
||||
let iceCompleted = false;
|
||||
let fsReady = false;
|
||||
|
||||
this.currentSession = currentSession;
|
||||
|
||||
let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
|
||||
// Edge sends a connected first and then a completed, but the call isn't ready until
|
||||
@ -291,7 +302,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
}
|
||||
|
||||
const checkIfCallReady = () => {
|
||||
if (this.connectionCompleted && this.inEcho) {
|
||||
if (iceCompleted && fsReady) {
|
||||
this.webrtcConnected = true;
|
||||
this.callback({ status: this.baseCallStates.started });
|
||||
resolve();
|
||||
}
|
||||
@ -312,9 +324,10 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
const handleSessionAccepted = () => {
|
||||
logger.info({ logCode: 'sip_js_session_accepted' }, 'Audio call session accepted');
|
||||
clearTimeout(callTimeout);
|
||||
currentSession.off('accepted', handleSessionAccepted);
|
||||
|
||||
// If ICE isn't connected yet then start timeout waiting for ICE to finish
|
||||
if (!this.connectionCompleted) {
|
||||
if (!iceCompleted) {
|
||||
iceNegotiationTimeout = setTimeout(() => {
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
@ -338,7 +351,7 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
|
||||
this.connectionCompleted = true;
|
||||
iceCompleted = true;
|
||||
|
||||
checkIfCallReady();
|
||||
};
|
||||
@ -347,6 +360,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
const handleSessionTerminated = (message, cause) => {
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
currentSession.off('terminated', handleSessionTerminated);
|
||||
|
||||
if (!message && !cause && !!this.userRequestedHangup) {
|
||||
return this.callback({
|
||||
status: this.baseCallStates.ended,
|
||||
@ -356,7 +371,7 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
logger.error({ logCode: 'sip_js_call_terminated' }, `Audio call terminated. cause=${cause}`);
|
||||
|
||||
let mappedCause;
|
||||
if (!this.connectionCompleted) {
|
||||
if (!iceCompleted) {
|
||||
mappedCause = '1004';
|
||||
} else {
|
||||
mappedCause = '1005';
|
||||
@ -371,7 +386,7 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
currentSession.on('terminated', handleSessionTerminated);
|
||||
|
||||
const handleIceNegotiationFailed = (peer) => {
|
||||
if (this.connectionCompleted) {
|
||||
if (iceCompleted) {
|
||||
logger.error({ logCode: 'sipjs_ice_failed_after' }, 'ICE connection failed after success');
|
||||
} else {
|
||||
logger.error({ logCode: 'sipjs_ice_failed_before' }, 'ICE connection failed before success');
|
||||
@ -404,19 +419,107 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
|
||||
const inEchoDTMF = (event) => {
|
||||
if (event.body && typeof event.body === 'string') {
|
||||
const dtmf = SIPBridge.parseDTMF(event.body);
|
||||
const dtmf = SIPSession.parseDTMF(event.body);
|
||||
if (dtmf === '0') {
|
||||
this.inEcho = true;
|
||||
fsReady = true;
|
||||
checkIfCallReady();
|
||||
}
|
||||
}
|
||||
currentSession.off('dtmf', inEchoDTMF);
|
||||
};
|
||||
currentSession.on('dtmf', inEchoDTMF);
|
||||
|
||||
this.currentSession = currentSession;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
|
||||
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let { hostname } = this;
|
||||
|
||||
this.activeSession = new SIPSession(this.user, this.userData, this.protocol, hostname, this.baseCallStates);
|
||||
|
||||
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
|
||||
this.activeSession.exitAudio().catch(() => {});
|
||||
|
||||
if (this.activeSession.webrtcConnected) {
|
||||
// webrtc was able to connect so just try again
|
||||
message.silenceNotifications = true;
|
||||
callback({ status: this.baseCallStates.reconnecting });
|
||||
shouldTryReconnect = true;
|
||||
} else if (hasFallbackDomain === true && hostname !== IPV4_FALLBACK_DOMAIN) {
|
||||
message.silenceNotifications = true;
|
||||
logger.info({ logCode: 'sip_js_attempt_ipv4_fallback' }, 'Attempting to fallback to IPv4 domain for audio');
|
||||
hostname = IPV4_FALLBACK_DOMAIN;
|
||||
shouldTryReconnect = true;
|
||||
}
|
||||
|
||||
if (shouldTryReconnect) {
|
||||
const fallbackExtension = this.activeSession.inEchoTest ? extension : undefined;
|
||||
this.activeSession = new SIPSession(this.user, this.userData, this.protocol, hostname, this.baseCallStates);
|
||||
this.activeSession.joinAudio({ isListenOnly, extension: fallbackExtension, inputStream }, callback)
|
||||
.then((value) => {
|
||||
resolve(value);
|
||||
}).catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return managerCallback(message);
|
||||
};
|
||||
|
||||
this.activeSession.joinAudio({ isListenOnly, extension, inputStream }, callback)
|
||||
.then((value) => {
|
||||
resolve(value);
|
||||
}).catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
transferCall(onTransferSuccess) {
|
||||
return this.activeSession.transferCall(onTransferSuccess);
|
||||
}
|
||||
|
||||
exitAudio() {
|
||||
return this.activeSession.exitAudio();
|
||||
}
|
||||
|
||||
setDefaultInputDevice() {
|
||||
const handleMediaSuccess = (mediaStream) => {
|
||||
@ -424,24 +527,23 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
window.defaultInputStream = mediaStream.getTracks();
|
||||
return navigator.mediaDevices.enumerateDevices().then((mediaDevices) => {
|
||||
const device = mediaDevices.find(d => d.label === deviceLabel);
|
||||
return this.changeInputDevice(device.deviceId);
|
||||
return this.changeInputDevice(device.deviceId, deviceLabel);
|
||||
});
|
||||
};
|
||||
|
||||
return navigator.mediaDevices.getUserMedia({ audio: true }).then(handleMediaSuccess);
|
||||
}
|
||||
|
||||
changeInputDevice(value) {
|
||||
changeInputDevice(deviceId, deviceLabel) {
|
||||
const {
|
||||
media,
|
||||
} = this;
|
||||
|
||||
if (media.inputDevice.audioContext) {
|
||||
const handleAudioContextCloseSuccess = () => {
|
||||
media.inputDevice.audioContext = null;
|
||||
media.inputDevice.scriptProcessor = null;
|
||||
media.inputDevice.source = null;
|
||||
return this.changeInputDevice(value);
|
||||
return this.changeInputDevice(deviceId);
|
||||
};
|
||||
|
||||
return media.inputDevice.audioContext.close().then(handleAudioContextCloseSuccess);
|
||||
@ -453,14 +555,15 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
media.inputDevice.audioContext = new window.webkitAudioContext();
|
||||
}
|
||||
|
||||
media.inputDevice.id = value;
|
||||
media.inputDevice.id = deviceId;
|
||||
media.inputDevice.label = deviceLabel;
|
||||
media.inputDevice.scriptProcessor = media.inputDevice.audioContext
|
||||
.createScriptProcessor(2048, 1, 1);
|
||||
media.inputDevice.source = null;
|
||||
|
||||
const constraints = {
|
||||
audio: {
|
||||
deviceId: value,
|
||||
deviceId,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -166,7 +166,7 @@ class ActionsDropdown extends Component {
|
||||
(CaptionsService.isCaptionsEnabled()
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="polling"
|
||||
icon="closed_caption"
|
||||
label={formatMessage(captionsLabel)}
|
||||
description={formatMessage(captionsDesc)}
|
||||
key={this.captionsId}
|
||||
|
@ -25,7 +25,7 @@ const intlMessages = defineMessages({
|
||||
const CaptionsButton = ({ intl, isActive, handleOnClick }) => (
|
||||
<Button
|
||||
className={cx(styles.button, isActive || styles.btn)}
|
||||
icon="polling"
|
||||
icon="closed_caption"
|
||||
label={intl.formatMessage(isActive ? intlMessages.stop : intlMessages.start)}
|
||||
color={isActive ? 'primary' : 'default'}
|
||||
ghost={!isActive}
|
||||
|
@ -162,14 +162,17 @@
|
||||
}
|
||||
|
||||
.connectingAnimation {
|
||||
margin: auto;
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
|
||||
&:after {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
content: "\2026"; /* ascii code for the ellipsis character */
|
||||
width: 0;
|
||||
margin-right: 0.75em;
|
||||
margin-left: 0.75em;
|
||||
margin-left: 0.25em;
|
||||
|
||||
:global(.animationsEnabled) & {
|
||||
animation: ellipsis steps(4, end) 900ms infinite;
|
||||
@ -179,9 +182,7 @@
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: 1.25em;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.audioManager.leftAudio',
|
||||
description: 'Left audio toast message',
|
||||
},
|
||||
reconnectingAudio: {
|
||||
id: 'app.audioManager.reconnectingAudio',
|
||||
description: 'Reconnecting audio toast message',
|
||||
},
|
||||
genericError: {
|
||||
id: 'app.audioManager.genericError',
|
||||
description: 'Generic error message',
|
||||
@ -118,6 +122,7 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
JOINED_AUDIO: intlMessages.joinedAudio,
|
||||
JOINED_ECHO: intlMessages.joinedEcho,
|
||||
LEFT_AUDIO: intlMessages.leftAudio,
|
||||
RECONNECTING_AUDIO: intlMessages.reconnectingAudio,
|
||||
},
|
||||
error: {
|
||||
GENERIC_ERROR: intlMessages.genericError,
|
||||
|
@ -53,7 +53,7 @@ const CaptionsListItem = (props) => {
|
||||
onClick={() => handleClickToggleCaptions(locale.locale)}
|
||||
aria-label={`${locale.name} ${intl.formatMessage(intlMessages.captionLabel)}`}
|
||||
>
|
||||
<Icon iconName="polling" />
|
||||
<Icon iconName="closed_caption" />
|
||||
<span aria-hidden>{locale.name}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,7 +7,6 @@
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 0 var(--sm-padding-x);
|
||||
color: var(--color-gray-light);
|
||||
margin-bottom: var(--lg-padding-y);
|
||||
margin-top: var(--sm-padding-x);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ const CALL_STATES = {
|
||||
STARTED: 'started',
|
||||
ENDED: 'ended',
|
||||
FAILED: 'failed',
|
||||
RECONNECTING: 'reconnecting',
|
||||
};
|
||||
|
||||
class AudioManager {
|
||||
@ -81,6 +82,12 @@ class AudioManager {
|
||||
}
|
||||
|
||||
askDevicesPermissions() {
|
||||
// Check to see if the stream has already been retrieved becasue then we don't need to
|
||||
// request. This is a fix for an issue with the input device selector.
|
||||
if (this.inputStream) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Only change the isWaitingPermissions for the case where the user didnt allowed it yet
|
||||
const permTimeout = setTimeout(() => {
|
||||
if (!this.devicesInitialized) { this.isWaitingPermissions = true; }
|
||||
@ -107,35 +114,38 @@ class AudioManager {
|
||||
this.isListenOnly = false;
|
||||
this.isEchoTest = false;
|
||||
|
||||
const callOptions = {
|
||||
isListenOnly: false,
|
||||
extension: null,
|
||||
inputStream: this.inputStream,
|
||||
};
|
||||
|
||||
return this.askDevicesPermissions()
|
||||
.then(this.onAudioJoining.bind(this))
|
||||
.then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
|
||||
.then(() => {
|
||||
const callOptions = {
|
||||
isListenOnly: false,
|
||||
extension: null,
|
||||
inputStream: this.inputStream,
|
||||
};
|
||||
return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
joinEchoTest() {
|
||||
this.isListenOnly = false;
|
||||
this.isEchoTest = true;
|
||||
|
||||
const callOptions = {
|
||||
isListenOnly: false,
|
||||
extension: ECHO_TEST_NUMBER,
|
||||
inputStream: this.inputStream,
|
||||
};
|
||||
|
||||
return this.askDevicesPermissions()
|
||||
.then(this.onAudioJoining.bind(this))
|
||||
.then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
|
||||
.then(() => {
|
||||
const callOptions = {
|
||||
isListenOnly: false,
|
||||
extension: ECHO_TEST_NUMBER,
|
||||
inputStream: this.inputStream,
|
||||
};
|
||||
return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
async joinListenOnly(retries = 0) {
|
||||
this.isListenOnly = true;
|
||||
this.isEchoTest = false;
|
||||
|
||||
const { name } = browser();
|
||||
// The kurento bridge isn't a full audio bridge yet, so we have to differ it
|
||||
const bridge = this.useKurento ? this.listenOnlyBridge : this.bridge;
|
||||
@ -222,7 +232,6 @@ class AudioManager {
|
||||
const bridge = (this.useKurento && this.isListenOnly) ? this.listenOnlyBridge : this.bridge;
|
||||
|
||||
this.isHangingUp = true;
|
||||
this.isEchoTest = false;
|
||||
|
||||
return bridge.exitAudio();
|
||||
}
|
||||
@ -289,6 +298,10 @@ class AudioManager {
|
||||
if (!this.error && !this.isEchoTest) {
|
||||
this.notify(this.intl.formatMessage(this.messages.info.LEFT_AUDIO), false, 'audio_off');
|
||||
}
|
||||
if (!this.isEchoTest) {
|
||||
this.playHangUpSound();
|
||||
}
|
||||
|
||||
window.parent.postMessage({ response: 'notInAudio' }, '*');
|
||||
}
|
||||
|
||||
@ -298,12 +311,14 @@ class AudioManager {
|
||||
STARTED,
|
||||
ENDED,
|
||||
FAILED,
|
||||
RECONNECTING,
|
||||
} = CALL_STATES;
|
||||
|
||||
const {
|
||||
status,
|
||||
error,
|
||||
bridgeError,
|
||||
silenceNotifications,
|
||||
} = response;
|
||||
|
||||
if (status === STARTED) {
|
||||
@ -316,10 +331,16 @@ class AudioManager {
|
||||
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 ${JSON.stringify(errorMsg)}`);
|
||||
this.exitAudio();
|
||||
this.onAudioExit();
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -409,6 +430,11 @@ class AudioManager {
|
||||
return this._userData;
|
||||
}
|
||||
|
||||
playHangUpSound() {
|
||||
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/LeftCall.mp3`);
|
||||
this.alert.play();
|
||||
}
|
||||
|
||||
notify(message, error = false, icon = 'unmute') {
|
||||
const audioIcon = this.isListenOnly ? 'listen' : icon;
|
||||
|
||||
|
@ -19,6 +19,7 @@ public:
|
||||
basename: "/html5client"
|
||||
askForFeedbackOnLogout: false
|
||||
allowUserLookup: false
|
||||
ipv4FallbackDomain: ""
|
||||
defaultSettings:
|
||||
application:
|
||||
animations: true
|
||||
|
@ -26,7 +26,7 @@
|
||||
"app.captions.menu.ariaStartDesc": "Opens captions editor and closes the modal",
|
||||
"app.captions.menu.select": "Select available language",
|
||||
"app.captions.menu.ariaSelect": "Captions language",
|
||||
"app.captions.menu.title": "Closed captions menu",
|
||||
"app.captions.menu.title": "Closed captions",
|
||||
"app.captions.menu.fontSize": "Size",
|
||||
"app.captions.menu.fontColor": "Text color",
|
||||
"app.captions.menu.fontFamily": "Font",
|
||||
@ -402,6 +402,7 @@
|
||||
"app.audioManager.joinedAudio": "You have joined the audio conference",
|
||||
"app.audioManager.joinedEcho": "You have joined the echo test",
|
||||
"app.audioManager.leftAudio": "You have left the audio conference",
|
||||
"app.audioManager.reconnectingAudio": "Attempting to reconnect audio",
|
||||
"app.audioManager.genericError": "Error: An error has occurred, please try again",
|
||||
"app.audioManager.connectionError": "Error: Connection error",
|
||||
"app.audioManager.requestTimeout": "Error: There was a timeout in the request",
|
||||
|
Binary file not shown.
BIN
bigbluebutton-html5/public/resources/sounds/LeftCall.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/LeftCall.mp3
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user