144 lines
4.9 KiB
JavaScript
144 lines
4.9 KiB
JavaScript
// @flow
|
|
import EventEmitter from 'events';
|
|
import { ACTIVE_DEVICE_DETECTED } from './Events';
|
|
import logger from '../../logger';
|
|
import JitsiMeetJS from '../../../lib-jitsi-meet';
|
|
|
|
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
|
|
|
// If after 3000 ms the detector did not find any active devices consider that there aren't any usable ones available
|
|
// i.e. audioLevel > 0.008
|
|
const DETECTION_TIMEOUT = 3000;
|
|
|
|
/**
|
|
* Detect active input devices based on their audio levels, currently this is very simplistic. It works by simply
|
|
* checking all monitored devices for TRACK_AUDIO_LEVEL_CHANGED if a device has a audio level > 0.008 ( 0.008 is
|
|
* no input from the perspective of a JitsiLocalTrack ), at which point it triggers a ACTIVE_DEVICE_DETECTED event.
|
|
* If there are no devices that meet that criteria for DETECTION_TIMEOUT an event with empty deviceLabel parameter
|
|
* will be triggered,
|
|
* signaling that no active device was detected.
|
|
* TODO Potentially improve the active device detection using rnnoise VAD scoring.
|
|
*/
|
|
export class ActiveDeviceDetector extends EventEmitter {
|
|
|
|
/**
|
|
* Currently monitored devices.
|
|
*/
|
|
_availableDevices: Array<Object>;
|
|
|
|
/**
|
|
* State flag, check if the instance was destroyed.
|
|
*/
|
|
_destroyed: boolean = false;
|
|
|
|
/**
|
|
* Create active device detector.
|
|
*
|
|
* @param {Array<MediaDeviceInfo>} micDeviceList - Device list that is monitored inside the service.
|
|
*
|
|
* @returns {ActiveDeviceDetector}
|
|
*/
|
|
static async create(micDeviceList: Array<MediaDeviceInfo>) {
|
|
const availableDevices = [];
|
|
|
|
try {
|
|
for (const micDevice of micDeviceList) {
|
|
const localTrack = await JitsiMeetJS.createLocalTracks({
|
|
devices: [ 'audio' ],
|
|
micDeviceId: micDevice.deviceId
|
|
});
|
|
|
|
// We provide a specific deviceId thus we expect a single JitsiLocalTrack to be returned.
|
|
availableDevices.push(localTrack[0]);
|
|
}
|
|
|
|
return new ActiveDeviceDetector(availableDevices);
|
|
} catch (error) {
|
|
logger.error('Cleaning up remaining JitsiLocalTrack, due to ActiveDeviceDetector create fail!');
|
|
|
|
for (const device of availableDevices) {
|
|
device.stopStream();
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param {Array<Object>} availableDevices - Device list that is monitored inside the service.
|
|
*/
|
|
constructor(availableDevices: Array<Object>) {
|
|
super();
|
|
|
|
this._availableDevices = availableDevices;
|
|
|
|
// Setup event handlers for monitored devices.
|
|
for (const device of this._availableDevices) {
|
|
device.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, audioLevel => {
|
|
this._handleAudioLevelEvent(device, audioLevel);
|
|
});
|
|
}
|
|
|
|
// Cancel the detection in case no devices was found with audioLevel > 0 in te set timeout.
|
|
setTimeout(this._handleDetectionTimeout.bind(this), DETECTION_TIMEOUT);
|
|
}
|
|
|
|
/**
|
|
* Handle what happens if no device publishes a score in the defined time frame, i.e. Emit an event with empty
|
|
* deviceLabel.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_handleDetectionTimeout() {
|
|
if (!this._destroyed) {
|
|
this.emit(ACTIVE_DEVICE_DETECTED, { deviceLabel: '',
|
|
audioLevel: 0 });
|
|
this.destroy();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles audio level event generated by JitsiLocalTracks.
|
|
*
|
|
* @param {Object} device - Label of the emitting track.
|
|
* @param {number} audioLevel - Audio level generated by device.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_handleAudioLevelEvent(device, audioLevel) {
|
|
if (!this._destroyed) {
|
|
// This is a very naive approach but works is most, a more accurate approach would ne to use rnnoise
|
|
// in order to limit the number of false positives.
|
|
// The 0.008 constant is due to how LocalStatsCollector from lib-jitsi-meet publishes audio-levels, in this
|
|
// case 0.008 denotes no input.
|
|
// TODO potentially refactor lib-jitsi-meet to expose this constant as a function. i.e. getSilenceLevel.
|
|
if (audioLevel > 0.008) {
|
|
this.emit(ACTIVE_DEVICE_DETECTED, { deviceId: device.deviceId,
|
|
deviceLabel: device.track.label,
|
|
audioLevel });
|
|
this.destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy the ActiveDeviceDetector, clean up the currently monitored devices associated JitsiLocalTracks.
|
|
*
|
|
* @returns {void}.
|
|
*/
|
|
destroy() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
for (const device of this._availableDevices) {
|
|
device.removeAllListeners();
|
|
device.stopStream();
|
|
}
|
|
|
|
this._destroyed = true;
|
|
}
|
|
}
|