You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ActiveDeviceDetector.js 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // @flow
  2. import EventEmitter from 'events';
  3. import { ACTIVE_DEVICE_DETECTED } from './Events';
  4. import logger from '../../logger';
  5. import JitsiMeetJS from '../../../lib-jitsi-meet';
  6. const JitsiTrackEvents = JitsiMeetJS.events.track;
  7. // If after 3000 ms the detector did not find any active devices consider that there aren't any usable ones available
  8. // i.e. audioLevel > 0.008
  9. const DETECTION_TIMEOUT = 3000;
  10. /**
  11. * Detect active input devices based on their audio levels, currently this is very simplistic. It works by simply
  12. * checking all monitored devices for TRACK_AUDIO_LEVEL_CHANGED if a device has a audio level > 0.008 ( 0.008 is
  13. * no input from the perspective of a JitsiLocalTrack ), at which point it triggers a ACTIVE_DEVICE_DETECTED event.
  14. * If there are no devices that meet that criteria for DETECTION_TIMEOUT an event with empty deviceLabel parameter
  15. * will be triggered,
  16. * signaling that no active device was detected.
  17. * TODO Potentially improve the active device detection using rnnoise VAD scoring.
  18. */
  19. export class ActiveDeviceDetector extends EventEmitter {
  20. /**
  21. * Currently monitored devices.
  22. */
  23. _availableDevices: Array<Object>;
  24. /**
  25. * State flag, check if the instance was destroyed.
  26. */
  27. _destroyed: boolean = false;
  28. /**
  29. * Create active device detector.
  30. *
  31. * @param {Array<MediaDeviceInfo>} micDeviceList - Device list that is monitored inside the service.
  32. *
  33. * @returns {ActiveDeviceDetector}
  34. */
  35. static async create(micDeviceList: Array<MediaDeviceInfo>) {
  36. const availableDevices = [];
  37. try {
  38. for (const micDevice of micDeviceList) {
  39. const localTrack = await JitsiMeetJS.createLocalTracks({
  40. devices: [ 'audio' ],
  41. micDeviceId: micDevice.deviceId
  42. });
  43. // We provide a specific deviceId thus we expect a single JitsiLocalTrack to be returned.
  44. availableDevices.push(localTrack[0]);
  45. }
  46. return new ActiveDeviceDetector(availableDevices);
  47. } catch (error) {
  48. logger.error('Cleaning up remaining JitsiLocalTrack, due to ActiveDeviceDetector create fail!');
  49. for (const device of availableDevices) {
  50. device.stopStream();
  51. }
  52. throw error;
  53. }
  54. }
  55. /**
  56. * Constructor.
  57. *
  58. * @param {Array<Object>} availableDevices - Device list that is monitored inside the service.
  59. */
  60. constructor(availableDevices: Array<Object>) {
  61. super();
  62. this._availableDevices = availableDevices;
  63. // Setup event handlers for monitored devices.
  64. for (const device of this._availableDevices) {
  65. device.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, audioLevel => {
  66. this._handleAudioLevelEvent(device, audioLevel);
  67. });
  68. }
  69. // Cancel the detection in case no devices was found with audioLevel > 0 in te set timeout.
  70. setTimeout(this._handleDetectionTimeout.bind(this), DETECTION_TIMEOUT);
  71. }
  72. /**
  73. * Handle what happens if no device publishes a score in the defined time frame, i.e. Emit an event with empty
  74. * deviceLabel.
  75. *
  76. * @returns {void}
  77. */
  78. _handleDetectionTimeout() {
  79. if (!this._destroyed) {
  80. this.emit(ACTIVE_DEVICE_DETECTED, { deviceLabel: '',
  81. audioLevel: 0 });
  82. this.destroy();
  83. }
  84. }
  85. /**
  86. * Handles audio level event generated by JitsiLocalTracks.
  87. *
  88. * @param {Object} device - Label of the emitting track.
  89. * @param {number} audioLevel - Audio level generated by device.
  90. *
  91. * @returns {void}
  92. */
  93. _handleAudioLevelEvent(device, audioLevel) {
  94. if (!this._destroyed) {
  95. // This is a very naive approach but works is most, a more accurate approach would ne to use rnnoise
  96. // in order to limit the number of false positives.
  97. // The 0.008 constant is due to how LocalStatsCollector from lib-jitsi-meet publishes audio-levels, in this
  98. // case 0.008 denotes no input.
  99. // TODO potentially refactor lib-jitsi-meet to expose this constant as a function. i.e. getSilenceLevel.
  100. if (audioLevel > 0.008) {
  101. this.emit(ACTIVE_DEVICE_DETECTED, { deviceId: device.deviceId,
  102. deviceLabel: device.track.label,
  103. audioLevel });
  104. this.destroy();
  105. }
  106. }
  107. }
  108. /**
  109. * Destroy the ActiveDeviceDetector, clean up the currently monitored devices associated JitsiLocalTracks.
  110. *
  111. * @returns {void}.
  112. */
  113. destroy() {
  114. if (this._destroyed) {
  115. return;
  116. }
  117. for (const device of this._availableDevices) {
  118. device.removeAllListeners();
  119. device.stopStream();
  120. }
  121. this._destroyed = true;
  122. }
  123. }