chromium/content/test/data/media/webrtc_test_audio.js

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Audio test utilities.

// GetStats reports audio output energy in the [0, 32768] range.
var MAX_AUDIO_OUTPUT_ENERGY = 32768;

// Queries WebRTC stats on |peerConnection| to find out whether audio is playing
// on the connection. Note this does not necessarily mean the audio is actually
// playing out (for instance if there's a bug in the WebRTC web media player).
function ensureAudioPlaying(peerConnection) {
  return new Promise((resolve, reject) => {
    var attempt = 1;
    gatherAudioLevelSamples(peerConnection, function(samples) {
      if (identifyFakeDeviceSignal_(samples)) {
        resolve();
        return true;
      }
      if (attempt++ % 5 == 0) {
        console.log('Still waiting for the fake audio signal.');
        console.log('Dumping samples so far for analysis: ' + samples);
      }
      return false;
    });
  });
}

// Queries WebRTC stats on |peerConnection| to find out whether audio is muted
// on the connection.
function ensureSilence(peerConnection) {
  return new Promise((resolve, reject) => {
    var attempt = 1;
    gatherAudioLevelSamples(peerConnection, function(samples) {
      if (identifySilence_(samples)) {
        resolve();
        return true;
      }
      if (attempt++ % 5 == 0) {
        console.log('Still waiting for audio to go silent.');
        console.log('Dumping samples so far for analysis: ' + samples);
      }
      return false;
    });
  });
}

// Not sure if this is a bug, but sometimes we get several audio ssrc's where
// just reports audio level zero. Think of the nonzero level as the more
// credible one here. http://crbug.com/479147.
function workAroundSeveralReportsIssue(audioOutputLevels) {
  if (audioOutputLevels.length == 1) {
    return audioOutputLevels[0];
  }

  console.log("Hit issue where one report batch returns two or more reports " +
              "with audioReportLevel; got " + audioOutputLevels);

  return Math.max(audioOutputLevels[0], audioOutputLevels[1]);
}

// Gathers samples from WebRTC stats as fast as possible for and calls back
// |callback| continuously with an array with numbers in the [0, 32768] range.
// The array will grow continuously over time as we gather more samples. The
// |callback| should return true when it is satisfied. It will be called about
// once a second and can contain expensive processing (but faster = better).
//
// There are no guarantees for how often we will be able to collect values,
// but this function deliberately avoids setTimeout calls in order be as
// insensitive as possible to starvation (particularly when this code runs in
// parallel with other tests on a heavily loaded bot).
function gatherAudioLevelSamples(peerConnection, callback) {
  console.log('Gathering audio samples...');
  var callbackIntervalMs = 1000;
  var audioLevelSamples = []

  // If this times out and never found any audio output levels, the call
  // probably doesn't have an audio stream.
  var lastRunAt = new Date();
  var gotStats = function(report) {
    audioOutputLevels = getAudioLevelFromStats_(report);
    if (audioOutputLevels.length == 0) {
      // The call probably isn't up yet.
      peerConnection.getStats().then(gotStats);
      return;
    }
    var outputLevel = workAroundSeveralReportsIssue(audioOutputLevels);
    audioLevelSamples.push(outputLevel);

    var elapsed = new Date() - lastRunAt;
    if (elapsed > callbackIntervalMs) {
      if (callback(audioLevelSamples)) {
        console.log('Done gathering samples: we found what we looked for.');
        return;
      }
      lastRunAt = new Date();
    }
    // Otherwise, continue as fast as we can.
    peerConnection.getStats().then(gotStats);
  }
  peerConnection.getStats().then(gotStats);
}

/**
* Tries to identify the beep-every-half-second signal generated by the fake
* audio device in media/capture/video/fake_video_capture_device.cc. Fails the
* test if we can't see a signal. The samples should have been gathered over at
* least two seconds since we expect to see at least three "peaks" in there
* (we should see either 3 or 4 depending on how things line up).
*
* @private
*/
function identifyFakeDeviceSignal_(samples) {
  var numPeaks = 0;
  var threshold = MAX_AUDIO_OUTPUT_ENERGY * 0.7;
  var currentlyOverThreshold = false;

  // Detect when we have been been over the threshold and is going back again
  // (i.e. count peaks). We should see about two peaks per second.
  for (var i = 0; i < samples.length; ++i) {
    if (currentlyOverThreshold && samples[i] < threshold)
      numPeaks++;
    currentlyOverThreshold = samples[i] >= threshold;
  }

  var expectedPeaks = 3;
  console.log(numPeaks + '/' + expectedPeaks + ' signal peaks identified.');
  return numPeaks >= expectedPeaks;
}

/**
 * @private
 */
function identifySilence_(samples) {
  // Look at the last 10K samples only to make detection a bit faster.
  var window = samples.slice(-10000);

  var average = 0;
  for (var i = 0; i < window.length; ++i)
    average += window[i] / window.length;

  // If silent (like when muted), we should get very near zero audio level.
  console.log('Average audio level (last 10k samples): ' + average);

  return average < 0.01 * MAX_AUDIO_OUTPUT_ENERGY;
}

/**
 * @private
 */
function getAudioLevelFromStats_(report) {
  // var reports = response.result();
  const audioOutputLevels = [];
  for (const stats of report.values()) {
    if (stats.type != 'inbound-rtp' || stats.kind != 'audio' ||
        stats.audioLevel == undefined) {
      continue;
    }
    // Convert from [0,1] range to [0, 32768].
    audioOutputLevels.push(stats.audioLevel*32768);
  }
  return audioOutputLevels;
}