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.

LocalStatsCollector.ts 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { getLogger } from '@jitsi/logger';
  2. const logger = getLogger('modules/statistics/LocalStatsCollector');
  3. /**
  4. * Size of the webaudio analyzer buffer.
  5. * @type {number}
  6. */
  7. const WEBAUDIO_ANALYZER_FFT_SIZE: number = 2048;
  8. /**
  9. * Value of the webaudio analyzer smoothing time parameter.
  10. * @type {number}
  11. */
  12. const WEBAUDIO_ANALYZER_SMOOTING_TIME: number = 0.8;
  13. /**
  14. * The audio context.
  15. * @type {AudioContext}
  16. */
  17. let context: AudioContext | null = null;
  18. /**
  19. * Converts time domain data array to audio level.
  20. * @param samples the time domain data array.
  21. * @returns {number} the audio level
  22. */
  23. function timeDomainDataToAudioLevel(samples: Uint8Array): number {
  24. let maxVolume = 0;
  25. const length = samples.length;
  26. for (let i = 0; i < length; i++) {
  27. if (maxVolume < samples[i]) {
  28. maxVolume = samples[i];
  29. }
  30. }
  31. return Number.parseFloat(((maxVolume - 127) / 128).toFixed(3));
  32. }
  33. /**
  34. * Animates audio level change
  35. * @param newLevel the new audio level
  36. * @param lastLevel the last audio level
  37. * @returns {Number} the audio level to be set
  38. */
  39. function animateLevel(newLevel: number, lastLevel: number): number {
  40. let value = 0;
  41. const diff = lastLevel - newLevel;
  42. if (diff > 0.2) {
  43. value = lastLevel - 0.2;
  44. } else if (diff < -0.4) {
  45. value = lastLevel + 0.4;
  46. } else {
  47. value = newLevel;
  48. }
  49. return Number.parseFloat(value.toFixed(3));
  50. }
  51. /**
  52. * Provides statistics for the local stream.
  53. */
  54. export default class LocalStatsCollector {
  55. stream: MediaStream;
  56. intervalId: Timeout | null;
  57. intervalMilis: number;
  58. audioLevel: number;
  59. callback: (audioLevel: number) => void;
  60. source: MediaStreamAudioSourceNode | null;
  61. analyser: AnalyserNode | null;
  62. /**
  63. * Creates a new instance of LocalStatsCollector.
  64. *
  65. * @param {MediaStream} stream - the local stream
  66. * @param {number} interval - stats refresh interval given in ms.
  67. * @param {Function} callback - function that receives the audio levels.
  68. * @constructor
  69. */
  70. constructor(
  71. stream: MediaStream,
  72. interval: number,
  73. callback: (audioLevel: number) => void
  74. ) {
  75. this.stream = stream;
  76. this.intervalId = null;
  77. this.intervalMilis = interval;
  78. this.audioLevel = 0;
  79. this.callback = callback;
  80. this.source = null;
  81. this.analyser = null;
  82. }
  83. /**
  84. * Starts the collecting the statistics.
  85. */
  86. start(): void {
  87. if (!LocalStatsCollector.isLocalStatsSupported()) {
  88. return;
  89. }
  90. context!.resume();
  91. this.analyser = context!.createAnalyser();
  92. this.analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
  93. this.analyser.fftSize = WEBAUDIO_ANALYZER_FFT_SIZE;
  94. this.source = context!.createMediaStreamSource(this.stream);
  95. this.source.connect(this.analyser);
  96. this.intervalId = setInterval(
  97. () => {
  98. const array = new Uint8Array(this.analyser!.frequencyBinCount);
  99. this.analyser!.getByteTimeDomainData(array);
  100. const audioLevel = timeDomainDataToAudioLevel(array);
  101. // Set the audio levels always as NoAudioSignalDetection now
  102. // uses audio levels from LocalStatsCollector and waits for
  103. // atleast 4 secs for a no audio signal before displaying the
  104. // notification on the UI.
  105. this.audioLevel = animateLevel(audioLevel, this.audioLevel);
  106. this.callback(this.audioLevel);
  107. },
  108. this.intervalMilis
  109. );
  110. }
  111. /**
  112. * Stops collecting the statistics.
  113. */
  114. stop(): void {
  115. if (this.intervalId) {
  116. clearInterval(this.intervalId);
  117. this.intervalId = null;
  118. }
  119. this.analyser?.disconnect();
  120. this.analyser = null;
  121. this.source?.disconnect();
  122. this.source = null;
  123. }
  124. /**
  125. * Initialize collector.
  126. */
  127. static init(): void {
  128. LocalStatsCollector.connectAudioContext();
  129. }
  130. /**
  131. * Checks if the environment has the necessary conditions to support
  132. * collecting stats from local streams.
  133. *
  134. * @returns {boolean}
  135. */
  136. static isLocalStatsSupported(): boolean {
  137. return Boolean(window?.AudioContext);
  138. }
  139. /**
  140. * Disconnects the audio context.
  141. */
  142. static async disconnectAudioContext(): Promise<void> {
  143. if (context) {
  144. logger.info('Disconnecting audio context');
  145. await context.close();
  146. context = null;
  147. }
  148. }
  149. /**
  150. * Connects the audio context.
  151. */
  152. static connectAudioContext(): void {
  153. if (!LocalStatsCollector.isLocalStatsSupported()) {
  154. return;
  155. }
  156. logger.info('Connecting audio context');
  157. context = new AudioContext();
  158. context.suspend();
  159. }
  160. }