Bug 1406350 - part2: Create new gUM basic audio test using loopback setup. r?pehrsons draft
authorAlex Chronopoulos <achronop@gmail.com>
Wed, 17 Jan 2018 14:59:42 +0200
changeset 721551 7926048dd1672f283f65decc8f5efaed94fd7369
parent 721550 17f9a6a5392acb75b93a1f00c436b9ad101edbb7
child 721552 328ec4b692776863521e1a6d692aaa1f87c4dd5f
child 721627 584d7c0a0f211df2340a9aa7227c4a3b931f75b8
push id95869
push userachronop@gmail.com
push dateWed, 17 Jan 2018 13:06:43 +0000
reviewerspehrsons
bugs1406350
milestone59.0a1
Bug 1406350 - part2: Create new gUM basic audio test using loopback setup. r?pehrsons MozReview-Commit-ID: 7IQPdLSQy8a
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_getUserMedia_GC_MediaStream.html
dom/media/tests/mochitest/test_getUserMedia_basicAudio_loopback.html
dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -11,23 +11,73 @@ var Ci = SpecialPowers.Ci;
 var FAKE_ENABLED = true;
 var TEST_AUDIO_FREQ = 1000;
 try {
   var audioDevice = SpecialPowers.getCharPref('media.audio_loopback_dev');
   var videoDevice = SpecialPowers.getCharPref('media.video_loopback_dev');
   dump('TEST DEVICES: Using media devices:\n');
   dump('audio: ' + audioDevice + '\nvideo: ' + videoDevice + '\n');
   FAKE_ENABLED = false;
-  TEST_AUDIO_FREQ = 440;
+  // It will be updated to 440 when/if DefaultLoopbackTone is instantiated.
+  TEST_AUDIO_FREQ = -1;
 } catch (e) {
   dump('TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n');
   FAKE_ENABLED = true;
 }
 
 /**
+ *  Global flag to skip LoopbackTone
+ */
+var DISABLE_LOOPBACK_TONE = false
+/**
+ * Helper class to setup a sine tone of a given frequency.
+ */
+class LoopbackTone {
+  constructor(audioContext, frequency) {
+    if (!audioContext) {
+      throw new Error("You must provide a valid AudioContext");
+    }
+    this.oscNode = audioContext.createOscillator();
+    var gainNode = audioContext.createGain();
+    gainNode.gain.value = 0.5;
+    this.oscNode.connect(gainNode);
+    gainNode.connect(audioContext.destination);
+    this.changeFrequency(frequency);
+  }
+
+  // Method should be used when FAKE_ENABLED is false.
+  start() {
+    if (!this.oscNode) {
+      throw new Error("Attempt to start a stopped LoopbackTone");
+    }
+    info(`Start loopback tone at ${this.oscNode.frequency.value}`);
+    this.oscNode.start();
+  }
+
+  // Change the frequency of the tone. It can be used after start.
+  // Frequency will change on the fly. No need to stop and create a new instance.
+  changeFrequency(frequency) {
+    if (!this.oscNode) {
+      throw new Error("Attempt to change frequency on a stopped LoopbackTone");
+    }
+    this.oscNode.frequency.value = frequency;
+  }
+
+  stop() {
+    if (!this.oscNode) {
+      throw new Error("Attempt to stop a stopped LoopbackTone");
+    }
+    this.oscNode.stop();
+    this.oscNode = null;
+  }
+};
+// Object that holds the default loopback tone.
+var DefaultLoopbackTone = null;
+
+/**
  * This class provides helpers around analysing the audio content in a stream
  * using WebAudio AnalyserNodes.
  *
  * @constructor
  * @param {object} stream
  *                 A MediaStream object whose audio track we shall analyse.
  */
 function AudioStreamAnalyser(ac, stream) {
@@ -246,16 +296,18 @@ function realCreateHTML(meta) {
  */
 function createMediaElement(type, id) {
   const element = document.createElement(type);
   element.setAttribute('id', id);
   element.setAttribute('height', 100);
   element.setAttribute('width', 150);
   element.setAttribute('controls', 'controls');
   element.setAttribute('autoplay', 'autoplay');
+  element.setAttribute('muted', 'muted');
+  element.muted = true;
   document.getElementById('content').appendChild(element);
 
   return element;
 }
 
 /**
  * Returns an existing element for the given track with the given idPrefix,
  * as it was added by createMediaElementForTrack().
@@ -291,16 +343,33 @@ function createMediaElementForTrack(trac
 /**
  * Wrapper function for mediaDevices.getUserMedia used by some tests. Whether
  * to use fake devices or not is now determined in pref further below instead.
  *
  * @param {Dictionary} constraints
  *        The constraints for this mozGetUserMedia callback
  */
 function getUserMedia(constraints) {
+  if (!FAKE_ENABLED
+      && !constraints.fake
+      && constraints.audio
+      && !DISABLE_LOOPBACK_TONE) {
+    // Loopback device is configured, start the default loopback tone
+    if (!DefaultLoopbackTone) {
+      TEST_AUDIO_FREQ = 440;
+      DefaultLoopbackTone = new LoopbackTone(new AudioContext, TEST_AUDIO_FREQ);
+      DefaultLoopbackTone.start();
+    }
+    // Disable input processing mode when it's not explicity enabled.
+    // This is to avoid distortion of the loopback tone
+    constraints.audio = Object.assign({}, {autoGainControl: false}
+                                        , {echoCancellation: false}
+                                        , {noiseSuppression: false}
+                                        , constraints.audio);
+  }
   info("Call getUserMedia for " + JSON.stringify(constraints));
   return navigator.mediaDevices.getUserMedia(constraints)
     .then(stream => (checkMediaStreamTracks(constraints, stream), stream));
 }
 
 // These are the promises we use to track that the prerequisites for the test
 // are in place before running it.
 var setTestOptions;
@@ -323,16 +392,22 @@ function setupEnvironment() {
       ['media.navigator.permission.disabled', true],
       ['media.navigator.streams.fake', FAKE_ENABLED],
       ['media.getusermedia.screensharing.enabled', true],
       ['media.getusermedia.audiocapture.enabled', true],
       ['media.recorder.audio_node.enabled', true]
     ]
   };
 
+  if (!FAKE_ENABLED) {
+    defaultMochitestPrefs.set.push(
+      ["media.volume_scale", "1"],
+    );
+  }
+
   const isAndroid = !!navigator.userAgent.includes("Android");
 
   if (isAndroid) {
     defaultMochitestPrefs.set.push(
       ["media.navigator.video.default_width", 320],
       ["media.navigator.video.default_height", 240],
       ["media.navigator.video.max_fr", 10],
       ["media.autoplay.enabled", true]
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -46,16 +46,18 @@ skip-if = true # needed by test_enumerat
 skip-if = os == 'android'
 [test_getUserMedia_active_autoplay.html]
 [test_getUserMedia_audioCapture.html]
 skip-if = toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333)
 [test_getUserMedia_addTrackRemoveTrack.html]
 skip-if = android_version == '18' || os == 'linux' # android(Bug 1189784, timeouts on 4.3 emulator), linux bug 1377450
 [test_getUserMedia_addtrack_removetrack_events.html]
 skip-if = os == 'linux' && debug # Bug 1389983
+[test_getUserMedia_basicAudio_loopback.html]
+skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
 [test_getUserMedia_basicAudio.html]
 [test_getUserMedia_basicVideo.html]
 [test_getUserMedia_basicVideo_playAfterLoadedmetadata.html]
 [test_getUserMedia_basicScreenshare.html]
 skip-if = toolkit == 'android' # no screenshare on android
 [test_getUserMedia_basicTabshare.html]
 skip-if = toolkit == 'android' # no windowshare on android
 [test_getUserMedia_basicWindowshare.html]
--- a/dom/media/tests/mochitest/test_getUserMedia_GC_MediaStream.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_GC_MediaStream.html
@@ -24,16 +24,20 @@
 
     copies = [];
     await new Promise(r => SpecialPowers.exactGC(r));
     is(await SpecialStream.countUnderlyingStreams(), startStreams,
        "MediaStreams should have been collected");
   }
 
   runTest(async () => {
+    // We do not need LoopbackTone because it is not used
+    // and creates extra streams that affect the result
+    DISABLE_LOOPBACK_TONE = true;
+
     let gUMStream = await getUserMedia({video: true});
     info("Testing GC of copy constructor");
     await testGC(gUMStream, 10, s => new MediaStream(s));
 
     info("Testing GC of track-array constructor");
     await testGC(gUMStream, 10, s => new MediaStream(s.getTracks()));
 
     info("Testing GC of empty constructor plus addTrack");
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicAudio_loopback.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+  createHTML({
+    title: "getUserMedia Basic Audio Test Loopback",
+    bug: "1406350",
+    visible: true
+  });
+  /**
+   * Run a test to verify the use of LoopbackTone as audio input.
+   */
+  scriptsReady
+  .then(() => FAKE_ENABLED = false)
+  .then(() => runTestWhenReady( async () => {
+    // At this point DefaultLoopbackTone has been instantiated
+    // automatically on frequency TEST_AUDIO_FREQ (440 Hz). Verify
+    // that a tone is detected on that frequency.
+    info("Capturing at default frequency");
+    let stream = await getUserMedia({audio: true});
+
+    let audioContext = new AudioContext();
+    let analyser = new AudioStreamAnalyser(audioContext, stream);
+    analyser.enableDebugCanvas();
+    await analyser.waitForAnalysisSuccess( array => {
+      // High energy on 1000 Hz low energy around that
+      const freg_50Hz   = array[analyser.binIndexForFrequency(50)];
+      const freq        = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+      const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+      info("Analysing audio frequency - low:target:high = "
+              + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+      return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+    })
+
+    // Use the LoopbackTone API to change the frequency of the default tone.
+    // Verify that a tone is detected on the new frequency (800 Hz).
+    info("Change loopback tone frequency");
+    DefaultLoopbackTone.changeFrequency(800);
+    await analyser.waitForAnalysisSuccess( array => {
+      const freg_50Hz   = array[analyser.binIndexForFrequency(50)];
+      const freq        = array[analyser.binIndexForFrequency(800)];
+      const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+      info("Analysing audio frequency - low:target:high = "
+              + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+      return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+    })
+
+    // Create a second tone at a different frequency.
+    // Verify that both tones are detected.
+    info("Multiple loopback tones");
+    DefaultLoopbackTone.changeFrequency(TEST_AUDIO_FREQ);
+    let second_tone = new LoopbackTone(audioContext, 2000);
+    second_tone.start();
+    await analyser.waitForAnalysisSuccess( array => {
+      const freg_50Hz   = array[analyser.binIndexForFrequency(50)];
+      const freq        = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+      const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+      const freq_4000Hz = array[analyser.binIndexForFrequency(4000)];
+
+      info("Analysing audio frequency - low:target1:target2:high = "
+              + freg_50Hz + ':' + freq + ':' + freq_2000Hz + ':' + freq_4000Hz);
+      return freg_50Hz < 50 && freq > 200 && freq_2000Hz > 200 && freq_4000Hz < 50;
+    })
+
+    // Stop all tones and verify that there is no audio on the given frequencies.
+    info("Stop all loopback tones");
+    DefaultLoopbackTone.stop();
+    second_tone.stop()
+    await analyser.waitForAnalysisSuccess( array => {
+      const freg_50Hz   = array[analyser.binIndexForFrequency(50)];
+      const freq        = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+      const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+      info("Analysing audio frequency - low:target:high = "
+              + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+      return freg_50Hz < 50 && freq < 50 && freq_2000Hz < 50;
+    })
+  }))
+  .then(() => finish())
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -34,17 +34,17 @@
     ok(sender, "We have a sender for video");
     ok(allLocalStreamsHaveSender(pc),
        "Shouldn't have any local streams without a corresponding sender");
     ok(allRemoteStreamsHaveReceiver(pc),
        "Shouldn't have any remote streams without a corresponding receiver");
 
     var newTrack;
     var audiotrack;
-    return navigator.mediaDevices.getUserMedia({video:true, audio:true})
+    return getUserMedia({video:true, audio:true})
       .then(newStream => {
         window.grip = newStream;
         newTrack = newStream.getVideoTracks()[0];
         audiotrack = newStream.getAudioTracks()[0];
         isnot(newTrack, sender.track, "replacing with a different track");
         ok(!pc.getLocalStreams().some(s => s == newStream),
            "from a different stream");
         // Use wrapper function, since it updates expected tracks