Bug 1406350 - part2: Create new gUM basic audio test using loopback setup. r?pehrsons
MozReview-Commit-ID: 7IQPdLSQy8a
--- 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