jiti-meet/react/features/base/devices/services/device-detect/ActiveDeviceDetector.js

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;
}
}