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.js 4.2KB

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