// @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; /** * State flag, check if the instance was destroyed. */ _destroyed: boolean = false; /** * Create active device detector. * * @param {Array} micDeviceList - Device list that is monitored inside the service. * * @returns {ActiveDeviceDetector} */ static async create(micDeviceList: Array) { 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} availableDevices - Device list that is monitored inside the service. */ constructor(availableDevices: Array) { 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; } }