b3eebbb926
There are some situations where previously set deviceIds ( local/session storage) may become stale. This causes an unexpected behavior where audio is temporarily borked until the user clears their local storage. This issue has been seen more recently on Safari endpoints when switching back-and-forth breakout rooms in environments running under iframes. Also seen randomly on endpoints with virtual input devices. This centralizes audio gUM calling into a single method that retries the gUM procedure without pre-set deviceIds only if the initial call fails due with an OverconstrainedError - hopefully circumventing the issue.
178 lines
5.1 KiB
JavaScript
Executable File
178 lines
5.1 KiB
JavaScript
Executable File
import { Tracker } from 'meteor/tracker';
|
|
import VoiceCallStates from '/imports/api/voice-call-states';
|
|
import CallStateOptions from '/imports/api/voice-call-states/utils/callStates';
|
|
import logger from '/imports/startup/client/logger';
|
|
import Auth from '/imports/ui/services/auth';
|
|
import {
|
|
getAudioConstraints,
|
|
doGUM,
|
|
} from '/imports/api/audio/client/bridge/service';
|
|
|
|
const MEDIA = Meteor.settings.public.media;
|
|
const BASE_BRIDGE_NAME = 'base';
|
|
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
|
const TRANSFER_TONE = '1';
|
|
|
|
export default class BaseAudioBridge {
|
|
constructor(userData) {
|
|
this.userData = userData;
|
|
|
|
this.baseErrorCodes = {
|
|
INVALID_TARGET: 'INVALID_TARGET',
|
|
CONNECTION_ERROR: 'CONNECTION_ERROR',
|
|
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
|
|
GENERIC_ERROR: 'GENERIC_ERROR',
|
|
MEDIA_ERROR: 'MEDIA_ERROR',
|
|
WEBRTC_NOT_SUPPORTED: 'WEBRTC_NOT_SUPPORTED',
|
|
ICE_NEGOTIATION_FAILED: 'ICE_NEGOTIATION_FAILED',
|
|
};
|
|
|
|
this.baseCallStates = {
|
|
started: 'started',
|
|
ended: 'ended',
|
|
failed: 'failed',
|
|
reconnecting: 'reconnecting',
|
|
autoplayBlocked: 'autoplayBlocked',
|
|
};
|
|
|
|
this.bridgeName = BASE_BRIDGE_NAME;
|
|
}
|
|
|
|
getPeerConnection() {
|
|
console.error('The Bridge must implement getPeerConnection');
|
|
}
|
|
|
|
exitAudio() {
|
|
console.error('The Bridge must implement exitAudio');
|
|
}
|
|
|
|
joinAudio() {
|
|
console.error('The Bridge must implement joinAudio');
|
|
}
|
|
|
|
changeInputDevice() {
|
|
console.error('The Bridge must implement changeInputDevice');
|
|
}
|
|
|
|
setInputStream() {
|
|
console.error('The Bridge must implement setInputStream');
|
|
}
|
|
|
|
sendDtmf() {
|
|
console.error('The Bridge must implement sendDtmf');
|
|
}
|
|
|
|
set inputDeviceId (deviceId) {
|
|
this._inputDeviceId = deviceId;
|
|
}
|
|
|
|
get inputDeviceId () {
|
|
return this._inputDeviceId;
|
|
|
|
}
|
|
|
|
/**
|
|
* Change the input device with the given deviceId, without renegotiating
|
|
* peer.
|
|
* A new MediaStream object is created for the given deviceId. This object
|
|
* is returned by the resolved promise.
|
|
* @param {String} deviceId The id of the device to be set as input
|
|
* @return {Promise} A promise that is resolved with the MediaStream
|
|
* object after changing the input device.
|
|
*/
|
|
async liveChangeInputDevice(deviceId) {
|
|
let newStream;
|
|
let backupStream;
|
|
|
|
try {
|
|
const constraints = {
|
|
audio: getAudioConstraints({ deviceId }),
|
|
};
|
|
|
|
// Backup stream (current one) in case the switch fails
|
|
if (this.inputStream && this.inputStream.active) {
|
|
backupStream = this.inputStream ? this.inputStream.clone() : null;
|
|
this.inputStream.getAudioTracks().forEach((track) => track.stop());
|
|
}
|
|
|
|
newStream = await doGUM(constraints);
|
|
await this.setInputStream(newStream);
|
|
if (backupStream && backupStream.active) {
|
|
backupStream.getAudioTracks().forEach((track) => track.stop());
|
|
backupStream = null;
|
|
}
|
|
|
|
return newStream;
|
|
} catch (error) {
|
|
// Device change failed. Clean up the tentative new stream to avoid lingering
|
|
// stuff, then try to rollback to the previous input stream.
|
|
if (newStream && typeof newStream.getAudioTracks === 'function') {
|
|
newStream.getAudioTracks().forEach((t) => t.stop());
|
|
newStream = null;
|
|
}
|
|
|
|
// Rollback to backup stream
|
|
if (backupStream && backupStream.active) {
|
|
this.setInputStream(backupStream).catch((rollbackError) => {
|
|
logger.error({
|
|
logCode: 'audio_changeinputdevice_rollback_failure',
|
|
extraInfo: {
|
|
bridgeName: this.bridgeName,
|
|
deviceId,
|
|
errorName: rollbackError.name,
|
|
errorMessage: rollbackError.message,
|
|
},
|
|
}, 'Microphone device change rollback failed - the device may become silent');
|
|
|
|
backupStream.getAudioTracks().forEach((track) => track.stop());
|
|
backupStream = null;
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
trackTransferState(transferCallback) {
|
|
return new Promise((resolve, reject) => {
|
|
let trackerControl = null;
|
|
|
|
const timeout = setTimeout(() => {
|
|
trackerControl.stop();
|
|
logger.warn({ logCode: 'audio_transfer_timed_out' },
|
|
'Timeout on transferring from echo test to conference');
|
|
this.callback({
|
|
status: this.baseCallStates.failed,
|
|
error: 1008,
|
|
bridgeError: 'Timeout on call transfer',
|
|
bridge: this.bridgeName,
|
|
});
|
|
|
|
this.exitAudio();
|
|
|
|
reject(this.baseErrorCodes.REQUEST_TIMEOUT);
|
|
}, CALL_TRANSFER_TIMEOUT);
|
|
|
|
this.sendDtmf(TRANSFER_TONE);
|
|
|
|
Tracker.autorun((c) => {
|
|
trackerControl = c;
|
|
const selector = { meetingId: Auth.meetingID, userId: Auth.userID };
|
|
const query = VoiceCallStates.find(selector);
|
|
|
|
query.observeChanges({
|
|
changed: (id, fields) => {
|
|
if (fields.callState === CallStateOptions.IN_CONFERENCE) {
|
|
clearTimeout(timeout);
|
|
transferCallback();
|
|
|
|
c.stop();
|
|
resolve();
|
|
}
|
|
},
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|