jiti-meet/react/features/vad-reporter/VADReportingService.js

285 lines
9.2 KiB
JavaScript

// @flow
import EventEmitter from 'events';
import logger from './logger';
import TrackVADEmitter from './TrackVADEmitter';
import { VAD_SCORE_PUBLISHED, VAD_REPORT_PUBLISHED } from './VADEvents';
import type { VADScore } from './TrackVADEmitter';
export type { VADScore };
/**
* Sample rate used by TrackVADEmitter, this value determines how often the ScriptProcessorNode is going to call the
* process audio function and with what sample size.
* Basically lower values mean more callbacks with lower processing times bigger values less callbacks with longer
* processing times. This value is somewhere in the middle, so we strike a balance between flooding with callbacks
* and processing time. Possible values 256, 512, 1024, 2048, 4096, 8192, 16384. Passing other values will default
* to closes neighbor.
*/
const SCRIPT_NODE_SAMPLE_RATE = 4096;
/**
* Context that contains the emitter and additional information about the device.
*/
type VADDeviceContext = {
/**
* MediaDeviceInfo for associated context
*/
deviceInfo: MediaDeviceInfo,
/**
* Array with VAD scores publish from the emitter.
*/
scoreArray: Array<VADScore>,
/**
* TrackVADEmitter associated with media device
*/
vadEmitter: TrackVADEmitter
};
/**
* The structure used by VADReportingService to relay a score report
*/
export type VADReportScore = {
/**
* Device ID associated with the VAD score
*/
deviceId: string,
/**
* The PCM score from 0 - 1 i.e. 0.60
*/
score: number,
/**
* Epoch time at which PCM was recorded
*/
timestamp: number
};
/**
* Voice activity detection reporting service. The service create TrackVADEmitters for the provided devices and
* publishes an average of their VAD score over the specified interval via EventEmitter.
* The service is not reusable if destroyed a new one needs to be created, i.e. when a new device is added to the system
* a new service needs to be created and the old discarded.
*/
export default class VADReportingService extends EventEmitter {
/**
* Map containing context for devices currently being monitored by the reporting service.
*/
_contextMap: Map<string, VADDeviceContext>;
/**
* State flag, check if the instance was destroyed.
*/
_destroyed: boolean = false;
/**
* Delay at which to publish VAD score for monitored devices.
*/
_intervalDelay: number;
/**
* Identifier for the interval publishing stats on the set interval.
*/
_intervalId: ?IntervalID;
/**
* Constructor.
*
* @param {number} intervalDelay - Delay at which to publish VAD score for monitored devices.
* @param {Function} publishScoreCallBack - Function called on the specific interval with the calculated VAD score.
*/
constructor(intervalDelay: number) {
super();
this._contextMap = new Map();
this._intervalDelay = intervalDelay;
logger.log(`Constructed VADReportingService with publish interval of: ${intervalDelay}`);
}
/**
* Factory methods that creates the TrackVADEmitters for the associated array of devices and instantiates
* a VADReportingService.
*
* @param {Array<MediaDeviceInfo>} micDeviceList - Device list that is monitored inside the service.
* @param {number} intervalDelay - Delay at which to publish VAD score for monitored devices.
* @param {Function} publishScoreCallBack - Function called on the specific interval with the calculated VAD score.
*
* @returns {Promise<VADReportingService>}
*/
static create(micDeviceList: Array<MediaDeviceInfo>, intervalDelay: number) {
const vadReportingService = new VADReportingService(intervalDelay);
const emitterPromiseArray = [];
// Create a TrackVADEmitter for each provided audioinput device.
for (const micDevice of micDeviceList) {
if (micDevice.kind !== 'audioinput') {
logger.warn(`Provided device ${micDevice.label} -> ${micDevice.deviceId}, is not audioinput ignoring!`);
return;
}
logger.log(`Initializing VAD context for mic: ${micDevice.label} -> ${micDevice.deviceId}`);
const emitterPromise = TrackVADEmitter.create(micDevice.deviceId, SCRIPT_NODE_SAMPLE_RATE).then(emitter => {
emitter.on(VAD_SCORE_PUBLISHED, vadReportingService._devicePublishVADScore.bind(vadReportingService));
return {
vadEmitter: emitter,
deviceInfo: micDevice,
scoreArray: []
};
});
emitterPromiseArray.push(emitterPromise);
}
// Once all the TrackVADEmitter promises are resolved check if all of them resolved properly if not reject
// the promise and clear the already created emitters.
// $FlowFixMe - allSettled is not part of flow prototype even though it's a valid Promise function
return Promise.allSettled(emitterPromiseArray).then(outcomeArray => {
const vadContextArray = [];
const rejectedEmitterPromiseArray = [];
for (const outcome of outcomeArray) {
if (outcome.status === 'fulfilled') {
vadContextArray.push(outcome.value);
} else {
// Promise was rejected.
logger.error(`Create TrackVADEmitter promise failed with ${outcome.reason}`);
rejectedEmitterPromiseArray.push(outcome);
}
}
// Check if there were any rejected promises and clear the already created ones list.
if (rejectedEmitterPromiseArray.length > 0) {
logger.error('Cleaning up remaining VADDeviceContext, due to create fail!');
for (const context of vadContextArray) {
context.vadEmitter.destroy();
}
// Reject create promise if one emitter failed to instantiate, we might one just ignore it,
// leaving it like this for now
throw new Error('Create VADReportingService failed due to TrackVADEmitter creation issues!');
}
vadReportingService._setVADContextArray(vadContextArray);
vadReportingService._startPublish();
return vadReportingService;
});
}
/**
* Destroy TrackVADEmitters and clear the context map.
*
* @returns {void}
*/
_clearContextMap() {
for (const vadContext of this._contextMap.values()) {
vadContext.vadEmitter.destroy();
}
this._contextMap.clear();
}
/**
* Set the watched device contexts.
*
* @param {Array<VADDeviceContext>} vadContextArray - List of mics.
* @returns {void}
*/
_setVADContextArray(vadContextArray: Array<VADDeviceContext>): void {
for (const vadContext of vadContextArray) {
this._contextMap.set(vadContext.deviceInfo.deviceId, vadContext);
}
}
/**
* Start the setInterval reporting process.
*
* @returns {void}.
*/
_startPublish() {
logger.log('VADReportingService started publishing.');
this._intervalId = setInterval(() => {
this._reportVadScore();
}, this._intervalDelay);
}
/**
* Function called at set interval with selected compute. The result will be published on the set callback.
*
* @returns {void}
*/
_reportVadScore() {
const vadComputeScoreArray = [];
const computeTimestamp = Date.now();
// Go through each device and compute cumulated VAD score.
for (const [ deviceId, vadContext ] of this._contextMap) {
const nrOfVADScores = vadContext.scoreArray.length;
let vadSum = 0;
vadContext.scoreArray.forEach(vadScore => {
vadSum += vadScore.score;
});
// TODO For now we just calculate the average score for each device, more compute algorithms will be added.
const avgVAD = vadSum / nrOfVADScores;
vadContext.scoreArray = [];
vadComputeScoreArray.push({
timestamp: computeTimestamp,
score: avgVAD,
deviceId
});
}
this.emit(VAD_REPORT_PUBLISHED, vadComputeScoreArray);
}
/**
* Callback method passed to vad emitters in order to publish their score.
*
* @param {VADScore} vadScore - Mic publishing the score.
* @returns {void}
*/
_devicePublishVADScore(vadScore: VADScore) {
const context = this._contextMap.get(vadScore.deviceId);
if (context) {
context.scoreArray.push(vadScore);
}
}
/**
* Destroy the VADReportingService, stops the setInterval reporting, destroys the emitters and clears the map.
* After this call the instance is no longer usable.
*
* @returns {void}.
*/
destroy() {
if (this._destroyed) {
return;
}
logger.log('Destroying VADReportingService.');
if (this._intervalId) {
clearInterval(this._intervalId);
this._intervalId = null;
}
this._clearContextMap();
this._destroyed = true;
}
}