2022-01-26 04:49:38 +08:00
|
|
|
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';
|
2022-02-01 03:30:38 +08:00
|
|
|
import {
|
2022-08-20 01:22:42 +08:00
|
|
|
getAudioConstraints,
|
2022-09-16 01:48:26 +08:00
|
|
|
doGUM,
|
2022-02-01 03:30:38 +08:00
|
|
|
} from '/imports/api/audio/client/bridge/service';
|
2024-08-28 01:00:26 +08:00
|
|
|
import { getTransportStats } from '/imports/utils/stats';
|
2022-01-26 04:49:38 +08:00
|
|
|
|
|
|
|
const MEDIA = Meteor.settings.public.media;
|
|
|
|
const BASE_BRIDGE_NAME = 'base';
|
|
|
|
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
|
|
|
const TRANSFER_TONE = '1';
|
2024-08-28 01:00:26 +08:00
|
|
|
/**
|
|
|
|
* Audio status to be filtered in getStats()
|
|
|
|
*/
|
|
|
|
const FILTER_AUDIO_STATS = [
|
|
|
|
'outbound-rtp',
|
|
|
|
'inbound-rtp',
|
|
|
|
'candidate-pair',
|
|
|
|
'local-candidate',
|
|
|
|
'transport',
|
|
|
|
];
|
2022-01-26 04:49:38 +08:00
|
|
|
|
2017-07-24 22:15:46 +08:00
|
|
|
export default class BaseAudioBridge {
|
2017-10-12 05:04:10 +08:00
|
|
|
constructor(userData) {
|
|
|
|
this.userData = userData;
|
|
|
|
|
|
|
|
this.baseErrorCodes = {
|
2017-10-19 03:40:01 +08:00
|
|
|
INVALID_TARGET: 'INVALID_TARGET',
|
|
|
|
CONNECTION_ERROR: 'CONNECTION_ERROR',
|
|
|
|
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
|
|
|
|
GENERIC_ERROR: 'GENERIC_ERROR',
|
2017-10-27 01:14:56 +08:00
|
|
|
MEDIA_ERROR: 'MEDIA_ERROR',
|
2018-06-27 21:56:03 +08:00
|
|
|
WEBRTC_NOT_SUPPORTED: 'WEBRTC_NOT_SUPPORTED',
|
2018-06-29 02:14:35 +08:00
|
|
|
ICE_NEGOTIATION_FAILED: 'ICE_NEGOTIATION_FAILED',
|
2017-10-12 05:04:10 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
this.baseCallStates = {
|
|
|
|
started: 'started',
|
|
|
|
ended: 'ended',
|
|
|
|
failed: 'failed',
|
2019-06-13 05:01:20 +08:00
|
|
|
reconnecting: 'reconnecting',
|
2019-08-03 05:32:42 +08:00
|
|
|
autoplayBlocked: 'autoplayBlocked',
|
2017-10-12 05:04:10 +08:00
|
|
|
};
|
2022-01-26 04:49:38 +08:00
|
|
|
|
|
|
|
this.bridgeName = BASE_BRIDGE_NAME;
|
2017-07-24 22:15:46 +08:00
|
|
|
}
|
|
|
|
|
2019-11-30 05:48:04 +08:00
|
|
|
getPeerConnection() {
|
|
|
|
console.error('The Bridge must implement getPeerConnection');
|
|
|
|
}
|
|
|
|
|
2017-07-24 22:15:46 +08:00
|
|
|
exitAudio() {
|
2017-10-12 05:04:10 +08:00
|
|
|
console.error('The Bridge must implement exitAudio');
|
2017-07-24 22:15:46 +08:00
|
|
|
}
|
|
|
|
|
2017-09-29 21:38:10 +08:00
|
|
|
joinAudio() {
|
2017-10-12 05:04:10 +08:00
|
|
|
console.error('The Bridge must implement joinAudio');
|
2017-07-24 22:15:46 +08:00
|
|
|
}
|
2017-10-19 03:40:01 +08:00
|
|
|
|
|
|
|
changeInputDevice() {
|
|
|
|
console.error('The Bridge must implement changeInputDevice');
|
|
|
|
}
|
|
|
|
|
2022-08-20 01:22:42 +08:00
|
|
|
setInputStream() {
|
|
|
|
console.error('The Bridge must implement setInputStream');
|
|
|
|
}
|
|
|
|
|
2022-01-26 04:49:38 +08:00
|
|
|
sendDtmf() {
|
|
|
|
console.error('The Bridge must implement sendDtmf');
|
|
|
|
}
|
|
|
|
|
2022-08-20 01:22:42 +08:00
|
|
|
set inputDeviceId (deviceId) {
|
|
|
|
this._inputDeviceId = deviceId;
|
2022-02-01 03:30:38 +08:00
|
|
|
}
|
|
|
|
|
2022-08-20 01:22:42 +08:00
|
|
|
get inputDeviceId () {
|
|
|
|
return this._inputDeviceId;
|
2022-02-01 03:30:38 +08:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-08-20 01:22:42 +08:00
|
|
|
/**
|
|
|
|
* 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());
|
2022-02-01 03:30:38 +08:00
|
|
|
}
|
|
|
|
|
2022-09-16 01:48:26 +08:00
|
|
|
newStream = await doGUM(constraints);
|
2022-08-20 01:22:42 +08:00
|
|
|
await this.setInputStream(newStream);
|
2022-08-26 01:14:41 +08:00
|
|
|
if (backupStream && backupStream.active) {
|
|
|
|
backupStream.getAudioTracks().forEach((track) => track.stop());
|
|
|
|
backupStream = null;
|
|
|
|
}
|
2022-08-20 01:22:42 +08:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2022-02-01 03:30:38 +08:00
|
|
|
}
|
|
|
|
|
2022-01-26 04:49:38 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2024-08-28 01:00:26 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get stats about active audio peer.
|
|
|
|
* We filter the status based on FILTER_AUDIO_STATS constant.
|
|
|
|
* We also append to the returned object the information about peer's
|
|
|
|
* transport. This transport information is retrieved by
|
|
|
|
* getTransportStatsFromPeer().
|
|
|
|
*
|
|
|
|
* @returns An Object containing the status about the active audio peer.
|
|
|
|
*
|
|
|
|
* For more information see:
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats
|
|
|
|
* and
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport
|
|
|
|
*/
|
|
|
|
async getStats(stats) {
|
|
|
|
let peer;
|
|
|
|
let peerStats = stats;
|
|
|
|
let transportStats = {};
|
|
|
|
|
|
|
|
if (!peerStats) {
|
|
|
|
peer = this.getPeerConnection();
|
|
|
|
|
|
|
|
if (!peer) return null;
|
|
|
|
|
|
|
|
peerStats = await peer.getStats();
|
|
|
|
}
|
|
|
|
|
|
|
|
const audioStats = {};
|
|
|
|
|
|
|
|
peerStats.forEach((stat) => {
|
|
|
|
if (FILTER_AUDIO_STATS.includes(stat.type)) {
|
|
|
|
audioStats[stat.id] = stat;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
|
|
|
transportStats = await getTransportStats(peer, audioStats);
|
|
|
|
} catch (error) {
|
|
|
|
logger.debug({
|
|
|
|
logCode: 'audio_transport_stats_failed',
|
|
|
|
extraInfo: {
|
|
|
|
errorCode: error.errorCode,
|
|
|
|
errorMessage: error.errorMessage,
|
|
|
|
bridgeName: this.bridgeName,
|
|
|
|
},
|
|
|
|
}, 'Failed to get transport stats for audio');
|
|
|
|
}
|
|
|
|
|
|
|
|
return { transportStats, ...audioStats };
|
|
|
|
}
|
2017-07-24 22:15:46 +08:00
|
|
|
}
|