diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/media/webrtc/tests/mochitests | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webrtc/tests/mochitests')
292 files changed, 29968 insertions, 0 deletions
diff --git a/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js new file mode 100644 index 0000000000..d3872f1519 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var browser = Services.wm.getMostRecentWindow("navigator:browser"); +var connection = browser.navigator.mozMobileConnections[0]; + +// provide a fake APN and enable data connection. +// enable 3G radio +function enableRadio() { + if (connection.radioState !== "enabled") { + connection.setRadioEnabled(true); + } +} + +// disable 3G radio +function disableRadio() { + if (connection.radioState === "enabled") { + connection.setRadioEnabled(false); + } +} + +addMessageListener("prepare-network", function (message) { + connection.addEventListener("datachange", function onDataChange() { + if (connection.data.connected) { + connection.removeEventListener("datachange", onDataChange); + Services.prefs.setIntPref("network.proxy.type", 2); + sendAsyncMessage("network-ready", true); + } + }); + + enableRadio(); +}); + +addMessageListener("network-cleanup", function (message) { + connection.addEventListener("datachange", function onDataChange() { + if (!connection.data.connected) { + connection.removeEventListener("datachange", onDataChange); + Services.prefs.setIntPref("network.proxy.type", 2); + sendAsyncMessage("network-disabled", true); + } + }); + disableRadio(); +}); diff --git a/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js new file mode 100644 index 0000000000..1e8be3a397 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +// This is only usable from the parent process, even for doing simple stuff like +// serializing a cert. +var gCertMaker = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +var gCertOverrides = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService +); + +addMessageListener("add-turns-certs", certs => { + var port = 5349; + certs.forEach(certDescription => { + var cert = gCertMaker.constructX509FromBase64(certDescription.cert); + gCertOverrides.rememberValidityOverride( + certDescription.hostname, + port, + {}, + cert, + false + ); + }); + sendAsyncMessage("certs-added"); +}); diff --git a/dom/media/webrtc/tests/mochitests/blacksilence.js b/dom/media/webrtc/tests/mochitests/blacksilence.js new file mode 100644 index 0000000000..5ea35f8a7f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/blacksilence.js @@ -0,0 +1,134 @@ +(function (global) { + "use strict"; + + // an invertible check on the condition. + // if the constraint is applied, then the check is direct + // if not applied, then the result should be reversed + function check(constraintApplied, condition, message) { + var good = constraintApplied ? condition : !condition; + message = + (constraintApplied ? "with" : "without") + + " constraint: should " + + (constraintApplied ? "" : "not ") + + message + + " = " + + (good ? "OK" : "waiting..."); + info(message); + return good; + } + + function mkElement(type) { + // This makes an unattached element. + // It's not rendered to save the cycles that costs on b2g emulator + // and it gets dropped (and GC'd) when the test is done. + var e = document.createElement(type); + e.width = 32; + e.height = 24; + document.getElementById("display").appendChild(e); + return e; + } + + // Runs checkFunc until it reports success. + // This is kludgy, but you have to wait for media to start flowing, and it + // can't be any old media, it has to include real data, for which we have no + // reliable signals to use as a trigger. + function periodicCheck(checkFunc) { + var resolve; + var done = false; + // This returns a function so that we create 10 closures in the loop, not + // one; and so that the timers don't all start straight away + var waitAndCheck = counter => () => { + if (done) { + return Promise.resolve(); + } + return new Promise(r => setTimeout(r, 200 << counter)).then(() => { + if (checkFunc()) { + done = true; + resolve(); + } + }); + }; + + var chain = Promise.resolve(); + for (var i = 0; i < 10; ++i) { + chain = chain.then(waitAndCheck(i)); + } + return new Promise(r => (resolve = r)); + } + + function isSilence(audioData) { + var silence = true; + for (var i = 0; i < audioData.length; ++i) { + if (audioData[i] !== 128) { + silence = false; + } + } + return silence; + } + + function checkAudio(constraintApplied, stream) { + var audio = mkElement("audio"); + audio.srcObject = stream; + audio.play(); + + var context = new AudioContext(); + var source = context.createMediaStreamSource(stream); + var analyser = context.createAnalyser(); + source.connect(analyser); + analyser.connect(context.destination); + + return periodicCheck(() => { + var sampleCount = analyser.frequencyBinCount; + info("got some audio samples: " + sampleCount); + var buffer = new Uint8Array(sampleCount); + analyser.getByteTimeDomainData(buffer); + + var silent = check( + constraintApplied, + isSilence(buffer), + "be silence for audio" + ); + return sampleCount > 0 && silent; + }).then(() => { + source.disconnect(); + analyser.disconnect(); + audio.pause(); + ok(true, "audio is " + (constraintApplied ? "" : "not ") + "silent"); + }); + } + + function checkVideo(constraintApplied, stream) { + var video = mkElement("video"); + video.srcObject = stream; + video.play(); + + return periodicCheck(() => { + try { + var canvas = mkElement("canvas"); + var ctx = canvas.getContext("2d"); + // Have to guard drawImage with the try as well, due to bug 879717. If + // we get an error, this round fails, but that failure is usually just + // transitory. + ctx.drawImage(video, 0, 0); + ctx.getImageData(0, 0, 1, 1); + return check( + constraintApplied, + false, + "throw on getImageData for video" + ); + } catch (e) { + return check( + constraintApplied, + e.name === "SecurityError", + "get a security error: " + e.name + ); + } + }).then(() => { + video.pause(); + ok(true, "video is " + (constraintApplied ? "" : "not ") + "protected"); + }); + } + + global.audioIsSilence = checkAudio; + global.videoIsBlack = checkVideo; +})(this); diff --git a/dom/media/webrtc/tests/mochitests/dataChannel.js b/dom/media/webrtc/tests/mochitests/dataChannel.js new file mode 100644 index 0000000000..eac52f96ab --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/dataChannel.js @@ -0,0 +1,352 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Returns the contents of a blob as text + * + * @param {Blob} blob + The blob to retrieve the contents from + */ +function getBlobContent(blob) { + return new Promise(resolve => { + var reader = new FileReader(); + // Listen for 'onloadend' which will always be called after a success or failure + reader.onloadend = event => resolve(event.target.result); + reader.readAsText(blob); + }); +} + +var commandsCreateDataChannel = [ + function PC_REMOTE_EXPECT_DATA_CHANNEL(test) { + test.pcRemote.expectDataChannel(); + }, + + function PC_LOCAL_CREATE_DATA_CHANNEL(test) { + var channel = test.pcLocal.createDataChannel({}); + is(channel.binaryType, "blob", channel + " is of binary type 'blob'"); + + is( + test.pcLocal.signalingState, + STABLE, + "Create datachannel does not change signaling state" + ); + return test.pcLocal.observedNegotiationNeeded; + }, +]; + +var commandsWaitForDataChannel = [ + function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) { + return test.pcLocal.dataChannels[0].opened; + }, + + function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) { + return test.pcRemote.nextDataChannel.then(channel => channel.opened); + }, +]; + +var commandsCheckDataChannel = [ + function SEND_MESSAGE(test) { + var message = "Lorem ipsum dolor sit amet"; + + info("Sending message:" + message); + return test.send(message).then(result => { + is( + result.data, + message, + "Message correctly transmitted from pcLocal to pcRemote." + ); + }); + }, + + function SEND_BLOB(test) { + var contents = "At vero eos et accusam et justo duo dolores et ea rebum."; + var blob = new Blob([contents], { type: "text/plain" }); + + info("Sending blob"); + return test + .send(blob) + .then(result => { + ok(result.data instanceof Blob, "Received data is of instance Blob"); + is(result.data.size, blob.size, "Received data has the correct size."); + + return getBlobContent(result.data); + }) + .then(recv_contents => + is(recv_contents, contents, "Received data has the correct content.") + ); + }, + + function CREATE_SECOND_DATA_CHANNEL(test) { + return test.createDataChannel({}).then(result => { + is( + result.remote.binaryType, + "blob", + "remote data channel is of binary type 'blob'" + ); + }); + }, + + function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) { + var channels = test.pcRemote.dataChannels; + var message = "I am the Omega"; + + info("Sending message:" + message); + return test.send(message).then(result => { + is( + channels.indexOf(result.channel), + channels.length - 1, + "Last channel used" + ); + is(result.data, message, "Received message has the correct content."); + }); + }, + + function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) { + var message = "Message through 1st channel"; + var options = { + sourceChannel: test.pcLocal.dataChannels[0], + targetChannel: test.pcRemote.dataChannels[0], + }; + + info("Sending message:" + message); + return test.send(message, options).then(result => { + is( + test.pcRemote.dataChannels.indexOf(result.channel), + 0, + "1st channel used" + ); + is(result.data, message, "Received message has the correct content."); + }); + }, + + function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) { + var message = "Return a message also through 1st channel"; + var options = { + sourceChannel: test.pcRemote.dataChannels[0], + targetChannel: test.pcLocal.dataChannels[0], + }; + + info("Sending message:" + message); + return test.send(message, options).then(result => { + is( + test.pcLocal.dataChannels.indexOf(result.channel), + 0, + "1st channel used" + ); + is(result.data, message, "Return message has the correct content."); + }); + }, + + function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_RETRANSMITS(test) { + var options = { + negotiated: true, + id: 5, + protocol: "foo/bar", + ordered: false, + maxRetransmits: 500, + }; + return test.createDataChannel(options).then(result => { + is( + result.local.binaryType, + "blob", + result.remote + " is of binary type 'blob'" + ); + is( + result.local.id, + options.id, + result.local + " id is:" + result.local.id + ); + is( + result.local.protocol, + options.protocol, + result.local + " protocol is:" + result.local.protocol + ); + is( + result.local.reliable, + false, + result.local + " reliable is:" + result.local.reliable + ); + is( + result.local.ordered, + options.ordered, + result.local + " ordered is:" + result.local.ordered + ); + is( + result.local.maxRetransmits, + options.maxRetransmits, + result.local + " maxRetransmits is:" + result.local.maxRetransmits + ); + is( + result.local.maxPacketLifeTime, + null, + result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime + ); + + is( + result.remote.binaryType, + "blob", + result.remote + " is of binary type 'blob'" + ); + is( + result.remote.id, + options.id, + result.remote + " id is:" + result.remote.id + ); + is( + result.remote.protocol, + options.protocol, + result.remote + " protocol is:" + result.remote.protocol + ); + is( + result.remote.reliable, + false, + result.remote + " reliable is:" + result.remote.reliable + ); + is( + result.remote.ordered, + options.ordered, + result.remote + " ordered is:" + result.remote.ordered + ); + is( + result.remote.maxRetransmits, + options.maxRetransmits, + result.remote + " maxRetransmits is:" + result.remote.maxRetransmits + ); + is( + result.remote.maxPacketLifeTime, + null, + result.remote + + " maxPacketLifeTime is:" + + result.remote.maxPacketLifeTime + ); + }); + }, + + function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) { + var channels = test.pcRemote.dataChannels; + var message = "I am the walrus; Goo goo g'joob"; + + info("Sending message:" + message); + return test.send(message).then(result => { + is( + channels.indexOf(result.channel), + channels.length - 1, + "Last channel used" + ); + is(result.data, message, "Received message has the correct content."); + }); + }, + + function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_PACKET_LIFE_TIME(test) { + var options = { + ordered: false, + maxPacketLifeTime: 10, + }; + return test.createDataChannel(options).then(result => { + is( + result.local.binaryType, + "blob", + result.local + " is of binary type 'blob'" + ); + is( + result.local.protocol, + "", + result.local + " protocol is:" + result.local.protocol + ); + is( + result.local.reliable, + false, + result.local + " reliable is:" + result.local.reliable + ); + is( + result.local.ordered, + options.ordered, + result.local + " ordered is:" + result.local.ordered + ); + is( + result.local.maxRetransmits, + null, + result.local + " maxRetransmits is:" + result.local.maxRetransmits + ); + is( + result.local.maxPacketLifeTime, + options.maxPacketLifeTime, + result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime + ); + + is( + result.remote.binaryType, + "blob", + result.remote + " is of binary type 'blob'" + ); + is( + result.remote.protocol, + "", + result.remote + " protocol is:" + result.remote.protocol + ); + is( + result.remote.reliable, + false, + result.remote + " reliable is:" + result.remote.reliable + ); + is( + result.remote.ordered, + options.ordered, + result.remote + " ordered is:" + result.remote.ordered + ); + is( + result.remote.maxRetransmits, + null, + result.remote + " maxRetransmits is:" + result.remote.maxRetransmits + ); + is( + result.remote.maxPacketLifeTime, + options.maxPacketLifeTime, + result.remote + + " maxPacketLifeTime is:" + + result.remote.maxPacketLifeTime + ); + }); + }, + + function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL3(test) { + var channels = test.pcRemote.dataChannels; + var message = "Nice to see you working maxPacketLifeTime"; + + info("Sending message:" + message); + return test.send(message).then(result => { + is( + channels.indexOf(result.channel), + channels.length - 1, + "Last channel used" + ); + is(result.data, message, "Received message has the correct content."); + }); + }, +]; + +var commandsCheckLargeXfer = [ + function SEND_BIG_BUFFER(test) { + var size = 2 * 1024 * 1024; // SCTP internal buffer is now 1MB, so use 2MB to ensure the buffer gets full + var buffer = new ArrayBuffer(size); + // note: type received is always blob for binary data + var options = {}; + options.bufferedAmountLowThreshold = 64 * 1024; + info("Sending arraybuffer"); + return test.send(buffer, options).then(result => { + ok(result.data instanceof Blob, "Received data is of instance Blob"); + is(result.data.size, size, "Received data has the correct size."); + }); + }, +]; + +function addInitialDataChannel(chain) { + chain.insertBefore("PC_LOCAL_CREATE_OFFER", commandsCreateDataChannel); + chain.insertBefore( + "PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + commandsWaitForDataChannel + ); + chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS"); + chain.append(commandsCheckDataChannel); +} diff --git a/dom/media/webrtc/tests/mochitests/head.js b/dom/media/webrtc/tests/mochitests/head.js new file mode 100644 index 0000000000..9a4e5682a3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/head.js @@ -0,0 +1,1435 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var Cc = SpecialPowers.Cc; +var Ci = SpecialPowers.Ci; + +// Specifies if we want fake audio streams for this run +let WANT_FAKE_AUDIO = true; +// Specifies if we want fake video streams for this run +let WANT_FAKE_VIDEO = true; +let TEST_AUDIO_FREQ = 1000; + +/** + * Reads the current values of preferences affecting fake and loopback devices + * and sets the WANT_FAKE_AUDIO and WANT_FAKE_VIDEO gloabals appropriately. + */ +function updateConfigFromFakeAndLoopbackPrefs() { + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (audioDevice) { + WANT_FAKE_AUDIO = false; + dump("TEST DEVICES: Got loopback audio: " + audioDevice + "\n"); + } else { + WANT_FAKE_AUDIO = true; + dump( + "TEST DEVICES: No test device found in media.audio_loopback_dev, using fake audio streams.\n" + ); + } + let videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev", ""); + if (videoDevice) { + WANT_FAKE_VIDEO = false; + dump("TEST DEVICES: Got loopback video: " + videoDevice + "\n"); + } else { + WANT_FAKE_VIDEO = true; + dump( + "TEST DEVICES: No test device found in media.video_loopback_dev, using fake video streams.\n" + ); + } +} + +updateConfigFromFakeAndLoopbackPrefs(); + +/** + * 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 WANT_FAKE_AUDIO 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) { + this.audioContext = ac; + this.stream = stream; + this.sourceNodes = []; + this.analyser = this.audioContext.createAnalyser(); + // Setting values lower than default for speedier testing on emulators + this.analyser.smoothingTimeConstant = 0.2; + this.analyser.fftSize = 1024; + this.connectTrack = t => { + let source = this.audioContext.createMediaStreamSource( + new MediaStream([t]) + ); + this.sourceNodes.push(source); + source.connect(this.analyser); + }; + this.stream.getAudioTracks().forEach(t => this.connectTrack(t)); + this.onaddtrack = ev => this.connectTrack(ev.track); + this.stream.addEventListener("addtrack", this.onaddtrack); + this.data = new Uint8Array(this.analyser.frequencyBinCount); +} + +AudioStreamAnalyser.prototype = { + /** + * Get an array of frequency domain data for our stream's audio track. + * + * @returns {array} A Uint8Array containing the frequency domain data. + */ + getByteFrequencyData() { + this.analyser.getByteFrequencyData(this.data); + return this.data; + }, + + /** + * Append a canvas to the DOM where the frequency data are drawn. + * Useful to debug tests. + */ + enableDebugCanvas() { + var cvs = (this.debugCanvas = document.createElement("canvas")); + const content = document.getElementById("content"); + content.insertBefore(cvs, content.children[0]); + + // Easy: 1px per bin + cvs.width = this.analyser.frequencyBinCount; + cvs.height = 128; + cvs.style.border = "1px solid red"; + + var c = cvs.getContext("2d"); + c.fillStyle = "black"; + + var self = this; + function render() { + c.clearRect(0, 0, cvs.width, cvs.height); + var array = self.getByteFrequencyData(); + for (var i = 0; i < array.length; i++) { + c.fillRect(i, cvs.height - array[i] / 2, 1, cvs.height); + } + if (!cvs.stopDrawing) { + requestAnimationFrame(render); + } + } + requestAnimationFrame(render); + }, + + /** + * Stop drawing of and remove the debug canvas from the DOM if it was + * previously added. + */ + disableDebugCanvas() { + if (!this.debugCanvas || !this.debugCanvas.parentElement) { + return; + } + + this.debugCanvas.stopDrawing = true; + this.debugCanvas.parentElement.removeChild(this.debugCanvas); + }, + + /** + * Disconnects the input stream from our internal analyser node. + * Call this to reduce main thread processing, mostly necessary on slow + * devices. + */ + disconnect() { + this.disableDebugCanvas(); + this.sourceNodes.forEach(n => n.disconnect()); + this.sourceNodes = []; + this.stream.removeEventListener("addtrack", this.onaddtrack); + }, + + /** + * Return a Promise, that will be resolved when the function passed as + * argument, when called, returns true (meaning the analysis was a + * success). The promise is rejected if the cancel promise resolves first. + * + * @param {function} analysisFunction + * A function that performs an analysis, and resolves with true if the + * analysis was a success (i.e. it found what it was looking for) + * @param {promise} cancel + * A promise that on resolving will reject the promise we returned. + */ + async waitForAnalysisSuccess( + analysisFunction, + cancel = wait(60000, new Error("Audio analysis timed out")) + ) { + let aborted = false; + cancel.then(() => (aborted = true)); + + // We need to give the Analyser some time to start gathering data. + await wait(200); + + do { + await new Promise(resolve => requestAnimationFrame(resolve)); + if (aborted) { + throw await cancel; + } + } while (!analysisFunction(this.getByteFrequencyData())); + }, + + /** + * Return the FFT bin index for a given frequency. + * + * @param {double} frequency + * The frequency for whicht to return the bin number. + * @returns {integer} the index of the bin in the FFT array. + */ + binIndexForFrequency(frequency) { + return Math.round( + (frequency * this.analyser.fftSize) / this.audioContext.sampleRate + ); + }, + + /** + * Reverse operation, get the frequency for a bin index. + * + * @param {integer} index an index in an FFT array + * @returns {double} the frequency for this bin + */ + frequencyForBinIndex(index) { + return (index * this.audioContext.sampleRate) / this.analyser.fftSize; + }, +}; + +/** + * Creates a MediaStream with an audio track containing a sine tone at the + * given frequency. + * + * @param {AudioContext} ac + * AudioContext in which to create the OscillatorNode backing the stream + * @param {double} frequency + * The frequency in Hz of the generated sine tone + * @returns {MediaStream} the MediaStream containing sine tone audio track + */ +function createOscillatorStream(ac, frequency) { + var osc = ac.createOscillator(); + osc.frequency.value = frequency; + + var oscDest = ac.createMediaStreamDestination(); + osc.connect(oscDest); + osc.start(); + return oscDest.stream; +} + +/** + * Create the necessary HTML elements for head and body as used by Mochitests + * + * @param {object} meta + * Meta information of the test + * @param {string} meta.title + * Description of the test + * @param {string} [meta.bug] + * Bug the test was created for + * @param {boolean} [meta.visible=false] + * Visibility of the media elements + */ +function realCreateHTML(meta) { + var test = document.getElementById("test"); + + // Create the head content + var elem = document.createElement("meta"); + elem.setAttribute("charset", "utf-8"); + document.head.appendChild(elem); + + var title = document.createElement("title"); + title.textContent = meta.title; + document.head.appendChild(title); + + // Create the body content + var anchor = document.createElement("a"); + anchor.textContent = meta.title; + if (meta.bug) { + anchor.setAttribute( + "href", + "https://bugzilla.mozilla.org/show_bug.cgi?id=" + meta.bug + ); + } else { + anchor.setAttribute("target", "_blank"); + } + + document.body.insertBefore(anchor, test); + + var display = document.createElement("p"); + display.setAttribute("id", "display"); + document.body.insertBefore(display, test); + + var content = document.createElement("div"); + content.setAttribute("id", "content"); + content.style.display = meta.visible ? "block" : "none"; + document.body.appendChild(content); +} + +/** + * Creates an element of the given type, assigns the given id, sets the controls + * and autoplay attributes and adds it to the content node. + * + * @param {string} type + * Defining if we should create an "audio" or "video" element + * @param {string} id + * A string to use as the element id. + */ +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(). + * + * @param {MediaStreamTrack} track + * Track used as the element's source. + * @param {string} idPrefix + * A string to use as the element id. The track id will also be appended. + */ +function getMediaElementForTrack(track, idPrefix) { + return document.getElementById(idPrefix + "_" + track.id); +} + +/** + * Create a media element with a track as source and attach it to the content + * node. + * + * @param {MediaStreamTrack} track + * Track for use as source. + * @param {string} idPrefix + * A string to use as the element id. The track id will also be appended. + * @return {HTMLMediaElement} The created HTML media element + */ +function createMediaElementForTrack(track, idPrefix) { + const id = idPrefix + "_" + track.id; + const element = createMediaElement(track.kind, id); + element.srcObject = new MediaStream([track]); + + return element; +} + +/** + * 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 (!constraints.fake && constraints.audio) { + // 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); + return stream; + }); +} + +// These are the promises we use to track that the prerequisites for the test +// are in place before running it. +var setTestOptions; +var testConfigured = new Promise(r => (setTestOptions = r)); + +function pushPrefs(...p) { + return SpecialPowers.pushPrefEnv({ set: p }); +} + +async function withPrefs(prefs, func) { + await SpecialPowers.pushPrefEnv({ set: prefs }); + try { + return await func(); + } finally { + await SpecialPowers.popPrefEnv(); + } +} + +function setupEnvironment() { + var defaultMochitestPrefs = { + set: [ + ["media.peerconnection.enabled", true], + ["media.peerconnection.identity.timeout", 120000], + ["media.peerconnection.ice.stun_client_maximum_transmits", 14], + ["media.peerconnection.ice.trickle_grace_period", 30000], + ["media.navigator.permission.disabled", true], + // If either fake audio or video is desired we enable fake streams. + // If loopback devices are set they will be chosen instead of fakes in gecko. + ["media.navigator.streams.fake", WANT_FAKE_AUDIO || WANT_FAKE_VIDEO], + ["media.getusermedia.audiocapture.enabled", true], + ["media.getusermedia.screensharing.enabled", true], + ["media.getusermedia.window.focus_source.enabled", false], + ["media.recorder.audio_node.enabled", true], + ["media.peerconnection.ice.obfuscate_host_addresses", false], + ["media.peerconnection.nat_simulator.filtering_type", ""], + ["media.peerconnection.nat_simulator.mapping_type", ""], + ["media.peerconnection.nat_simulator.block_tcp", false], + ["media.peerconnection.nat_simulator.block_udp", false], + ["media.peerconnection.nat_simulator.redirect_address", ""], + ["media.peerconnection.nat_simulator.redirect_targets", ""], + ], + }; + + if (navigator.userAgent.includes("Android")) { + defaultMochitestPrefs.set.push( + ["media.navigator.video.default_width", 320], + ["media.navigator.video.default_height", 240], + ["media.navigator.video.max_fr", 10], + ["media.autoplay.default", Ci.nsIAutoplay.ALLOWED] + ); + } + + // Platform codec prefs should be matched because fake H.264 GMP codec doesn't + // produce/consume real bitstreams. [TODO] remove after bug 1509012 is fixed. + const platformEncoderEnabled = SpecialPowers.getBoolPref( + "media.webrtc.platformencoder" + ); + defaultMochitestPrefs.set.push([ + "media.navigator.mediadatadecoder_h264_enabled", + platformEncoderEnabled, + ]); + + // Running as a Mochitest. + SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts"); + window.finish = () => SimpleTest.finish(); + SpecialPowers.pushPrefEnv(defaultMochitestPrefs, setTestOptions); + + // We don't care about waiting for this to complete, we just want to ensure + // that we don't build up a huge backlog of GC work. + SpecialPowers.exactGC(); +} + +// [TODO] remove after bug 1509012 is fixed. +async function matchPlatformH264CodecPrefs() { + const hasHW264 = + SpecialPowers.getBoolPref("media.webrtc.platformencoder") && + !SpecialPowers.getBoolPref("media.webrtc.platformencoder.sw_only") && + (navigator.userAgent.includes("Android") || + navigator.userAgent.includes("Mac OS X")); + + await pushPrefs( + ["media.webrtc.platformencoder", hasHW264], + ["media.navigator.mediadatadecoder_h264_enabled", hasHW264] + ); +} + +async function runTestWhenReady(testFunc) { + setupEnvironment(); + const options = await testConfigured; + try { + await testFunc(options); + } catch (e) { + ok( + false, + `Error executing test: ${e} +${e.stack ? e.stack : ""}` + ); + } finally { + SimpleTest.finish(); + } +} + +/** + * Checks that the media stream tracks have the expected amount of tracks + * with the correct attributes based on the type and constraints given. + * + * @param {Object} constraints specifies whether the stream should have + * audio, video, or both + * @param {String} type the type of media stream tracks being checked + * @param {sequence<MediaStreamTrack>} mediaStreamTracks the media stream + * tracks being checked + */ +function checkMediaStreamTracksByType(constraints, type, mediaStreamTracks) { + if (constraints[type]) { + is(mediaStreamTracks.length, 1, "One " + type + " track shall be present"); + + if (mediaStreamTracks.length) { + is(mediaStreamTracks[0].kind, type, "Track kind should be " + type); + ok(mediaStreamTracks[0].id, "Track id should be defined"); + ok(!mediaStreamTracks[0].muted, "Track should not be muted"); + } + } else { + is(mediaStreamTracks.length, 0, "No " + type + " tracks shall be present"); + } +} + +/** + * Check that the given media stream contains the expected media stream + * tracks given the associated audio & video constraints provided. + * + * @param {Object} constraints specifies whether the stream should have + * audio, video, or both + * @param {MediaStream} mediaStream the media stream being checked + */ +function checkMediaStreamTracks(constraints, mediaStream) { + checkMediaStreamTracksByType( + constraints, + "audio", + mediaStream.getAudioTracks() + ); + checkMediaStreamTracksByType( + constraints, + "video", + mediaStream.getVideoTracks() + ); +} + +/** + * Check that a media stream contains exactly a set of media stream tracks. + * + * @param {MediaStream} mediaStream the media stream being checked + * @param {Array} tracks the tracks that should exist in mediaStream + * @param {String} [message] an optional message to pass to asserts + */ +function checkMediaStreamContains(mediaStream, tracks, message) { + message = message ? message + ": " : ""; + tracks.forEach(t => + ok( + mediaStream.getTrackById(t.id), + message + "MediaStream " + mediaStream.id + " contains track " + t.id + ) + ); + is( + mediaStream.getTracks().length, + tracks.length, + message + "MediaStream " + mediaStream.id + " contains no extra tracks" + ); +} + +function checkMediaStreamCloneAgainstOriginal(clone, original) { + isnot(clone.id.length, 0, "Stream clone should have an id string"); + isnot(clone, original, "Stream clone should be different from the original"); + isnot( + clone.id, + original.id, + "Stream clone's id should be different from the original's" + ); + is( + clone.getAudioTracks().length, + original.getAudioTracks().length, + "All audio tracks should get cloned" + ); + is( + clone.getVideoTracks().length, + original.getVideoTracks().length, + "All video tracks should get cloned" + ); + is(clone.active, original.active, "Active state should be preserved"); + original + .getTracks() + .forEach(t => + ok(!clone.getTrackById(t.id), "The clone's tracks should be originals") + ); +} + +function checkMediaStreamTrackCloneAgainstOriginal(clone, original) { + isnot(clone.id.length, 0, "Track clone should have an id string"); + isnot(clone, original, "Track clone should be different from the original"); + isnot( + clone.id, + original.id, + "Track clone's id should be different from the original's" + ); + is( + clone.kind, + original.kind, + "Track clone's kind should be same as the original's" + ); + is( + clone.enabled, + original.enabled, + "Track clone's kind should be same as the original's" + ); + is( + clone.readyState, + original.readyState, + "Track clone's readyState should be same as the original's" + ); + is( + clone.muted, + original.muted, + "Track clone's muted state should be same as the original's" + ); +} + +/*** Utility methods */ + +/** The dreadful setTimeout, use sparingly */ +function wait(time, message) { + return new Promise(r => setTimeout(() => r(message), time)); +} + +/** The even more dreadful setInterval, use even more sparingly */ +function waitUntil(func, time) { + return new Promise(resolve => { + var interval = setInterval(() => { + if (func()) { + clearInterval(interval); + resolve(); + } + }, time || 200); + }); +} + +/** Time out while waiting for a promise to get resolved or rejected. */ +var timeout = (promise, time, msg) => + Promise.race([ + promise, + wait(time).then(() => Promise.reject(new Error(msg))), + ]); + +/** Adds a |finally| function to a promise whose argument is invoked whether the + * promise is resolved or rejected, and that does not interfere with chaining.*/ +var addFinallyToPromise = promise => { + promise.finally = func => { + return promise.then( + result => { + func(); + return Promise.resolve(result); + }, + error => { + func(); + return Promise.reject(error); + } + ); + }; + return promise; +}; + +/** Use event listener to call passed-in function on fire until it returns true */ +var listenUntil = (target, eventName, onFire) => { + return new Promise(resolve => + target.addEventListener(eventName, function callback(event) { + var result = onFire(event); + if (result) { + target.removeEventListener(eventName, callback); + resolve(result); + } + }) + ); +}; + +/* Test that a function throws the right error */ +function mustThrowWith(msg, reason, f) { + try { + f(); + ok(false, msg + " must throw"); + } catch (e) { + is(e.name, reason, msg + " must throw: " + e.message); + } +} + +/* Get a dummy audio track */ +function getSilentTrack() { + let ctx = new AudioContext(), + oscillator = ctx.createOscillator(); + let dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return Object.assign(dst.stream.getAudioTracks()[0], { enabled: false }); +} + +function getBlackTrack({ width = 640, height = 480 } = {}) { + let canvas = Object.assign(document.createElement("canvas"), { + width, + height, + }); + canvas.getContext("2d").fillRect(0, 0, width, height); + let stream = canvas.captureStream(); + return Object.assign(stream.getVideoTracks()[0], { enabled: false }); +} + +/*** Test control flow methods */ + +/** + * Generates a callback function fired only under unexpected circumstances + * while running the tests. The generated function kills off the test as well + * gracefully. + * + * @param {String} [message] + * An optional message to show if no object gets passed into the + * generated callback method. + */ +function generateErrorCallback(message) { + var stack = new Error().stack.split("\n"); + stack.shift(); // Don't include this instantiation frame + + /** + * @param {object} aObj + * The object fired back from the callback + */ + return aObj => { + if (aObj) { + if (aObj.name && aObj.message) { + ok( + false, + "Unexpected callback for '" + + aObj.name + + "' with message = '" + + aObj.message + + "' at " + + JSON.stringify(stack) + ); + } else { + ok( + false, + "Unexpected callback with = '" + + aObj + + "' at: " + + JSON.stringify(stack) + ); + } + } else { + ok( + false, + "Unexpected callback with message = '" + + message + + "' at: " + + JSON.stringify(stack) + ); + } + throw new Error("Unexpected callback"); + }; +} + +var unexpectedEventArrived; +var rejectOnUnexpectedEvent = new Promise((x, reject) => { + unexpectedEventArrived = reject; +}); + +/** + * Generates a callback function fired only for unexpected events happening. + * + * @param {String} description + Description of the object for which the event has been fired + * @param {String} eventName + Name of the unexpected event + */ +function unexpectedEvent(message, eventName) { + var stack = new Error().stack.split("\n"); + stack.shift(); // Don't include this instantiation frame + + return e => { + var details = + "Unexpected event '" + + eventName + + "' fired with message = '" + + message + + "' at: " + + JSON.stringify(stack); + ok(false, details); + unexpectedEventArrived(new Error(details)); + }; +} + +/** + * Implements the one-shot event pattern used throughout. Each of the 'onxxx' + * attributes on the wrappers can be set with a custom handler. Prior to the + * handler being set, if the event fires, it causes the test execution to halt. + * That handler is used exactly once, after which the original, error-generating + * handler is re-installed. Thus, each event handler is used at most once. + * + * @param {object} wrapper + * The wrapper on which the psuedo-handler is installed + * @param {object} obj + * The real source of events + * @param {string} event + * The name of the event + */ +function createOneShotEventWrapper(wrapper, obj, event) { + var onx = "on" + event; + var unexpected = unexpectedEvent(wrapper, event); + wrapper[onx] = unexpected; + obj[onx] = e => { + info(wrapper + ': "on' + event + '" event fired'); + e.wrapper = wrapper; + wrapper[onx](e); + wrapper[onx] = unexpected; + }; +} + +/** + * Returns a promise that resolves when `target` has raised an event with the + * given name the given number of times. Cancel the returned promise by passing + * in a `cancel` promise and resolving it. + * + * @param {object} target + * The target on which the event should occur. + * @param {string} name + * The name of the event that should occur. + * @param {integer} count + * Optional number of times the event should be raised before resolving. + * @param {promise} cancel + * Optional promise that on resolving rejects the returned promise, + * so we can avoid logging results after a test has finished. + * @returns {promise} A promise that resolves to the last of the seen events. + */ +function haveEvents(target, name, count, cancel) { + var listener; + var counter = count || 1; + return Promise.race([ + (cancel || new Promise(() => {})).then(e => Promise.reject(e)), + new Promise(resolve => + target.addEventListener( + name, + (listener = e => --counter < 1 && resolve(e)) + ) + ), + ]).then(e => { + target.removeEventListener(name, listener); + return e; + }); +} + +/** + * Returns a promise that resolves when `target` has raised an event with the + * given name. Cancel the returned promise by passing in a `cancel` promise and + * resolving it. + * + * @param {object} target + * The target on which the event should occur. + * @param {string} name + * The name of the event that should occur. + * @param {promise} cancel + * Optional promise that on resolving rejects the returned promise, + * so we can avoid logging results after a test has finished. + * @returns {promise} A promise that resolves to the seen event. + */ +function haveEvent(target, name, cancel) { + return haveEvents(target, name, 1, cancel); +} + +/** + * Returns a promise that resolves if the target has not seen the given event + * after one crank (or until the given timeoutPromise resolves) of the event + * loop. + * + * @param {object} target + * The target on which the event should not occur. + * @param {string} name + * The name of the event that should not occur. + * @param {promise} timeoutPromise + * Optional promise defining how long we should wait before resolving. + * @returns {promise} A promise that is rejected if we see the given event, or + * resolves after a timeout otherwise. + */ +function haveNoEvent(target, name, timeoutPromise) { + return haveEvent(target, name, timeoutPromise || wait(0)).then( + () => Promise.reject(new Error("Too many " + name + " events")), + () => {} + ); +} + +/** + * Returns a promise that resolves after the target has seen the given number + * of events but no such event in a following crank of the event loop. + * + * @param {object} target + * The target on which the events should occur. + * @param {string} name + * The name of the event that should occur. + * @param {integer} count + * Optional number of times the event should be raised before resolving. + * @param {promise} cancel + * Optional promise that on resolving rejects the returned promise, + * so we can avoid logging results after a test has finished. + * @returns {promise} A promise that resolves to the last of the seen events. + */ +function haveEventsButNoMore(target, name, count, cancel) { + return haveEvents(target, name, count, cancel).then(e => + haveNoEvent(target, name).then(() => e) + ); +} + +/* + * Resolves the returned promise with an object with usage and reportCount + * properties. `usage` is in the same units as reported by the reporter for + * `path`. + */ +const collectMemoryUsage = async path => { + const MemoryReporterManager = Cc[ + "@mozilla.org/memory-reporter-manager;1" + ].getService(Ci.nsIMemoryReporterManager); + + let usage = 0; + let reportCount = 0; + await new Promise(resolve => + MemoryReporterManager.getReports( + (aProcess, aPath, aKind, aUnits, aAmount, aDesc) => { + if (aPath != path) { + return; + } + ++reportCount; + usage += aAmount; + }, + null, + resolve, + null, + /* anonymized = */ false + ) + ); + return { usage, reportCount }; +}; + +// Some DNS helper functions +const dnsLookup = async hostname => { + // Convenience API for various networking related stuff. _Almost_ convenient + // enough. + const neckoDashboard = SpecialPowers.Cc[ + "@mozilla.org/network/dashboard;1" + ].getService(Ci.nsIDashboard); + + const results = await new Promise(r => { + neckoDashboard.requestDNSLookup(hostname, results => { + r(SpecialPowers.wrap(results)); + }); + }); + + // |address| is an array-like dictionary (ie; keys are all integers). + // We convert to an array to make it less unwieldy. + const addresses = [...results.address]; + info(`DNS results for ${hostname}: ${JSON.stringify(addresses)}`); + return addresses; +}; + +const dnsLookupV4 = async hostname => { + const addresses = await dnsLookup(hostname); + return addresses.filter(address => !address.includes(":")); +}; + +const dnsLookupV6 = async hostname => { + const addresses = await dnsLookup(hostname); + return addresses.filter(address => address.includes(":")); +}; + +const getTurnHostname = turnUrl => { + const urlNoParams = turnUrl.split("?")[0]; + // Strip off scheme + const hostAndMaybePort = urlNoParams.split(":", 2)[1]; + if (hostAndMaybePort[0] == "[") { + // IPV6 literal, strip out '[', and split at closing ']' + return hostAndMaybePort.substring(1).split("]")[0]; + } + return hostAndMaybePort.split(":")[0]; +}; + +// Yo dawg I heard you like yo dawg I heard you like Proxies +// Example: let value = await GleanTest.category.metric.testGetValue(); +// For labeled metrics: +// let value = await GleanTest.category.metric["label"].testGetValue(); +// Please don't try to use the string "testGetValue" as a label. +const GleanTest = new Proxy( + {}, + { + get(target, categoryName, receiver) { + return new Proxy( + {}, + { + get(target, metricName, receiver) { + return new Proxy( + { + async testGetValue() { + return SpecialPowers.spawnChrome( + [categoryName, metricName], + async (categoryName, metricName) => { + await Services.fog.testFlushAllChildren(); + const window = this.browsingContext.topChromeWindow; + return window.Glean[categoryName][ + metricName + ].testGetValue(); + } + ); + }, + }, + { + get(target, prop, receiver) { + // The only prop that will be there is testGetValue, but we + // might add more later. + if (prop in target) { + return target[prop]; + } + + // |prop| must be a label? + const label = prop; + return { + async testGetValue() { + return SpecialPowers.spawnChrome( + [categoryName, metricName, label], + async (categoryName, metricName, label) => { + await Services.fog.testFlushAllChildren(); + const window = this.browsingContext.topChromeWindow; + return window.Glean[categoryName][metricName][ + label + ].testGetValue(); + } + ); + }, + }; + }, + } + ); + }, + } + ); + }, + } +); + +/** + * This class executes a series of functions in a continuous sequence. + * Promise-bearing functions are executed after the previous promise completes. + * + * @constructor + * @param {object} framework + * A back reference to the framework which makes use of the class. It is + * passed to each command callback. + * @param {function[]} commandList + * Commands to set during initialization + */ +function CommandChain(framework, commandList) { + this._framework = framework; + this.commands = commandList || []; +} + +CommandChain.prototype = { + /** + * Start the command chain. This returns a promise that always resolves + * cleanly (this catches errors and fails the test case). + */ + execute() { + return this.commands + .reduce((prev, next, i) => { + if (typeof next !== "function" || !next.name) { + throw new Error("registered non-function" + next); + } + + return prev.then(() => { + info("Run step " + (i + 1) + ": " + next.name); + return Promise.race([next(this._framework), rejectOnUnexpectedEvent]); + }); + }, Promise.resolve()) + .catch(e => + ok( + false, + "Error in test execution: " + + e + + (typeof e.stack === "string" + ? " " + e.stack.split("\n").join(" ... ") + : "") + ) + ); + }, + + /** + * Add new commands to the end of the chain + */ + append(commands) { + this.commands = this.commands.concat(commands); + }, + + /** + * Returns the index of the specified command in the chain. + * @param {occurrence} Optional param specifying which occurrence to match, + * with 0 representing the first occurrence. + */ + indexOf(functionOrName, occurrence) { + occurrence = occurrence || 0; + return this.commands.findIndex(func => { + if (typeof functionOrName === "string") { + if (func.name !== functionOrName) { + return false; + } + } else if (func !== functionOrName) { + return false; + } + if (occurrence) { + --occurrence; + return false; + } + return true; + }); + }, + + mustHaveIndexOf(functionOrName, occurrence) { + var index = this.indexOf(functionOrName, occurrence); + if (index == -1) { + throw new Error("Unknown test: " + functionOrName); + } + return index; + }, + + /** + * Inserts the new commands after the specified command. + */ + insertAfter(functionOrName, commands, all, occurrence) { + this._insertHelper(functionOrName, commands, 1, all, occurrence); + }, + + /** + * Inserts the new commands after every occurrence of the specified command + */ + insertAfterEach(functionOrName, commands) { + this._insertHelper(functionOrName, commands, 1, true); + }, + + /** + * Inserts the new commands before the specified command. + */ + insertBefore(functionOrName, commands, all, occurrence) { + this._insertHelper(functionOrName, commands, 0, all, occurrence); + }, + + _insertHelper(functionOrName, commands, delta, all, occurrence) { + occurrence = occurrence || 0; + for ( + var index = this.mustHaveIndexOf(functionOrName, occurrence); + index !== -1; + index = this.indexOf(functionOrName, ++occurrence) + ) { + this.commands = [].concat( + this.commands.slice(0, index + delta), + commands, + this.commands.slice(index + delta) + ); + if (!all) { + break; + } + } + }, + + /** + * Removes the specified command, returns what was removed. + */ + remove(functionOrName, occurrence) { + return this.commands.splice( + this.mustHaveIndexOf(functionOrName, occurrence), + 1 + ); + }, + + /** + * Removes all commands after the specified one, returns what was removed. + */ + removeAfter(functionOrName, occurrence) { + return this.commands.splice( + this.mustHaveIndexOf(functionOrName, occurrence) + 1 + ); + }, + + /** + * Removes all commands before the specified one, returns what was removed. + */ + removeBefore(functionOrName, occurrence) { + return this.commands.splice( + 0, + this.mustHaveIndexOf(functionOrName, occurrence) + ); + }, + + /** + * Replaces a single command, returns what was removed. + */ + replace(functionOrName, commands) { + this.insertBefore(functionOrName, commands); + return this.remove(functionOrName); + }, + + /** + * Replaces all commands after the specified one, returns what was removed. + */ + replaceAfter(functionOrName, commands, occurrence) { + var oldCommands = this.removeAfter(functionOrName, occurrence); + this.append(commands); + return oldCommands; + }, + + /** + * Replaces all commands before the specified one, returns what was removed. + */ + replaceBefore(functionOrName, commands) { + var oldCommands = this.removeBefore(functionOrName); + this.insertBefore(functionOrName, commands); + return oldCommands; + }, + + /** + * Remove all commands whose name match the specified regex. + */ + filterOut(id_match) { + this.commands = this.commands.filter(c => !id_match.test(c.name)); + }, +}; + +function AudioStreamFlowingHelper() { + this._context = new AudioContext(); + // Tests may have changed the values of prefs, so recheck + updateConfigFromFakeAndLoopbackPrefs(); + if (!WANT_FAKE_AUDIO) { + // Loopback device is configured, start the default loopback tone + if (!DefaultLoopbackTone) { + DefaultLoopbackTone = new LoopbackTone(this._context, TEST_AUDIO_FREQ); + DefaultLoopbackTone.start(); + } + } +} + +AudioStreamFlowingHelper.prototype = { + checkAudio(stream, analyser, fun) { + /* + analyser.enableDebugCanvas(); + return analyser.waitForAnalysisSuccess(fun) + .then(() => analyser.disableDebugCanvas()); + */ + return analyser.waitForAnalysisSuccess(fun); + }, + + checkAudioFlowing(stream) { + var analyser = new AudioStreamAnalyser(this._context, stream); + var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return this.checkAudio(stream, analyser, array => array[freq] > 200); + }, + + // Use checkAudioNotFlowing() only after checkAudioFlowing() or similar to + // know that audio had previously been flowing on the same stream, as + // checkAudioNotFlowing() does not wait for the loopback device to return + // any audio that it receives. + checkAudioNotFlowing(stream) { + var analyser = new AudioStreamAnalyser(this._context, stream); + var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return this.checkAudio(stream, analyser, array => array[freq] < 50); + }, +}; + +class VideoFrameEmitter { + constructor(color1, color2, width, height) { + if (!width) { + width = 50; + } + if (!height) { + height = width; + } + this._helper = new CaptureStreamTestHelper2D(width, height); + this._canvas = this._helper.createAndAppendElement( + "canvas", + "source_canvas" + ); + this._canvas.width = width; + this._canvas.height = height; + this._color1 = color1 ? color1 : this._helper.green; + this._color2 = color2 ? color2 : this._helper.red; + // Make sure this is initted + this._helper.drawColor(this._canvas, this._color1); + this._stream = this._canvas.captureStream(); + this._started = false; + } + + stream() { + return this._stream; + } + + helper() { + return this._helper; + } + + colors(color1, color2) { + this._color1 = color1 ? color1 : this._helper.green; + this._color2 = color2 ? color2 : this._helper.red; + try { + this._helper.drawColor(this._canvas, this._color1); + } catch (e) { + // ignore; stream might have shut down + } + } + + size(width, height) { + this._canvas.width = width; + this._canvas.height = height; + } + + start() { + if (this._started) { + info("*** emitter already started"); + return; + } + + let i = 0; + this._started = true; + this._intervalId = setInterval(() => { + try { + this._helper.drawColor(this._canvas, i ? this._color1 : this._color2); + i = 1 - i; + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + + stop() { + if (this._started) { + clearInterval(this._intervalId); + this._started = false; + } + } +} + +class VideoStreamHelper { + constructor() { + this._helper = new CaptureStreamTestHelper2D(50, 50); + } + + async checkHasFrame(video, { offsetX, offsetY, threshold } = {}) { + const h = this._helper; + await h.waitForPixel( + video, + px => { + let result = h.isOpaquePixelNot(px, h.black, threshold); + info( + "Checking that we have a frame, got [" + + Array.from(px) + + "]. Ref=[" + + Array.from(h.black.data) + + "]. Threshold=" + + threshold + + ". Pass=" + + result + ); + return result; + }, + { offsetX, offsetY } + ); + } + + async checkVideoPlaying( + video, + { offsetX = 10, offsetY = 10, threshold = 16 } = {} + ) { + const h = this._helper; + await this.checkHasFrame(video, { offsetX, offsetY, threshold }); + let startPixel = { + data: h.getPixel(video, offsetX, offsetY), + name: "startcolor", + }; + await h.waitForPixel( + video, + px => { + let result = h.isPixelNot(px, startPixel, threshold); + info( + "Checking playing, [" + + Array.from(px) + + "] vs [" + + Array.from(startPixel.data) + + "]. Threshold=" + + threshold + + " Pass=" + + result + ); + return result; + }, + { offsetX, offsetY } + ); + } + + async checkVideoPaused( + video, + { offsetX = 10, offsetY = 10, threshold = 16, time = 5000 } = {} + ) { + const h = this._helper; + await this.checkHasFrame(video, { offsetX, offsetY, threshold }); + let startPixel = { + data: h.getPixel(video, offsetX, offsetY), + name: "startcolor", + }; + try { + await h.waitForPixel( + video, + px => { + let result = h.isOpaquePixelNot(px, startPixel, threshold); + info( + "Checking paused, [" + + Array.from(px) + + "] vs [" + + Array.from(startPixel.data) + + "]. Threshold=" + + threshold + + " Pass=" + + result + ); + return result; + }, + { offsetX, offsetY, cancel: wait(time, "timeout") } + ); + ok(false, "Frame changed within " + time / 1000 + " seconds"); + } catch (e) { + is( + e, + "timeout", + "Frame shouldn't change for " + time / 1000 + " seconds" + ); + } + } +} + +(function () { + var el = document.createElement("link"); + el.rel = "stylesheet"; + el.type = "text/css"; + el.href = "/tests/SimpleTest/test.css"; + document.head.appendChild(el); +})(); diff --git a/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js new file mode 100644 index 0000000000..6460f64a44 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js @@ -0,0 +1,889 @@ +/* eslint-env node */ +"use strict"; + +// SDP helpers. +var SDPUtils = {}; + +// Generate an alphanumeric identifier for cname or mids. +// TODO: use UUIDs instead? https://gist.github.com/jed/982883 +SDPUtils.generateIdentifier = function() { + return Math.random() + .toString(36) + .substr(2, 10); +}; + +// The RTCP CNAME used by all peerconnections from the same JS. +SDPUtils.localCName = SDPUtils.generateIdentifier(); + +// Splits SDP into lines, dealing with both CRLF and LF. +SDPUtils.splitLines = function(blob) { + return blob + .trim() + .split("\n") + .map(function(line) { + return line.trim(); + }); +}; +// Splits SDP into sessionpart and mediasections. Ensures CRLF. +SDPUtils.splitSections = function(blob) { + var parts = blob.split("\nm="); + return parts.map(function(part, index) { + return (index > 0 ? "m=" + part : part).trim() + "\r\n"; + }); +}; + +// returns the session description. +SDPUtils.getDescription = function(blob) { + var sections = SDPUtils.splitSections(blob); + return sections && sections[0]; +}; + +// returns the individual media sections. +SDPUtils.getMediaSections = function(blob) { + var sections = SDPUtils.splitSections(blob); + sections.shift(); + return sections; +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf("a=candidate:") === 0) { + parts = line.substring(12).split(" "); + } else { + parts = line.substring(10).split(" "); + } + + var candidate = { + foundation: parts[0], + component: parseInt(parts[1], 10), + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + address: parts[4], // address is an alias for ip. + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7], + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case "raddr": + candidate.relatedAddress = parts[i + 1]; + break; + case "rport": + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case "tcptype": + candidate.tcpType = parts[i + 1]; + break; + case "ufrag": + candidate.ufrag = parts[i + 1]; // for backward compability. + candidate.usernameFragment = parts[i + 1]; + break; + default: + // extension handling, in particular ufrag + candidate[parts[i]] = parts[i + 1]; + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.address || candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push("typ"); + sdp.push(type); + if (type !== "host" && candidate.relatedAddress && candidate.relatedPort) { + sdp.push("raddr"); + sdp.push(candidate.relatedAddress); + sdp.push("rport"); + sdp.push(candidate.relatedPort); + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === "tcp") { + sdp.push("tcptype"); + sdp.push(candidate.tcpType); + } + if (candidate.usernameFragment || candidate.ufrag) { + sdp.push("ufrag"); + sdp.push(candidate.usernameFragment || candidate.ufrag); + } + return "candidate:" + sdp.join(" "); +}; + +// Parses an ice-options line, returns an array of option tags. +// a=ice-options:foo bar +SDPUtils.parseIceOptions = function(line) { + return line.substr(14).split(" "); +}; + +// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(" "); + var parsed = { + payloadType: parseInt(parts.shift(), 10), // was: id + }; + + parts = parts[0].split("/"); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + // legacy alias, got renamed back to channels in ORTC. + parsed.numChannels = parsed.channels; + return parsed; +}; + +// Generate an a=rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + var channels = codec.channels || codec.numChannels || 1; + return ( + "a=rtpmap:" + + pt + + " " + + codec.name + + "/" + + codec.clockRate + + (channels !== 1 ? "/" + channels : "") + + "\r\n" + ); +}; + +// Parses an a=extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(" "); + return { + id: parseInt(parts[0], 10), + direction: parts[0].indexOf("/") > 0 ? parts[0].split("/")[1] : "sendrecv", + uri: parts[1], + }; +}; + +// Generates a=extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function(headerExtension) { + return ( + "a=extmap:" + + (headerExtension.id || headerExtension.preferredId) + + (headerExtension.direction && headerExtension.direction !== "sendrecv" + ? "/" + headerExtension.direction + : "") + + " " + + headerExtension.uri + + "\r\n" + ); +}; + +// Parses an ftmp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(" ") + 1).split(";"); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split("="); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function(codec) { + var line = ""; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + if (codec.parameters[param]) { + params.push(param + "=" + codec.parameters[param]); + } else { + params.push(param); + } + }); + line += "a=fmtp:" + pt + " " + params.join(";") + "\r\n"; + } + return line; +}; + +// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(" ") + 1).split(" "); + return { + type: parts.shift(), + parameter: parts.join(" "), + }; +}; +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function(codec) { + var lines = ""; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += + "a=rtcp-fb:" + + pt + + " " + + fb.type + + (fb.parameter && fb.parameter.length ? " " + fb.parameter : "") + + "\r\n"; + }); + } + return lines; +}; + +// Parses an RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(" "); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10), + }; + var colon = line.indexOf(":", sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; +}; + +SDPUtils.parseSsrcGroup = function(line) { + var parts = line.substr(13).split(" "); + return { + semantics: parts.shift(), + ssrcs: parts.map(function(ssrc) { + return parseInt(ssrc, 10); + }), + }; +}; + +// Extracts the MID (RFC 5888) from a media section. +// returns the MID or undefined if no mid line was found. +SDPUtils.getMid = function(mediaSection) { + var mid = SDPUtils.matchPrefix(mediaSection, "a=mid:")[0]; + if (mid) { + return mid.substr(6); + } +}; + +SDPUtils.parseFingerprint = function(line) { + var parts = line.substr(14).split(" "); + return { + algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. + value: parts[1], + }; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix( + mediaSection + sessionpart, + "a=fingerprint:" + ); + // Note: a=setup line is ignored since we use the 'auto' role. + // Note2: 'algorithm' is not case sensitive except in Edge. + return { + role: "auto", + fingerprints: lines.map(SDPUtils.parseFingerprint), + }; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = "a=setup:" + setupType + "\r\n"; + params.fingerprints.forEach(function(fp) { + sdp += "a=fingerprint:" + fp.algorithm + " " + fp.value + "\r\n"; + }); + return sdp; +}; + +// Parses a=crypto lines into +// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members +SDPUtils.parseCryptoLine = function(line) { + var parts = line.substr(9).split(" "); + return { + tag: parseInt(parts[0], 10), + cryptoSuite: parts[1], + keyParams: parts[2], + sessionParams: parts.slice(3), + }; +}; + +SDPUtils.writeCryptoLine = function(parameters) { + return ( + "a=crypto:" + + parameters.tag + + " " + + parameters.cryptoSuite + + " " + + (typeof parameters.keyParams === "object" + ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) + : parameters.keyParams) + + (parameters.sessionParams ? " " + parameters.sessionParams.join(" ") : "") + + "\r\n" + ); +}; + +// Parses the crypto key parameters into +// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* +SDPUtils.parseCryptoKeyParams = function(keyParams) { + if (keyParams.indexOf("inline:") !== 0) { + return null; + } + var parts = keyParams.substr(7).split("|"); + return { + keyMethod: "inline", + keySalt: parts[0], + lifeTime: parts[1], + mkiValue: parts[2] ? parts[2].split(":")[0] : undefined, + mkiLength: parts[2] ? parts[2].split(":")[1] : undefined, + }; +}; + +SDPUtils.writeCryptoKeyParams = function(keyParams) { + return ( + keyParams.keyMethod + + ":" + + keyParams.keySalt + + (keyParams.lifeTime ? "|" + keyParams.lifeTime : "") + + (keyParams.mkiValue && keyParams.mkiLength + ? "|" + keyParams.mkiValue + ":" + keyParams.mkiLength + : "") + ); +}; + +// Extracts all SDES paramters. +SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=crypto:"); + return lines.map(SDPUtils.parseCryptoLine); +}; + +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var ufrag = SDPUtils.matchPrefix( + mediaSection + sessionpart, + "a=ice-ufrag:" + )[0]; + var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=ice-pwd:")[0]; + if (!(ufrag && pwd)) { + return null; + } + return { + usernameFragment: ufrag.substr(12), + password: pwd.substr(10), + }; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function(params) { + return ( + "a=ice-ufrag:" + + params.usernameFragment + + "\r\n" + + "a=ice-pwd:" + + params.password + + "\r\n" + ); +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [], + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(" "); + for (var i = 3; i < mline.length; i++) { + // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, + "a=rtpmap:" + pt + " " + )[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix(mediaSection, "a=fmtp:" + pt + " "); + // Only the first a=fmtp:<pt> is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, + "a=rtcp-fb:" + pt + " " + ).map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case "RED": + case "ULPFEC": + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: + // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, "a=extmap:").forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ""; + + // Build the mline. + sdp += "m=" + kind + " "; + sdp += caps.codecs.length > 0 ? "9" : "0"; // reject if no codecs. + sdp += " UDP/TLS/RTP/SAVPF "; + sdp += + caps.codecs + .map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }) + .join(" ") + "\r\n"; + + sdp += "c=IN IP4 0.0.0.0\r\n"; + sdp += "a=rtcp:9 IN IP4 0.0.0.0\r\n"; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + var maxptime = 0; + caps.codecs.forEach(function(codec) { + if (codec.maxptime > maxptime) { + maxptime = codec.maxptime; + } + }); + if (maxptime > 0) { + sdp += "a=maxptime:" + maxptime + "\r\n"; + } + sdp += "a=rtcp-mux\r\n"; + + if (caps.headerExtensions) { + caps.headerExtensions.forEach(function(extension) { + sdp += SDPUtils.writeExtmap(extension); + }); + } + // FIXME: write fecMechanisms. + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf("RED") !== -1; + var hasUlpfec = description.fecMechanisms.indexOf("ULPFEC") !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === "cname"; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, "a=ssrc-group:FID").map( + function(line) { + var parts = line.substr(17).split(" "); + return parts.map(function(part) { + return parseInt(part, 10); + }); + } + ); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === "RTX" && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10), + }; + if (primarySsrc && secondarySsrc) { + encParam.rtx = { ssrc: secondarySsrc }; + } + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: primarySsrc, + mechanism: hasUlpfec ? "red+ulpfec" : "red", + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc, + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, "b="); + if (bandwidth.length) { + if (bandwidth[0].indexOf("b=TIAS:") === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf("b=AS:") === 0) { + // use formula from JSEP to convert b=AS to TIAS value. + bandwidth = + parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - 50 * 40 * 8; + } else { + bandwidth = undefined; + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +// parses http://draft.ortc.org/#rtcrtcpparameters* +SDPUtils.parseRtcpParameters = function(mediaSection) { + var rtcpParameters = {}; + + // Gets the first SSRC. Note tha with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === "cname"; + })[0]; + if (remoteSsrc) { + rtcpParameters.cname = remoteSsrc.value; + rtcpParameters.ssrc = remoteSsrc.ssrc; + } + + // Edge uses the compound attribute instead of reducedSize + // compound is !reducedSize + var rsize = SDPUtils.matchPrefix(mediaSection, "a=rtcp-rsize"); + rtcpParameters.reducedSize = rsize.length > 0; + rtcpParameters.compound = rsize.length === 0; + + // parses the rtcp-mux attrіbute. + // Note that Edge does not support unmuxed RTCP. + var mux = SDPUtils.matchPrefix(mediaSection, "a=rtcp-mux"); + rtcpParameters.mux = mux.length > 0; + + return rtcpParameters; +}; + +// parses either a=msid: or a=ssrc:... msid lines and returns +// the id of the MediaStream and MediaStreamTrack. +SDPUtils.parseMsid = function(mediaSection) { + var parts; + var spec = SDPUtils.matchPrefix(mediaSection, "a=msid:"); + if (spec.length === 1) { + parts = spec[0].substr(7).split(" "); + return { stream: parts[0], track: parts[1] }; + } + var planB = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(msidParts) { + return msidParts.attribute === "msid"; + }); + if (planB.length > 0) { + parts = planB[0].value.split(" "); + return { stream: parts[0], track: parts[1] }; + } +}; + +// SCTP +// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back +// to draft-ietf-mmusic-sctp-sdp-05 +SDPUtils.parseSctpDescription = function(mediaSection) { + var mline = SDPUtils.parseMLine(mediaSection); + var maxSizeLine = SDPUtils.matchPrefix(mediaSection, "a=max-message-size:"); + var maxMessageSize; + if (maxSizeLine.length > 0) { + maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10); + } + if (isNaN(maxMessageSize)) { + maxMessageSize = 65536; + } + var sctpPort = SDPUtils.matchPrefix(mediaSection, "a=sctp-port:"); + if (sctpPort.length > 0) { + return { + port: parseInt(sctpPort[0].substr(12), 10), + protocol: mline.fmt, + maxMessageSize, + }; + } + var sctpMapLines = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:"); + if (sctpMapLines.length > 0) { + var parts = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:")[0] + .substr(10) + .split(" "); + return { + port: parseInt(parts[0], 10), + protocol: parts[1], + maxMessageSize, + }; + } +}; + +// SCTP +// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers +// support by now receiving in this format, unless we originally parsed +// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line +// protocol of DTLS/SCTP -- without UDP/ or TCP/) +SDPUtils.writeSctpDescription = function(media, sctp) { + var output = []; + if (media.protocol !== "DTLS/SCTP") { + output = [ + "m=" + media.kind + " 9 " + media.protocol + " " + sctp.protocol + "\r\n", + "c=IN IP4 0.0.0.0\r\n", + "a=sctp-port:" + sctp.port + "\r\n", + ]; + } else { + output = [ + "m=" + media.kind + " 9 " + media.protocol + " " + sctp.port + "\r\n", + "c=IN IP4 0.0.0.0\r\n", + "a=sctpmap:" + sctp.port + " " + sctp.protocol + " 65535\r\n", + ]; + } + if (sctp.maxMessageSize !== undefined) { + output.push("a=max-message-size:" + sctp.maxMessageSize + "\r\n"); + } + return output.join(""); +}; + +// Generate a session ID for SDP. +// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 +// recommends using a cryptographically random +ve 64-bit value +// but right now this should be acceptable and within the right range +SDPUtils.generateSessionId = function() { + return Math.floor((Math.random() * 4294967296) + 1); +}; + +// Write boilder plate for start of SDP +// sessId argument is optional - if not supplied it will +// be generated randomly +// sessVersion is optional and defaults to 2 +// sessUser is optional and defaults to 'thisisadapterortc' +SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { + var sessionId; + var version = sessVer !== undefined ? sessVer : 2; + if (sessId) { + sessionId = sessId; + } else { + sessionId = SDPUtils.generateSessionId(); + } + var user = sessUser || "thisisadapterortc"; + // FIXME: sess-id should be an NTP timestamp. + return ( + "v=0\r\n" + + "o=" + + user + + " " + + sessionId + + " " + + version + + " IN IP4 127.0.0.1\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + ); +}; + +SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters() + ); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === "offer" ? "actpass" : "active" + ); + + sdp += "a=mid:" + transceiver.mid + "\r\n"; + + if (transceiver.direction) { + sdp += "a=" + transceiver.direction + "\r\n"; + } else if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += "a=sendrecv\r\n"; + } else if (transceiver.rtpSender) { + sdp += "a=sendonly\r\n"; + } else if (transceiver.rtpReceiver) { + sdp += "a=recvonly\r\n"; + } else { + sdp += "a=inactive\r\n"; + } + + if (transceiver.rtpSender) { + // spec. + var msid = + "msid:" + stream.id + " " + transceiver.rtpSender.track.id + "\r\n"; + sdp += "a=" + msid; + + // for Chrome. + sdp += "a=ssrc:" + transceiver.sendEncodingParameters[0].ssrc + " " + msid; + if (transceiver.sendEncodingParameters[0].rtx) { + sdp += + "a=ssrc:" + transceiver.sendEncodingParameters[0].rtx.ssrc + " " + msid; + sdp += + "a=ssrc-group:FID " + + transceiver.sendEncodingParameters[0].ssrc + + " " + + transceiver.sendEncodingParameters[0].rtx.ssrc + + "\r\n"; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp += + "a=ssrc:" + + transceiver.sendEncodingParameters[0].ssrc + + " cname:" + + SDPUtils.localCName + + "\r\n"; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp += + "a=ssrc:" + + transceiver.sendEncodingParameters[0].rtx.ssrc + + " cname:" + + SDPUtils.localCName + + "\r\n"; + } + return sdp; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case "a=sendrecv": + case "a=sendonly": + case "a=recvonly": + case "a=inactive": + return lines[i].substr(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return "sendrecv"; +}; + +SDPUtils.getKind = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(" "); + return mline[0].substr(2); +}; + +SDPUtils.isRejected = function(mediaSection) { + return mediaSection.split(" ", 2)[1] === "0"; +}; + +SDPUtils.parseMLine = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var parts = lines[0].substr(2).split(" "); + return { + kind: parts[0], + port: parseInt(parts[1], 10), + protocol: parts[2], + fmt: parts.slice(3).join(" "), + }; +}; + +SDPUtils.parseOLine = function(mediaSection) { + var line = SDPUtils.matchPrefix(mediaSection, "o=")[0]; + var parts = line.substr(2).split(" "); + return { + username: parts[0], + sessionId: parts[1], + sessionVersion: parseInt(parts[2], 10), + netType: parts[3], + addressType: parts[4], + address: parts[5], + }; +}; + +// a very naive interpretation of a valid SDP. +SDPUtils.isValidSDP = function(blob) { + if (typeof blob !== "string" || blob.length === 0) { + return false; + } + var lines = SDPUtils.splitLines(blob); + for (var i = 0; i < lines.length; i++) { + if (lines[i].length < 2 || lines[i].charAt(1) !== "=") { + return false; + } + // TODO: check the modifier a bit more. + } + return true; +}; + +// Expose public methods. +if (typeof module === "object") { + module.exports = SDPUtils; +} diff --git a/dom/media/webrtc/tests/mochitests/iceTestUtils.js b/dom/media/webrtc/tests/mochitests/iceTestUtils.js new file mode 100644 index 0000000000..d4d1f5c4b4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/iceTestUtils.js @@ -0,0 +1,302 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is mostly so test_peerConnection_gatherWithStun300.html and +// test_peerConnection_gatherWithStun300IPv6 can share this code. I would have +// put the ipv6 test code in the same file, but our ipv6 tester support is +// inconsistent enough that we need to be able to track the ipv6 test +// separately. + +async function findStatsRelayCandidates(pc, protocol) { + const stats = await pc.getStats(); + return [...stats.values()].filter( + v => + v.type == "local-candidate" && + v.candidateType == "relay" && + v.relayProtocol == protocol + ); +} + +// Trickles candidates if pcDst is set, and resolves the candidate list +async function trickleIce(pc, pcDst) { + const candidates = [], + addCandidatePromises = []; + while (true) { + const { candidate } = await new Promise(r => + pc.addEventListener("icecandidate", r, { once: true }) + ); + if (!candidate) { + break; + } + candidates.push(candidate); + if (pcDst) { + addCandidatePromises.push(pcDst.addIceCandidate(candidate)); + } + } + await Promise.all(addCandidatePromises); + return candidates; +} + +async function gather(pc) { + if (pc.signalingState == "stable") { + await pc.setLocalDescription( + await pc.createOffer({ offerToReceiveAudio: true }) + ); + } else if (pc.signalingState == "have-remote-offer") { + await pc.setLocalDescription(); + } + + return trickleIce(pc); +} + +async function gatherWithTimeout(pc, timeout, context) { + const throwOnTimeout = async () => { + await wait(timeout); + throw new Error( + `Gathering did not complete within ${timeout} ms with ${context}` + ); + }; + + return Promise.race([gather(pc), throwOnTimeout()]); +} + +async function iceConnected(pc) { + return new Promise((resolve, reject) => { + pc.addEventListener("iceconnectionstatechange", () => { + if (["connected", "completed"].includes(pc.iceConnectionState)) { + resolve(); + } else if (pc.iceConnectionState == "failed") { + reject(new Error(`ICE failed`)); + } + }); + }); +} + +// Set up trickle, but does not wait for it to complete. Can be used by itself +// in cases where we do not expect any new candidates, but want to still set up +// the signal handling in case new candidates _do_ show up. +async function connectNoTrickleWait(offerer, answerer, timeout, context) { + return connect(offerer, answerer, timeout, context, true); +} + +async function connect( + offerer, + answerer, + timeout, + context, + noTrickleWait = false +) { + const trickle1 = trickleIce(offerer, answerer); + const trickle2 = trickleIce(answerer, offerer); + try { + const offer = await offerer.createOffer({ offerToReceiveAudio: true }); + await offerer.setLocalDescription(offer); + await answerer.setRemoteDescription(offer); + const answer = await answerer.createAnswer(); + await Promise.all([ + offerer.setRemoteDescription(answer), + answerer.setLocalDescription(answer), + ]); + + const throwOnTimeout = async () => { + if (timeout) { + await wait(timeout); + throw new Error( + `ICE did not complete within ${timeout} ms with ${context}` + ); + } + }; + + await Promise.race([ + Promise.all([iceConnected(offerer), iceConnected(answerer)]), + throwOnTimeout(timeout, context), + ]); + } finally { + if (!noTrickleWait) { + // TODO(bug 1751509): For now, we need to let gathering finish before we + // proceed, because there are races in ICE restart wrt gathering state. + await Promise.all([trickle1, trickle2]); + } + } +} + +function isV6HostCandidate(candidate) { + const fields = candidate.candidate.split(" "); + const type = fields[7]; + const ipAddress = fields[4]; + return type == "host" && ipAddress.includes(":"); +} + +async function ipv6Supported() { + const pc = new RTCPeerConnection(); + const candidates = await gatherWithTimeout(pc, 8000); + info(`baseline candidates: ${JSON.stringify(candidates)}`); + pc.close(); + return candidates.some(isV6HostCandidate); +} + +function makeContextString(iceServers) { + const currentRedirectAddress = SpecialPowers.getCharPref( + "media.peerconnection.nat_simulator.redirect_address", + "" + ); + const currentRedirectTargets = SpecialPowers.getCharPref( + "media.peerconnection.nat_simulator.redirect_targets", + "" + ); + return `redirect rule: ${currentRedirectAddress}=>${currentRedirectTargets} iceServers: ${JSON.stringify( + iceServers + )}`; +} + +async function checkSrflx(iceServers) { + const context = makeContextString(iceServers); + info(`checkSrflx ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx")); + info(`candidates: ${JSON.stringify(srflxCandidates)}`); + // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to + // result in a single srflx candidate + is( + srflxCandidates.length, + 2, + `Should have two srflx candidates with ${context}` + ); + pc.close(); +} + +async function checkNoSrflx(iceServers) { + const context = makeContextString(iceServers); + info(`checkNoSrflx ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx")); + info(`candidates: ${JSON.stringify(srflxCandidates)}`); + is( + srflxCandidates.length, + 0, + `Should have no srflx candidates with ${context}` + ); + pc.close(); +} + +async function checkRelayUdp(iceServers) { + const context = makeContextString(iceServers); + info(`checkRelayUdp ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const relayCandidates = candidates.filter(c => c.candidate.includes("relay")); + info(`candidates: ${JSON.stringify(relayCandidates)}`); + // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to + // result in a single relay candidate + is( + relayCandidates.length, + 2, + `Should have two relay candidates with ${context}` + ); + // It would be nice if RTCIceCandidate had a field telling us what the + // "related protocol" is (similar to relatedAddress and relatedPort). + // Because there is no such thing, we need to go through the stats API, + // which _does_ have that information. + is( + (await findStatsRelayCandidates(pc, "tcp")).length, + 0, + `No TCP relay candidates should be present with ${context}` + ); + pc.close(); +} + +async function checkRelayTcp(iceServers) { + const context = makeContextString(iceServers); + info(`checkRelayTcp ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const relayCandidates = candidates.filter(c => c.candidate.includes("relay")); + info(`candidates: ${JSON.stringify(relayCandidates)}`); + // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to + // result in a single relay candidate + is( + relayCandidates.length, + 2, + `Should have two relay candidates with ${context}` + ); + // It would be nice if RTCIceCandidate had a field telling us what the + // "related protocol" is (similar to relatedAddress and relatedPort). + // Because there is no such thing, we need to go through the stats API, + // which _does_ have that information. + is( + (await findStatsRelayCandidates(pc, "udp")).length, + 0, + `No UDP relay candidates should be present with ${context}` + ); + pc.close(); +} + +async function checkRelayUdpTcp(iceServers) { + const context = makeContextString(iceServers); + info(`checkRelayUdpTcp ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const relayCandidates = candidates.filter(c => c.candidate.includes("relay")); + info(`candidates: ${JSON.stringify(relayCandidates)}`); + // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to + // result in a single relay candidate each for UDP and TCP + is( + relayCandidates.length, + 4, + `Should have two relay candidates for each protocol with ${context}` + ); + // It would be nice if RTCIceCandidate had a field telling us what the + // "related protocol" is (similar to relatedAddress and relatedPort). + // Because there is no such thing, we need to go through the stats API, + // which _does_ have that information. + is( + (await findStatsRelayCandidates(pc, "udp")).length, + 2, + `Two UDP relay candidates should be present with ${context}` + ); + // TODO(bug 1705563): This is 1 because of bug 1705563 + is( + (await findStatsRelayCandidates(pc, "tcp")).length, + 1, + `One TCP relay candidates should be present with ${context}` + ); + pc.close(); +} + +async function checkNoRelay(iceServers) { + const context = makeContextString(iceServers); + info(`checkNoRelay ${context}`); + const pc = new RTCPeerConnection({ + iceServers, + bundlePolicy: "max-bundle", // Avoids extra candidates + }); + const candidates = await gatherWithTimeout(pc, 8000, context); + const relayCandidates = candidates.filter(c => c.candidate.includes("relay")); + info(`candidates: ${JSON.stringify(relayCandidates)}`); + is( + relayCandidates.length, + 0, + `Should have no relay candidates with ${context}` + ); + pc.close(); +} diff --git a/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js new file mode 100644 index 0000000000..c22130e23e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js @@ -0,0 +1,86 @@ +function identityPcTest(remoteOptions) { + var user = "someone"; + var domain1 = "test1.example.com"; + var domain2 = "test2.example.com"; + var id1 = user + "@" + domain1; + var id2 = user + "@" + domain2; + + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. TEST_AUDIO_FREQ matches the frequency of the + // tone in fake microphone devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + + test = new PeerConnectionTest({ + config_local: { + peerIdentity: id2, + }, + config_remote: { + peerIdentity: id1, + }, + }); + test.setMediaConstraints( + [ + { + audio: true, + video: true, + peerIdentity: id2, + }, + ], + [ + remoteOptions || { + audio: true, + video: true, + peerIdentity: id1, + }, + ] + ); + test.pcLocal.setIdentityProvider("test1.example.com", { protocol: "idp.js" }); + test.pcRemote.setIdentityProvider("test2.example.com", { + protocol: "idp.js", + }); + test.chain.append([ + function PEER_IDENTITY_IS_SET_CORRECTLY(test) { + // no need to wait to check identity in this case, + // setRemoteDescription should wait for the IdP to complete + function checkIdentity(pc, pfx, idp, name) { + return pc.peerIdentity.then(peerInfo => { + is(peerInfo.idp, idp, pfx + "IdP check"); + is(peerInfo.name, name + "@" + idp, pfx + "identity check"); + }); + } + + return Promise.all([ + checkIdentity( + test.pcLocal._pc, + "local: ", + "test2.example.com", + "someone" + ), + checkIdentity( + test.pcRemote._pc, + "remote: ", + "test1.example.com", + "someone" + ), + ]); + }, + + function REMOTE_STREAMS_ARE_RESTRICTED(test) { + var remoteStream = test.pcLocal._pc.getRemoteStreams()[0]; + for (const track of remoteStream.getTracks()) { + mustThrowWith( + `Freshly received ${track.kind} track with peerIdentity`, + "SecurityError", + () => new MediaRecorder(new MediaStream([track])).start() + ); + } + return Promise.all([ + audioIsSilence(true, remoteStream), + videoIsBlack(true, remoteStream), + ]); + }, + ]); + return test.run().finally(() => tone.stop()); +} diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-bad.js b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js new file mode 100644 index 0000000000..86e1cb7a34 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js @@ -0,0 +1 @@ +<This isn't valid JS> diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-min.js b/dom/media/webrtc/tests/mochitests/identity/idp-min.js new file mode 100644 index 0000000000..a4b2c55cee --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-min.js @@ -0,0 +1,24 @@ +(function (global) { + "use strict"; + // A minimal implementation of the interface. + // Though this isn't particularly functional. + // This is needed so that we can have a "working" IdP served + // from two different locations in the tree. + global.rtcIdentityProvider.register({ + generateAssertion(payload, origin, usernameHint) { + dump("idp: generateAssertion(" + payload + ")\n"); + return Promise.resolve({ + idp: { domain: "example.com", protocol: "idp.js" }, + assertion: "bogus", + }); + }, + + validateAssertion(assertion, origin) { + dump("idp: validateAssertion(" + assertion + ")\n"); + return Promise.resolve({ + identity: "user@example.com", + contents: "bogus", + }); + }, + }); +})(this); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js new file mode 100644 index 0000000000..75390cbf4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js @@ -0,0 +1,3 @@ +(function () { + dump("ERROR\n"); +})(); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^ new file mode 100644 index 0000000000..b3a2afd90a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/.well-known/idp-proxy/idp-redirect-https.js diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js new file mode 100644 index 0000000000..75390cbf4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js @@ -0,0 +1,3 @@ +(function () { + dump("ERROR\n"); +})(); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^ new file mode 100644 index 0000000000..d2380984e7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/.well-known/idp-proxy/idp.js diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js new file mode 100644 index 0000000000..75390cbf4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js @@ -0,0 +1,3 @@ +(function () { + dump("ERROR\n"); +})(); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^ new file mode 100644 index 0000000000..3fb8a35ae7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: https://example.com/.well-known/idp-proxy/idp-redirect-https.js diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js new file mode 100644 index 0000000000..75390cbf4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js @@ -0,0 +1,3 @@ +(function () { + dump("ERROR\n"); +})(); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^ new file mode 100644 index 0000000000..6e2931eda9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: https://example.com/.well-known/idp-min.js diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js new file mode 100644 index 0000000000..75390cbf4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js @@ -0,0 +1,3 @@ +(function () { + dump("ERROR\n"); +})(); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^ new file mode 100644 index 0000000000..77d56ac442 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: https://example.com/.well-known/idp-proxy/idp.js diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.js b/dom/media/webrtc/tests/mochitests/identity/idp.js new file mode 100644 index 0000000000..557740657f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp.js @@ -0,0 +1,119 @@ +(function (global) { + "use strict"; + + // rather than create a million different IdP configurations and litter the + // world with files all containing near-identical code, let's use the hash/URL + // fragment as a way of generating instructions for the IdP + var instructions = global.location.hash.replace("#", "").split(":"); + function is(target) { + return function (instruction) { + return instruction === target; + }; + } + + function IDPJS() { + this.domain = global.location.host; + var path = global.location.pathname; + this.protocol = + path.substring(path.lastIndexOf("/") + 1) + global.location.hash; + this.id = crypto.getRandomValues(new Uint8Array(10)).join("."); + } + + IDPJS.prototype = { + getLogin() { + return fetch( + "https://example.com/.well-known/idp-proxy/idp.sjs?" + this.id + ).then(response => response.status === 200); + }, + checkLogin(result) { + return this.getLogin().then(loggedIn => { + if (loggedIn) { + return result; + } + return Promise.reject({ + name: "IdpLoginError", + loginUrl: + "https://example.com/.well-known/idp-proxy/login.html#" + this.id, + }); + }); + }, + + borkResult(result) { + if (instructions.some(is("throw"))) { + throw new Error("Throwing!"); + } + if (instructions.some(is("fail"))) { + return Promise.reject(new Error("Failing!")); + } + if (instructions.some(is("login"))) { + return this.checkLogin(result); + } + if (instructions.some(is("hang"))) { + return new Promise(r => {}); + } + dump("idp: result=" + JSON.stringify(result) + "\n"); + return Promise.resolve(result); + }, + + _selectUsername(usernameHint) { + dump("_selectUsername: usernameHint(" + usernameHint + ")\n"); + var username = "someone@" + this.domain; + if (usernameHint) { + var at = usernameHint.indexOf("@"); + if (at < 0) { + username = usernameHint + "@" + this.domain; + } else if (usernameHint.substring(at + 1) === this.domain) { + username = usernameHint; + } + } + return username; + }, + + generateAssertion(payload, origin, options) { + dump( + "idp: generateAssertion(" + + payload + + ", " + + origin + + ", " + + JSON.stringify(options) + + ")\n" + ); + var idpDetails = { + domain: this.domain, + protocol: this.protocol, + }; + if (instructions.some(is("bad-assert"))) { + idpDetails = {}; + } + return this.borkResult({ + idp: idpDetails, + assertion: JSON.stringify({ + username: this._selectUsername(options.usernameHint), + contents: payload, + }), + }); + }, + + validateAssertion(assertion, origin) { + dump("idp: validateAssertion(" + assertion + ")\n"); + var assertion = JSON.parse(assertion); + if (instructions.some(is("bad-validate"))) { + assertion.contents = {}; + } + return this.borkResult({ + identity: assertion.username, + contents: assertion.contents, + }); + }, + }; + + if (!instructions.some(is("not_ready"))) { + dump("registering idp.js" + global.location.hash + "\n"); + var idp = new IDPJS(); + global.rtcIdentityProvider.register({ + generateAssertion: idp.generateAssertion.bind(idp), + validateAssertion: idp.validateAssertion.bind(idp), + }); + } +})(this); diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.sjs b/dom/media/webrtc/tests/mochitests/identity/idp.sjs new file mode 100644 index 0000000000..e1a245be78 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/idp.sjs @@ -0,0 +1,18 @@ +function handleRequest(request, response) { + var key = "/.well-known/idp-proxy/" + request.queryString; + dump(getState(key) + "\n"); + if (request.method === "GET") { + if (getState(key)) { + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + } + } else if (request.method === "PUT") { + setState(key, "OK"); + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + response.setStatusLine(request.httpVersion, 406, "Method Not Allowed"); + } + response.setHeader("Content-Type", "text/plain;charset=UTF-8"); + response.write("OK"); +} diff --git a/dom/media/webrtc/tests/mochitests/identity/login.html b/dom/media/webrtc/tests/mochitests/identity/login.html new file mode 100644 index 0000000000..eafba22f2d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/login.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Identity Provider Login</title> + <script type="application/javascript"> + window.onload = () => { + var xhr = new XMLHttpRequest(); + xhr.open("PUT", "https://example.com/.well-known/idp-proxy/idp.sjs?" + + window.location.hash.replace('#', '')); + xhr.onload = () => { + var isFramed = (window !== window.top); + var parent = isFramed ? window.parent : window.opener; + // Using '*' is cheating, but that's OK. + parent.postMessage('LOGINDONE', '*'); + var done = document.createElement('div'); + + done.textContent = 'Done'; + document.body.appendChild(done); + + if (!isFramed) { + window.close(); + } + }; + xhr.send(); + }; + </script> +</head> +<body> + <div>Logging in...</div> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/mochitest.toml b/dom/media/webrtc/tests/mochitests/identity/mochitest.toml new file mode 100644 index 0000000000..9b661663a6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/mochitest.toml @@ -0,0 +1,57 @@ +[DEFAULT] +subsuite = "media" +skip-if = ["os == 'linux' && !debug"] +support-files = [ + "/.well-known/idp-proxy/idp.js", + "identityPcTest.js", + "!/dom/media/webrtc/tests/mochitests/blacksilence.js", + "!/dom/media/webrtc/tests/mochitests/dataChannel.js", + "!/dom/media/webrtc/tests/mochitests/head.js", + "!/dom/media/webrtc/tests/mochitests/network.js", + "!/dom/media/webrtc/tests/mochitests/pc.js", + "!/dom/media/webrtc/tests/mochitests/sdpUtils.js", + "!/dom/media/webrtc/tests/mochitests/templates.js", + "!/dom/media/webrtc/tests/mochitests/turnConfig.js", +] +tags = "mtg" + +["test_fingerprints.html"] +scheme = "https" + +["test_getIdentityAssertion.html"] + +["test_idpproxy.html"] +support-files = [ + "/.well-known/idp-proxy/idp-redirect-http.js", + "/.well-known/idp-proxy/idp-redirect-http.js^headers^", + "/.well-known/idp-proxy/idp-redirect-http-trick.js", + "/.well-known/idp-proxy/idp-redirect-http-trick.js^headers^", + "/.well-known/idp-proxy/idp-redirect-https.js", + "/.well-known/idp-proxy/idp-redirect-https.js^headers^", + "/.well-known/idp-proxy/idp-redirect-https-double.js", + "/.well-known/idp-proxy/idp-redirect-https-double.js^headers^", + "/.well-known/idp-proxy/idp-redirect-https-odd-path.js", + "/.well-known/idp-proxy/idp-redirect-https-odd-path.js^headers^", + "/.well-known/idp-min.js", + "/.well-known/idp-proxy/idp-bad.js", +] + +["test_loginNeeded.html"] +support-files = [ + "/.well-known/idp-proxy/login.html", + "/.well-known/idp-proxy/idp.sjs", +] + +["test_peerConnection_asymmetricIsolation.html"] +scheme = "https" +skip-if = ["os == 'android'"] + +["test_peerConnection_peerIdentity.html"] +scheme = "https" +skip-if = ["os == 'android'"] + +["test_setIdentityProvider.html"] +scheme = "https" + +["test_setIdentityProviderWithErrors.html"] +scheme = "https" diff --git a/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html new file mode 100644 index 0000000000..bc30ba5c4c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html @@ -0,0 +1,91 @@ +<html> +<head> +<meta charset="utf-8" /> +<script type="application/javascript">var scriptRelativePath = "../";</script> +<script type="application/javascript" src="../pc.js"></script> +</head> +<body> +<script class="testbody" type="application/javascript"> +createHTML({ title: "Test multiple identity fingerprints", bug: "1005152" }); + +// here we call the identity provider directly +async function getIdentityAssertion(fingerprint) { + const { IdpSandbox } = SpecialPowers.ChromeUtils.importESModule( + 'resource://gre/modules/media/IdpSandbox.sys.mjs' + ); + const sandbox = new IdpSandbox('example.com', 'idp.js', window); + const idp = SpecialPowers.wrap(await sandbox.start()); + const assertion = SpecialPowers.wrap(await + idp.generateAssertion(JSON.stringify({ fingerprint }), + 'https://example.com', + {})); + const assertionString = btoa(JSON.stringify(assertion)); + sandbox.stop(); + return assertionString; +} + +// This takes a real fingerprint and makes some extra bad ones. +function makeFingerprints(algorithm, digest) { + const fingerprints = []; + fingerprints.push({ algorithm, digest }); + for (var i = 0; i < 3; ++i) { + fingerprints.push({ + algorithm, + digest: digest.replace(/:./g, ':' + i.toString(16)) + }); + } + return fingerprints; +} + +const fingerprintRegex = /^a=fingerprint:(\S+) (\S+)/m; +const identityRegex = /^a=identity:(\S+)/m; + +function fingerprintSdp(fingerprints) { + return fingerprints.map(fp => 'a=fInGeRpRiNt:' + fp.algorithm + + ' ' + fp.digest + '\n').join(''); +} + +// Firefox only uses a single fingerprint. +// That doesn't mean we have it create SDP that describes two. +// This function synthesizes that SDP and tries to set it. + +runNetworkTest(async () => { + // this one fails setRemoteDescription if the identity is not good + const pcStrict = new RTCPeerConnection({ peerIdentity: 'someone@example.com'}); + // this one will be manually tweaked to have two fingerprints + const pcDouble = new RTCPeerConnection({}); + + const stream = await getUserMedia({ video: true }); + ok(stream, 'Got test stream'); + const [track] = stream.getTracks(); + pcDouble.addTrack(track, stream); + try { + const offer = await pcDouble.createOffer(); + ok(offer, 'Got offer'); + const match = offer.sdp.match(fingerprintRegex); + if (!match) { + throw new Error('No fingerprint in offer SDP'); + } + const fingerprints = makeFingerprints(match[1], match[2]); + const assertion = await getIdentityAssertion(fingerprints); + ok(assertion, 'Should have assertion'); + + const sdp = offer.sdp.slice(0, match.index) + + 'a=identity:' + assertion + '\n' + + fingerprintSdp(fingerprints.slice(1)) + + offer.sdp.slice(match.index); + + await pcStrict.setRemoteDescription({ type: 'offer', sdp }); + ok(true, 'Modified fingerprints were accepted'); + } catch (error) { + const e = SpecialPowers.wrap(error); + ok(false, 'error in test: ' + + (e.message ? (e.message + '\n' + e.stack) : e)); + } + pcStrict.close(); + pcDouble.close(); + track.stop(); +}); +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html new file mode 100644 index 0000000000..47e1cb1df6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getIdentityAssertion Tests", + bug: "942367" + }); + +function checkIdentity(assertion, identity) { + // here we dig into the payload, which means we need to know something + // about how the IdP actually works (not good in general, but OK here) + var assertion = JSON.parse(atob(assertion)).assertion; + var user = JSON.parse(assertion).username; + is(user, identity, 'id should be "' + identity + '" is "' + user + '"'); +} + +function getAssertion(t, instructions, userHint) { + dump('instructions: ' + instructions + '\n'); + dump('userHint: ' + userHint + '\n'); + t.pcLocal.setIdentityProvider('example.com', + { protocol: 'idp.js' + instructions, + usernameHint: userHint }); + return t.pcLocal._pc.getIdentityAssertion(); +} + +var test; +function theTest() { + test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE'); + test.chain.append([ + function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER(t) { + return t.pcLocal._pc.getIdentityAssertion() + .then(a => ok(false, 'should fail without provider'), + e => ok(e, 'should fail without provider')); + }, + + function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITH_BAD_PROVIDER(t) { + t.pcLocal._pc.setIdentityProvider('example.com', + { protocol: 'idp-bad.js', + usernameHint: '' }); + return t.pcLocal._pc.getIdentityAssertion() + .then(a => ok(false, 'should fail with bad provider'), + e => { + is(e.name, 'IdpError', 'should fail with bad provider'); + ok(e.message, 'should include a nice message'); + }); + }, + + function PC_LOCAL_GET_TWO_ASSERTIONS(t) { + return Promise.all([ + getAssertion(t, ''), + getAssertion(t, '') + ]).then(assertions => { + is(assertions.length, 2, "Two assertions generated"); + assertions.forEach(a => checkIdentity(a, 'someone@example.com')); + }); + }, + + function PC_LOCAL_IDP_FAILS(t) { + return getAssertion(t, '#fail') + .then(a => ok(false, '#fail should not get an identity result'), + e => is(e.name, 'IdpError', '#fail should cause rejection')); + }, + + function PC_LOCAL_IDP_LOGIN_ERROR(t) { + return getAssertion(t, '#login') + .then(a => ok(false, '#login should not work'), + e => { + is(e.name, 'IdpLoginError', 'name is IdpLoginError'); + is(t.pcLocal._pc.idpLoginUrl.split('#')[0], + 'https://example.com/.well-known/idp-proxy/login.html', + 'got the right login URL from the IdP'); + }); + }, + + function PC_LOCAL_IDP_NOT_READY(t) { + return getAssertion(t, '#not_ready') + .then(a => ok(false, '#not_ready should not get an identity result'), + e => is(e.name, 'IdpError', '#not_ready should cause rejection')); + }, + + function PC_LOCAL_ASSERTION_WITH_SPECIFIC_NAME(t) { + return getAssertion(t, '', 'user@example.com') + .then(a => checkIdentity(a, 'user@example.com')); + } + ]); + return test.run(); +} +runNetworkTest(theTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html new file mode 100644 index 0000000000..5ab1ac119f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html @@ -0,0 +1,178 @@ +<html> +<head> +<meta charset="utf-8" /> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> + <script class="testbody" type="application/javascript"> +"use strict"; +var { IdpSandbox } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/media/IdpSandbox.sys.mjs" +); +var dummyPayload = JSON.stringify({ + this: 'is', + a: ['stu', 6], + obj: null +}); + +function test_domain_sandbox() { + var diabolical = { + toString() { + return 'example.com/path'; + } + }; + var domains = [ 'ex/foo', 'user@ex', 'user:pass@ex', 'ex#foo', 'ex?foo', + '', 12, null, diabolical, true ]; + domains.forEach(function(domain) { + try { + var idp = new IdpSandbox(domain, undefined, window); + ok(false, 'IdpSandbox allowed a bad domain: ' + domain); + } catch (e) { + var str = (typeof domain === 'string') ? domain : typeof domain; + ok(true, 'Evil domain "' + str + '" raises exception'); + } + }); +} + +function test_protocol_sandbox() { + var protos = [ '../evil/proto', '..%2Fevil%2Fproto', + '\\evil', '%5cevil', 12, true, {} ]; + protos.forEach(function(proto) { + try { + var idp = new IdpSandbox('example.com', proto, window); + ok(false, 'IdpSandbox allowed a bad protocol: ' + proto); + } catch (e) { + var str = (typeof proto === 'string') ? proto : typeof proto; + ok(true, 'Evil protocol "' + proto + '" raises exception'); + } + }); +} + +function idpName(hash) { + return 'idp.js' + (hash ? ('#' + hash) : ''); +} + +function makeSandbox(js) { + var name = js || idpName(); + info('Creating a sandbox for the protocol: ' + name); + var sandbox = new IdpSandbox('example.com', name, window); + return sandbox.start().then(idp => SpecialPowers.wrap(idp)); +} + +function test_generate_assertion() { + return makeSandbox() + .then(idp => idp.generateAssertion(dummyPayload, + 'https://example.net', + {})) + .then(response => { + response = SpecialPowers.wrap(response); + is(response.idp.domain, 'example.com', 'domain is correct'); + is(response.idp.protocol, 'idp.js', 'protocol is correct'); + ok(typeof response.assertion === 'string', 'assertion is present'); + }); +} + +// test that the test IdP can eat its own dogfood; which is the only way to test +// validateAssertion, since that consumes the output of generateAssertion (in +// theory, generateAssertion could identify a different IdP domain). + +function test_validate_assertion() { + return makeSandbox() + .then(idp => idp.generateAssertion(dummyPayload, + 'https://example.net', + { usernameHint: 'user' })) + .then(assertion => { + var wrapped = SpecialPowers.wrap(assertion); + return makeSandbox() + .then(idp => idp.validateAssertion(wrapped.assertion, + 'https://example.net')); + }).then(response => { + response = SpecialPowers.wrap(response); + is(response.identity, 'user@example.com'); + is(response.contents, dummyPayload); + }); +} + +// We don't want to test the #bad or the #hang instructions, +// errors of the sort those generate aren't handled by the sandbox code. +function test_assertion_failure(reason) { + return () => { + return makeSandbox(idpName(reason)) + .then(idp => idp.generateAssertion('hello', 'example.net', {})) + .then(r => ok(false, 'should not succeed on ' + reason), + e => ok(true, 'failed correctly on ' + reason)); + }; +} + +function test_load_failure() { + return makeSandbox('non-existent-file') + .then(() => ok(false, 'Should fail to load non-existent file'), + e => ok(e, 'Should fail to load non-existent file')); +} + +function test_redirect_ok(from) { + return () => { + return makeSandbox(from) + .then(idp => idp.generateAssertion('hello', 'example.net')) + .then(r => ok(SpecialPowers.wrap(r).assertion, + 'Redirect to https should be OK')); + }; +} + +function test_redirect_fail(from) { + return () => { + return makeSandbox(from) + .then(() => ok(false, 'Redirect to https should fail'), + e => ok(e, 'Redirect to https should fail')); + }; +} + +function test_bad_js() { + return makeSandbox('idp-bad.js') + .then(() => ok(false, 'Bad JS should not load'), + e => ok(e, 'Bad JS should not load')); +} + +function run_all_tests() { + [ + test_domain_sandbox, + test_protocol_sandbox, + test_generate_assertion, + test_validate_assertion, + + // fail of the IdP fails + test_assertion_failure('fail'), + // fail if the IdP throws + test_assertion_failure('throw'), + // fail if the IdP is not ready + test_assertion_failure('not_ready'), + + test_load_failure(), + // Test a redirect to an HTTPS origin, which should be OK + test_redirect_ok('idp-redirect-https.js'), + // Two redirects is fine too + test_redirect_ok('idp-redirect-https-double.js'), + // A secure redirect to a path other than /.well-known/idp-proxy/* should + // also work fine. + test_redirect_ok('idp-redirect-https-odd-path.js'), + // A redirect to HTTP is not-cool + test_redirect_fail('idp-redirect-http.js'), + // Also catch tricks like https->http->https + test_redirect_fail('idp-redirect-http-trick.js'), + + test_bad_js + ].reduce((p, test) => { + return p.then(test) + .catch(e => ok(false, test.name + ' failed: ' + + SpecialPowers.wrap(e).message + '\n' + + SpecialPowers.wrap(e).stack)); + }, Promise.resolve()) + .then(() => SimpleTest.finish()); +} + +SimpleTest.waitForExplicitFinish(); +run_all_tests(); +</script> + </body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html new file mode 100644 index 0000000000..550dc20d92 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: 'RTCPeerConnection identity with login', + bug: '1153314' + }); + +function waitForLoginDone() { + return new Promise(resolve => { + window.addEventListener('message', function listener(e) { + is(e.origin, 'https://example.com', 'got the right message origin'); + is(e.data, 'LOGINDONE', 'got the right message'); + window.removeEventListener('message', listener); + resolve(); + }); + }); +} + +function checkLogin(t, name, onLoginNeeded) { + t.pcLocal.setIdentityProvider('example.com', + { protocol: 'idp.js#login:' + name }); + return t.pcLocal._pc.getIdentityAssertion() + .then(a => ok(false, 'should request login'), + e => { + is(e.name, 'IdpLoginError', 'name is IdpLoginError'); + is(t.pcLocal._pc.idpLoginUrl.split('#')[0], + 'https://example.com/.well-known/idp-proxy/login.html', + 'got the right login URL from the IdP'); + return t.pcLocal._pc.idpLoginUrl; + }) + .then(onLoginNeeded) + .then(waitForLoginDone) + .then(() => t.pcLocal._pc.getIdentityAssertion()) + .then(a => ok(a, 'got assertion')); +} + +function theTest() { + var test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE'); + test.chain.append([ + function PC_LOCAL_IDENTITY_ASSERTION_WITH_IFRAME_LOGIN(t) { + return checkLogin(t, 'iframe', loginUrl => { + var iframe = document.createElement('iframe'); + iframe.setAttribute('src', loginUrl); + iframe.frameBorder = 0; + iframe.width = 400; + iframe.height = 60; + document.getElementById('display').appendChild(iframe); + }); + }, + function PC_LOCAL_IDENTITY_ASSERTION_WITH_WINDOW_LOGIN(t) { + return checkLogin(t, 'openwin', loginUrl => { + window.open(loginUrl, 'login', 'width=400,height=60'); + }); + } + ]); + return test.run(); +} +runNetworkTest(theTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html new file mode 100644 index 0000000000..65a2fc5392 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> + <script type="application/javascript" src="../blacksilence.js"></script> + <script type="application/javascript" src="identityPcTest.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "Non-isolated media entering an isolated session becomes isolated", + bug: "996238" +}); + +function theTest() { + // Override the remote media capture options to remove isolation for the + // remote party; the test verifies that the media it receives on the local + // side is isolated anyway. + return identityPcTest({ + audio: true, + video: true + }); +} +runNetworkTest(theTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html new file mode 100644 index 0000000000..a8116cc451 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> + <script type="application/javascript" src="../blacksilence.js"></script> + <script type="application/javascript" src="identityPcTest.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "setIdentityProvider leads to peerIdentity and assertions in SDP", + bug: "942367" +}); + +runNetworkTest(identityPcTest); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html new file mode 100644 index 0000000000..ac7cba6a5e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "setIdentityProvider leads to peerIdentity and assertions in SDP", + bug: "942367" + }); + +function checkIdentity(peer, prefix, idp, name) { + prefix = prefix + ": "; + return peer._pc.peerIdentity.then(peerIdentity => { + ok(peerIdentity, prefix + "peerIdentity is set"); + is(peerIdentity.idp, idp, prefix + "IdP is correct"); + is(peerIdentity.name, name + "@" + idp, prefix + "identity is correct"); + }); +} + +function theTest() { + var test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.pcLocal.setIdentityProvider("test1.example.com", + { protocol: "idp.js", + usernameHint: "someone" }); + test.pcRemote.setIdentityProvider("test2.example.com", + { protocol: "idp.js", + usernameHinte: "someone"}); + + test.chain.append([ + function PC_LOCAL_PEER_IDENTITY_IS_SET_CORRECTLY(test) { + return checkIdentity(test.pcLocal, "local", "test2.example.com", "someone"); + }, + function PC_REMOTE_PEER_IDENTITY_IS_SET_CORRECTLY(test) { + return checkIdentity(test.pcRemote, "remote", "test1.example.com", "someone"); + }, + + function OFFER_AND_ANSWER_INCLUDES_IDENTITY(test) { + ok(test.originalOffer.sdp.includes("a=identity"), "a=identity is in the offer SDP"); + ok(test.originalAnswer.sdp.includes("a=identity"), "a=identity is in the answer SDP"); + }, + + function PC_LOCAL_DESCRIPTIONS_CONTAIN_IDENTITY(test) { + ok(test.pcLocal.localDescription.sdp.includes("a=identity"), + "a=identity is in the local copy of the offer"); + ok(test.pcLocal.remoteDescription.sdp.includes("a=identity"), + "a=identity is in the local copy of the answer"); + }, + function PC_REMOTE_DESCRIPTIONS_CONTAIN_IDENTITY(test) { + ok(test.pcRemote.localDescription.sdp.includes("a=identity"), + "a=identity is in the remote copy of the offer"); + ok(test.pcRemote.remoteDescription.sdp.includes("a=identity"), + "a=identity is in the remote copy of the answer"); + } + ]); + return test.run(); +} +runNetworkTest(theTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html new file mode 100644 index 0000000000..ce6832d1e6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript">var scriptRelativePath = "../";</script> + <script type="application/javascript" src="../pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +'use strict'; + createHTML({ + title: "Identity Provider returning errors is handled correctly", + bug: "942367" + }); + +runNetworkTest(function () { + var test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + // No IdP for local. + // Remote generates a bad assertion, but that only fails to validate + test.pcRemote.setIdentityProvider('example.com', + { protocol: 'idp.js#bad-validate', + usernameHint: 'nobody' }); + + // Save the peerIdentity promises now, since when they reject they are + // replaced and we expect them to be rejected this time + var peerIdentityLocal = test.pcLocal._pc.peerIdentity; + var peerIdentityRemote = test.pcRemote._pc.peerIdentity; + + test.chain.append([ + function ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION(t) { + ok(!t.originalOffer.sdp.includes('a=identity'), + 'a=identity not contained in the offer SDP'); + ok(t.originalAnswer.sdp.includes('a=identity'), + 'a=identity is contained in the answer SDP'); + }, + function PEER_IDENTITY_IS_EMPTY(t) { + // we are only waiting for the local side to complete + // an error on the remote side is immediately fatal though + return Promise.race([ + peerIdentityLocal.then( + () => ok(false, t.pcLocal + ' incorrectly received valid peer identity'), + e => ok(e, t.pcLocal + ' correctly failed to validate peer identity')), + peerIdentityRemote.then( + () => ok(false, t.pcRemote + ' incorrecly received a valid peer identity'), + e => ok(false, t.pcRemote + ' incorrectly rejected peer identity')) + ]); + } + ]); + + return test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js new file mode 100644 index 0000000000..0f813fd627 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js @@ -0,0 +1,242 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const ENDED_TIMEOUT_LENGTH = 30000; + +/* The time we wait depends primarily on the canplaythrough event firing + * Note: this needs to be at least 30s because the + * B2G emulator in VMs is really slow. */ +const VERIFYPLAYING_TIMEOUT_LENGTH = 60000; + +/** + * This class manages playback of a HTMLMediaElement with a MediaStream. + * When constructed by a caller, an object instance is created with + * a media element and a media stream object. + * + * @param {HTMLMediaElement} mediaElement the media element for playback + * @param {MediaStream} mediaStream the media stream used in + * the mediaElement for playback + */ +function MediaStreamPlayback(mediaElement, mediaStream) { + this.mediaElement = mediaElement; + this.mediaStream = mediaStream; +} + +MediaStreamPlayback.prototype = { + /** + * Starts media element with a media stream, runs it until a canplaythrough + * and timeupdate event fires, and calls stop() on all its tracks. + * + * @param {Boolean} isResume specifies if this media element is being resumed + * from a previous run + */ + playMedia(isResume) { + this.startMedia(isResume); + return this.verifyPlaying() + .then(() => this.stopTracksForStreamInMediaPlayback()) + .then(() => this.detachFromMediaElement()); + }, + + /** + * Stops the local media stream's tracks while it's currently in playback in + * a media element. + * + * Precondition: The media stream and element should both be actively + * being played. All the stream's tracks must be local. + */ + stopTracksForStreamInMediaPlayback() { + var elem = this.mediaElement; + return Promise.all([ + haveEvent( + elem, + "ended", + wait(ENDED_TIMEOUT_LENGTH, new Error("Timeout")) + ), + ...this.mediaStream.getTracks().map(t => { + t.stop(); + return haveNoEvent(t, "ended"); + }), + ]); + }, + + /** + * Starts media with a media stream, runs it until a canplaythrough and + * timeupdate event fires, and detaches from the element without stopping media. + * + * @param {Boolean} isResume specifies if this media element is being resumed + * from a previous run + */ + playMediaWithoutStoppingTracks(isResume) { + this.startMedia(isResume); + return this.verifyPlaying().then(() => this.detachFromMediaElement()); + }, + + /** + * Starts the media with the associated stream. + * + * @param {Boolean} isResume specifies if the media element playback + * is being resumed from a previous run + */ + startMedia(isResume) { + // If we're playing media element for the first time, check that time is zero. + if (!isResume) { + is( + this.mediaElement.currentTime, + 0, + "Before starting the media element, currentTime = 0" + ); + } + this.canPlayThroughFired = listenUntil( + this.mediaElement, + "canplaythrough", + () => true + ); + + // Hooks up the media stream to the media element and starts playing it + this.mediaElement.srcObject = this.mediaStream; + this.mediaElement.play(); + }, + + /** + * Verifies that media is playing. + */ + verifyPlaying() { + var lastElementTime = this.mediaElement.currentTime; + + var mediaTimeProgressed = listenUntil( + this.mediaElement, + "timeupdate", + () => this.mediaElement.currentTime > lastElementTime + ); + + return timeout( + Promise.all([this.canPlayThroughFired, mediaTimeProgressed]), + VERIFYPLAYING_TIMEOUT_LENGTH, + "verifyPlaying timed out" + ).then(() => { + is(this.mediaElement.paused, false, "Media element should be playing"); + is( + this.mediaElement.duration, + Number.POSITIVE_INFINITY, + "Duration should be infinity" + ); + + // When the media element is playing with a real-time stream, we + // constantly switch between having data to play vs. queuing up data, + // so we can only check that the ready state is one of those two values + ok( + this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || + this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA, + "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA" + ); + + is(this.mediaElement.seekable.length, 0, "Seekable length shall be zero"); + is(this.mediaElement.buffered.length, 0, "Buffered length shall be zero"); + + is( + this.mediaElement.seeking, + false, + "MediaElement is not seekable with MediaStream" + ); + ok( + isNaN(this.mediaElement.startOffsetTime), + "Start offset time shall not be a number" + ); + is( + this.mediaElement.defaultPlaybackRate, + 1, + "DefaultPlaybackRate should be 1" + ); + is(this.mediaElement.playbackRate, 1, "PlaybackRate should be 1"); + is(this.mediaElement.preload, "none", 'Preload should be "none"'); + is(this.mediaElement.src, "", "No src should be defined"); + is( + this.mediaElement.currentSrc, + "", + "Current src should still be an empty string" + ); + }); + }, + + /** + * Detaches from the element without stopping the media. + * + * Precondition: The media stream and element should both be actively + * being played. + */ + detachFromMediaElement() { + this.mediaElement.pause(); + this.mediaElement.srcObject = null; + }, +}; + +// haxx to prevent SimpleTest from failing at window.onload +function addLoadEvent() {} + +/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */ +/* import-globals-from head.js */ +const scriptsReady = Promise.all( + ["/tests/SimpleTest/SimpleTest.js", "head.js"].map(script => { + const el = document.createElement("script"); + el.src = script; + document.head.appendChild(el); + return new Promise(r => (el.onload = r)); + }) +); + +function createHTML(options) { + return scriptsReady.then(() => realCreateHTML(options)); +} + +async function runTest(testFunction) { + await Promise.all([ + scriptsReady, + SpecialPowers.pushPrefEnv({ + set: [["media.navigator.permission.fake", true]], + }), + ]); + await runTestWhenReady(async (...args) => { + await testFunction(...args); + await noGum(); + }); +} + +// noGum - Helper to detect whether active guM tracks still exist. +// +// Note it relies on the permissions system to detect active tracks, so it won't +// catch getUserMedia use while media.navigator.permission.disabled is true +// (which is common in automation), UNLESS we set +// media.navigator.permission.fake to true also, like runTest() does above. +async function noGum() { + if (!navigator.mediaDevices) { + // No mediaDevices, then gUM cannot have been called either. + return; + } + const mediaManagerService = Cc[ + "@mozilla.org/mediaManagerService;1" + ].getService(Ci.nsIMediaManagerService); + + const hasCamera = {}; + const hasMicrophone = {}; + mediaManagerService.mediaCaptureWindowState( + window, + hasCamera, + hasMicrophone, + {}, + {}, + {}, + {}, + false + ); + is( + hasCamera.value, + mediaManagerService.STATE_NOCAPTURE, + "Test must leave no active camera gUM tracks behind." + ); + is( + hasMicrophone.value, + mediaManagerService.STATE_NOCAPTURE, + "Test must leave no active microphone gUM tracks behind." + ); +} diff --git a/dom/media/webrtc/tests/mochitests/mochitest.toml b/dom/media/webrtc/tests/mochitests/mochitest.toml new file mode 100644 index 0000000000..cf6381c63c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mochitest.toml @@ -0,0 +1,85 @@ +[DEFAULT] +tags = "mtg webrtc" +subsuite = "media" +scheme = "https" +support-files = [ + "head.js", + "dataChannel.js", + "mediaStreamPlayback.js", + "network.js", + "nonTrickleIce.js", + "pc.js", + "stats.js", + "templates.js", + "test_enumerateDevices_iframe.html", + "test_enumerateDevices_iframe_pre_gum.html", + "test_getUserMedia_permission_iframe.html", + "NetworkPreparationChromeScript.js", + "blacksilence.js", + "turnConfig.js", + "sdpUtils.js", + "addTurnsSelfsignedCert.js", + "parser_rtp.js", + "peerconnection_audio_forced_sample_rate.js", + "iceTestUtils.js", + "simulcast.js", + "helpers_from_wpt/sdp.js", + "!/dom/canvas/test/captureStream_common.js", + "!/dom/canvas/test/webgl-mochitest/webgl-util.js", + "!/dom/media/test/manifest.js", + "!/dom/media/test/seek.webm", + "!/dom/media/test/gizmo.mp4", + "!/docshell/test/navigation/blank.html", +] +prefs = [ + "focusmanager.testmode=true", # emulate focus + "privacy.partition.network_state=false", + "network.proxy.allow_hijacking_localhost=true", + "media.devices.enumerate.legacy.enabled=false", +] + +["test_1488832.html"] +skip-if = ["os == 'linux'"] # Bug 1714410 + +["test_1717318.html"] + +["test_a_noOp.html"] +scheme = "http" + +["test_enumerateDevices.html"] + +["test_enumerateDevices_getUserMediaFake.html"] + +["test_enumerateDevices_legacy.html"] + +["test_enumerateDevices_navigation.html"] +skip-if = ["true"] # Disabled because it is a racy test and causes timeouts, see bug 1650932 + +["test_fingerprinting_resistance.html"] +skip-if = ["os == 'linux' && asan"] # Bug 1646309 - low frequency intermittent + +["test_forceSampleRate.html"] +scheme = "http" + +["test_groupId.html"] + +["test_multi_mics.html"] +skip-if = ["os != 'linux'"] # the only platform with real devices + +["test_ondevicechange.html"] +run-sequentially = "sets prefs that may disrupt other tests" + +["test_setSinkId-echoCancellation.html"] +skip-if = ["os == 'android'"] # bug 1473346 - no setSinkId() + +["test_setSinkId-stream-source.html"] +skip-if = ["os == 'android'"] # bug 1473346 - no setSinkId() + +["test_setSinkId.html"] +skip-if = ["os != 'linux'"] # the only platform with real devices + +["test_setSinkId_default_addTrack.html"] + +["test_setSinkId_preMutedElement.html"] + +["test_unfocused_pref.html"] diff --git a/dom/media/webrtc/tests/mochitests/mochitest_datachannel.toml b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.toml new file mode 100644 index 0000000000..24c6c2f252 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.toml @@ -0,0 +1,65 @@ +[DEFAULT] +tags = "mtg webrtc" +subsuite = "media" +scheme = "https" +support-files = [ + "head.js", + "dataChannel.js", + "mediaStreamPlayback.js", + "network.js", + "nonTrickleIce.js", + "pc.js", + "stats.js", + "templates.js", + "test_enumerateDevices_iframe.html", + "test_getUserMedia_permission_iframe.html", + "NetworkPreparationChromeScript.js", + "blacksilence.js", + "turnConfig.js", + "sdpUtils.js", + "addTurnsSelfsignedCert.js", + "parser_rtp.js", + "peerconnection_audio_forced_sample_rate.js", + "iceTestUtils.js", + "simulcast.js", + "helpers_from_wpt/sdp.js", + "!/dom/canvas/test/captureStream_common.js", + "!/dom/canvas/test/webgl-mochitest/webgl-util.js", + "!/dom/media/test/manifest.js", + "!/dom/media/test/seek.webm", + "!/dom/media/test/gizmo.mp4", + "!/docshell/test/navigation/blank.html", +] +prefs = [ + "focusmanager.testmode=true", # emulate focus + "privacy.partition.network_state=false", + "network.proxy.allow_hijacking_localhost=true", +] + +["test_dataChannel_basicAudio.html"] + +["test_dataChannel_basicAudioVideo.html"] + +["test_dataChannel_basicAudioVideoCombined.html"] + +["test_dataChannel_basicAudioVideoNoBundle.html"] + +["test_dataChannel_basicDataOnly.html"] + +["test_dataChannel_basicVideo.html"] + +["test_dataChannel_bug1013809.html"] + +["test_dataChannel_dataOnlyBufferedAmountLow.html"] +scheme = "http" + +["test_dataChannel_dtlsVersions.html"] + +["test_dataChannel_hostnameObfuscation.html"] +scheme = "http" + +["test_dataChannel_noOffer.html"] +scheme = "http" + +["test_dataChannel_stats.html"] +scheme = "http" diff --git a/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.toml b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.toml new file mode 100644 index 0000000000..3e98ce10c6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.toml @@ -0,0 +1,156 @@ +[DEFAULT] +tags = "mtg webrtc" +subsuite = "media" +scheme = "https" +support-files = [ + "head.js", + "dataChannel.js", + "mediaStreamPlayback.js", + "network.js", + "nonTrickleIce.js", + "pc.js", + "stats.js", + "templates.js", + "test_enumerateDevices_iframe.html", + "test_getUserMedia_permission_iframe.html", + "NetworkPreparationChromeScript.js", + "blacksilence.js", + "turnConfig.js", + "sdpUtils.js", + "addTurnsSelfsignedCert.js", + "parser_rtp.js", + "peerconnection_audio_forced_sample_rate.js", + "iceTestUtils.js", + "simulcast.js", + "helpers_from_wpt/sdp.js", + "!/dom/canvas/test/captureStream_common.js", + "!/dom/canvas/test/webgl-mochitest/webgl-util.js", + "!/dom/media/test/manifest.js", + "!/dom/media/test/seek.webm", + "!/dom/media/test/sin-441-1s-44100.flac", + "!/dom/media/test/gizmo.mp4", + "!/docshell/test/navigation/blank.html", +] +prefs = [ + "focusmanager.testmode=true", # emulate focus + "privacy.partition.network_state=false", + "network.proxy.allow_hijacking_localhost=true", + "media.devices.enumerate.legacy.enabled=false", +] + +["test_defaultAudioConstraints.html"] +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", # Bug 1404995, no loopback devices on some platforms +] + +["test_getUserMedia_GC_MediaStream.html"] + +["test_getUserMedia_active_autoplay.html"] + +["test_getUserMedia_addTrackRemoveTrack.html"] + +["test_getUserMedia_addtrack_removetrack_events.html"] + +["test_getUserMedia_audioCapture.html"] +skip-if = ["os == 'android'"] # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333), aarch64 due to 1538359 + +["test_getUserMedia_audioConstraints.html"] +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", # Bug 1404995, no loopback devices on some platforms +] + +["test_getUserMedia_audioConstraints_concurrentIframes.html"] +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", + "(os == 'linux' && debug)", # Bug 1404995, no loopback devices on some platforms # Bug 1481101 + "os == 'linux' && !debug && !fission", # bug 1645930, lower frequency intermittent +] + +["test_getUserMedia_audioConstraints_concurrentStreams.html"] +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", # Bug 1404995, no loopback devices on some platforms +] + +["test_getUserMedia_basicAudio.html"] + +["test_getUserMedia_basicAudio_loopback.html"] +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", # Bug 1404995, no loopback devices on some platforms +] + +["test_getUserMedia_basicScreenshare.html"] +skip-if = [ + "os == 'android'", # no screenshare on android + "apple_silicon", # bug 1707742 +] + +["test_getUserMedia_basicTabshare.html"] +skip-if = ["os == 'android'"] # no windowshare on android + +["test_getUserMedia_basicVideo.html"] + +["test_getUserMedia_basicVideoAudio.html"] + +["test_getUserMedia_basicVideo_playAfterLoadedmetadata.html"] + +["test_getUserMedia_basicWindowshare.html"] +skip-if = ["os == 'android'"] # no windowshare on android + +["test_getUserMedia_bug1223696.html"] + +["test_getUserMedia_callbacks.html"] + +["test_getUserMedia_constraints.html"] + +["test_getUserMedia_cubebDisabled.html"] + +["test_getUserMedia_cubebDisabledFakeStreams.html"] + +["test_getUserMedia_getTrackById.html"] + +["test_getUserMedia_gumWithinGum.html"] + +["test_getUserMedia_loadedmetadata.html"] + +["test_getUserMedia_mediaElementCapture_audio.html"] + +["test_getUserMedia_mediaElementCapture_tracks.html"] + +["test_getUserMedia_mediaElementCapture_video.html"] + +["test_getUserMedia_mediaStreamClone.html"] + +["test_getUserMedia_mediaStreamConstructors.html"] + +["test_getUserMedia_mediaStreamTrackClone.html"] + +["test_getUserMedia_nonDefaultRate.html"] + +["test_getUserMedia_peerIdentity.html"] + +["test_getUserMedia_permission.html"] + +["test_getUserMedia_playAudioTwice.html"] + +["test_getUserMedia_playVideoAudioTwice.html"] + +["test_getUserMedia_playVideoTwice.html"] + +["test_getUserMedia_scarySources.html"] +skip-if = ["os == 'android'"] # no screenshare or windowshare on android + +["test_getUserMedia_spinEventLoop.html"] + +["test_getUserMedia_trackCloneCleanup.html"] + +["test_getUserMedia_trackEnded.html"] diff --git a/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml new file mode 100644 index 0000000000..22c7f6a70f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml @@ -0,0 +1,489 @@ +[DEFAULT] +tags = "mtg webrtc" +subsuite = "media" +scheme = "https" +support-files = [ + "head.js", + "dataChannel.js", + "mediaStreamPlayback.js", + "network.js", + "nonTrickleIce.js", + "pc.js", + "stats.js", + "templates.js", + "test_enumerateDevices_iframe.html", + "test_getUserMedia_permission_iframe.html", + "NetworkPreparationChromeScript.js", + "blacksilence.js", + "turnConfig.js", + "sdpUtils.js", + "addTurnsSelfsignedCert.js", + "parser_rtp.js", + "peerconnection_audio_forced_sample_rate.js", + "iceTestUtils.js", + "simulcast.js", + "helpers_from_wpt/sdp.js", + "!/dom/canvas/test/captureStream_common.js", + "!/dom/canvas/test/webgl-mochitest/webgl-util.js", + "!/dom/media/test/manifest.js", + "!/dom/media/test/seek.webm", + "!/dom/media/test/gizmo.mp4", + "!/docshell/test/navigation/blank.html", +] +prefs = [ + "focusmanager.testmode=true", # emulate focus + "privacy.partition.network_state=false", + "network.proxy.allow_hijacking_localhost=true", + "media.devices.enumerate.legacy.enabled=false", + "media.aboutwebrtc.hist.poll_interval_ms=2000", +] + +["test_peerConnection_addAudioTrackToExistingVideoStream.html"] + +["test_peerConnection_addDataChannel.html"] + +["test_peerConnection_addDataChannelNoBundle.html"] + +["test_peerConnection_addSecondAudioStream.html"] + +["test_peerConnection_addSecondAudioStreamNoBundle.html"] + +["test_peerConnection_addSecondVideoStream.html"] + +["test_peerConnection_addSecondVideoStreamNoBundle.html"] + +["test_peerConnection_addtrack_removetrack_events.html"] + +["test_peerConnection_answererAddSecondAudioStream.html"] + +["test_peerConnection_audioChannels.html"] + +["test_peerConnection_audioCodecs.html"] + +["test_peerConnection_audioContributingSources.html"] + +["test_peerConnection_audioRenegotiationInactiveAnswer.html"] + +["test_peerConnection_audioSynchronizationSources.html"] + +["test_peerConnection_audioSynchronizationSourcesUnidirectional.html"] + +["test_peerConnection_basicAudio.html"] + +["test_peerConnection_basicAudioDynamicPtMissingRtpmap.html"] + +["test_peerConnection_basicAudioNATRelay.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNATRelayTCP.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNATRelayTCPWithStun300.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNATRelayTLS.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNATRelayWithStun300.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNATSrflx.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioNoisyUDPBlock.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioPcmaPcmuOnly.html"] + +["test_peerConnection_basicAudioRelayPolicy.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_basicAudioRequireEOC.html"] + +["test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html"] + +["test_peerConnection_basicAudioVideo.html"] + +["test_peerConnection_basicAudioVideoCombined.html"] + +["test_peerConnection_basicAudioVideoNoBundle.html"] + +["test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html"] + +["test_peerConnection_basicAudioVideoNoRtcpMux.html"] + +["test_peerConnection_basicAudioVideoTransceivers.html"] + +["test_peerConnection_basicAudioVideoVerifyExtmap.html"] + +["test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html"] + +["test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html"] + +["test_peerConnection_basicAudio_forced_higher_rate.html"] + +["test_peerConnection_basicAudio_forced_lower_rate.html"] + +["test_peerConnection_basicH264Video.html"] +skip-if = ["os == 'android' && is_emulator"] # Bug 1355786, No h264 support on android emulator + +["test_peerConnection_basicScreenshare.html"] +skip-if = ["os == 'android'"] # no screenshare on android + +["test_peerConnection_basicVideo.html"] + +["test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html"] + +["test_peerConnection_basicWindowshare.html"] +skip-if = ["os == 'android'"] # no screenshare on android + +["test_peerConnection_bug822674.html"] +scheme = "http" + +["test_peerConnection_bug825703.html"] +scheme = "http" + +["test_peerConnection_bug827843.html"] + +["test_peerConnection_bug834153.html"] +scheme = "http" + +["test_peerConnection_bug1013809.html"] + +["test_peerConnection_bug1042791.html"] +skip-if = ["os == 'android' && is_emulator"] # Bug 1355786, No h264 support on android emulator + +["test_peerConnection_bug1227781.html"] +scheme = "http" + +["test_peerConnection_bug1512281.html"] +fail-if = ["true"] + +["test_peerConnection_bug1773067.html"] + +["test_peerConnection_callbacks.html"] + +["test_peerConnection_captureStream_canvas_2d.html"] +scheme = "http" + +["test_peerConnection_captureStream_canvas_2d_noSSRC.html"] +scheme = "http" + +["test_peerConnection_captureStream_canvas_webgl.html"] +scheme = "http" + +["test_peerConnection_capturedVideo.html"] +tags = "capturestream" +skip-if = ["os == 'android'"] # android(Bug 1189784, timeouts on 4.3 emulator), Bug 1264340 + +["test_peerConnection_certificates.html"] + +["test_peerConnection_checkPacketDumpHook.html"] + +["test_peerConnection_close.html"] +scheme = "http" + +["test_peerConnection_closeDuringIce.html"] + +["test_peerConnection_codecNegotiationFailure.html"] + +["test_peerConnection_constructedStream.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_disabledVideoPreNegotiation.html"] + +["test_peerConnection_encodingsNegotiation.html"] + +["test_peerConnection_errorCallbacks.html"] +scheme = "http" + +["test_peerConnection_extmapRenegotiation.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_forwarding_basicAudioVideoCombined.html"] +skip-if = ["os == 'android'"] # Bug 1189784 + +["test_peerConnection_gatherWithSetConfiguration.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_gatherWithStun300.html"] +skip-if = ["os == 'android'"] # websockets don't work on android (bug 1266217) +scheme = "http" + +["test_peerConnection_gatherWithStun300IPv6.html"] +skip-if = [ + "os == 'android'", # websockets don't work on android (bug 1266217) + "os == 'mac'", # no ipv6 support on OS X testers (bug 1710706) + "os == 'win'", # no ipv6 support on windows testers (bug 1710706) +] +scheme = "http" + +["test_peerConnection_glean.html"] +disabled = "bug 1832459" + +["test_peerConnection_iceFailure.html"] +skip-if = ["true"] # (Bug 1180388 for win, mac and linux), android(Bug 1189784), Bug 1180388 +scheme = "http" + +["test_peerConnection_insertDTMF.html"] + +["test_peerConnection_localReofferRollback.html"] + +["test_peerConnection_localRollback.html"] + +["test_peerConnection_maxFsConstraint.html"] + +["test_peerConnection_multiple_captureStream_canvas_2d.html"] +scheme = "http" + +["test_peerConnection_noTrickleAnswer.html"] + +["test_peerConnection_noTrickleOffer.html"] + +["test_peerConnection_noTrickleOfferAnswer.html"] + +["test_peerConnection_nonDefaultRate.html"] + +["test_peerConnection_offerRequiresReceiveAudio.html"] + +["test_peerConnection_offerRequiresReceiveVideo.html"] + +["test_peerConnection_offerRequiresReceiveVideoAudio.html"] + +["test_peerConnection_portRestrictions.html"] + +["test_peerConnection_promiseSendOnly.html"] + +["test_peerConnection_recordReceiveTrack.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_relayOnly.html"] +disabled = "bug 1612063 # test is racy" + +["test_peerConnection_remoteReofferRollback.html"] + +["test_peerConnection_remoteRollback.html"] + +["test_peerConnection_removeAudioTrack.html"] + +["test_peerConnection_removeThenAddAudioTrack.html"] + +["test_peerConnection_removeThenAddAudioTrackNoBundle.html"] + +["test_peerConnection_removeThenAddVideoTrack.html"] + +["test_peerConnection_removeThenAddVideoTrackNoBundle.html"] + +["test_peerConnection_removeVideoTrack.html"] + +["test_peerConnection_renderAfterRenegotiation.html"] +scheme = "http" + +["test_peerConnection_replaceNullTrackThenRenegotiateAudio.html"] + +["test_peerConnection_replaceNullTrackThenRenegotiateVideo.html"] + +["test_peerConnection_replaceTrack.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_replaceTrack_camera.html"] +skip-if = ["os == 'android'"] # Bug 1614460 + +["test_peerConnection_replaceTrack_disabled.html"] +skip-if = ["os == 'android'"] # Bug 1614460 + +["test_peerConnection_replaceTrack_microphone.html"] + +["test_peerConnection_replaceVideoThenRenegotiate.html"] + +["test_peerConnection_restartIce.html"] + +["test_peerConnection_restartIceBadAnswer.html"] + +["test_peerConnection_restartIceLocalAndRemoteRollback.html"] + +["test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html"] + +["test_peerConnection_restartIceLocalRollback.html"] + +["test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html"] + +["test_peerConnection_restartIceNoBundle.html"] + +["test_peerConnection_restartIceNoBundleNoRtcpMux.html"] + +["test_peerConnection_restartIceNoRtcpMux.html"] + +["test_peerConnection_restrictBandwidthTargetBitrate.html"] + +["test_peerConnection_restrictBandwidthWithTias.html"] + +["test_peerConnection_rtcp_rsize.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_scaleResolution.html"] + +["test_peerConnection_scaleResolution_oldSetParameters.html"] + +["test_peerConnection_sender_and_receiver_stats.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_setLocalAnswerInHaveLocalOffer.html"] + +["test_peerConnection_setLocalAnswerInStable.html"] + +["test_peerConnection_setLocalOfferInHaveRemoteOffer.html"] + +["test_peerConnection_setParameters.html"] + +["test_peerConnection_setParameters_maxFramerate.html"] + +["test_peerConnection_setParameters_maxFramerate_oldSetParameters.html"] + +["test_peerConnection_setParameters_oldSetParameters.html"] + +["test_peerConnection_setParameters_scaleResolutionDownBy.html"] + +["test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html"] + +["test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html"] + +["test_peerConnection_setRemoteAnswerInStable.html"] + +["test_peerConnection_setRemoteOfferInHaveLocalOffer.html"] + +["test_peerConnection_sillyCodecPriorities.html"] + +["test_peerConnection_simulcastAnswer.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_simulcastAnswer_lowResFirst.html"] +skip-if = [ + "os == 'android'", # no simulcast support on android +] + +["test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html"] +skip-if = [ + "os == 'android'", # no simulcast support on android +] + +["test_peerConnection_simulcastAnswer_oldSetParameters.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_simulcastOddResolution.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_simulcastOddResolution_oldSetParameters.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_simulcastOffer.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_simulcastOffer_lowResFirst.html"] +skip-if = [ + "os == 'android'", # no simulcast support on android +] + +["test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html"] +skip-if = [ + "os == 'android'", # no simulcast support on android +] + +["test_peerConnection_simulcastOffer_oldSetParameters.html"] +skip-if = ["os == 'android'"] # no simulcast support on android + +["test_peerConnection_stats.html"] +skip-if = [ + "os == 'linux' && tsan", #Bug 1838736 + "win11_2009", # Bug 1780717 +] + +["test_peerConnection_stats_jitter.html"] +skip-if = ["tsan"] # Bug 1672590, TSan is just too slow to pass this test + +["test_peerConnection_stats_oneway.html"] + +["test_peerConnection_stats_relayProtocol.html"] +skip-if = [ + "os == 'android'", + "socketprocess_e10s", # android(Bug 1189784, timeouts on 4.3 emulator, Bug 1373858, Bug 1521117) +] +scheme = "http" + +["test_peerConnection_stereoFmtpPref.html"] + +["test_peerConnection_syncSetDescription.html"] + +["test_peerConnection_telephoneEventFirst.html"] + +["test_peerConnection_threeUnbundledConnections.html"] + +["test_peerConnection_throwInCallbacks.html"] + +["test_peerConnection_toJSON.html"] +scheme = "http" + +["test_peerConnection_trackDisabling.html"] +skip-if = ["os == 'android'"] # Bug 1614460 + +["test_peerConnection_trackDisabling_clones.html"] + +["test_peerConnection_trackless_sender_stats.html"] +skip-if = ["os == 'linux' && tsan"] #Bug 1838736 + +["test_peerConnection_twoAudioStreams.html"] + +["test_peerConnection_twoAudioTracksInOneStream.html"] + +["test_peerConnection_twoAudioVideoStreams.html"] +skip-if = [ + "os == 'android'", # android(Bug 1189784) + "os == 'linux' && tsan", #Bug 1838736 +] + +["test_peerConnection_twoAudioVideoStreamsCombined.html"] +skip-if = [ + "os == 'android'", # android(Bug 1189784) + "os == 'linux' && asan", # Bug 1480942 for Linux asan + "os == 'linux' && tsan", #Bug 1838736 +] + +["test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html"] +skip-if = [ + "os == 'android'", # Bug 1189784 + "os == 'linux' && asan", # Bug 1480942 + "os == 'linux' && tsan", #Bug 1838736 +] + +["test_peerConnection_twoVideoStreams.html"] + +["test_peerConnection_twoVideoTracksInOneStream.html"] + +["test_peerConnection_verifyAudioAfterRenegotiation.html"] +skip-if = ["os == 'android' && processor == 'x86_64' && !debug"] # Bug 1783287 + +["test_peerConnection_verifyDescriptions.html"] + +["test_peerConnection_verifyVideoAfterRenegotiation.html"] + +["test_peerConnection_videoCodecs.html"] +skip-if = ["os == 'android'"] # android(Bug 1614460) + +["test_peerConnection_videoRenegotiationInactiveAnswer.html"] + +["test_peerConnection_webAudio.html"] +tags = "webaudio webrtc" +scheme = "http" + +["test_selftest.html"] +# Bug 1227781: Crash with bogus TURN server. +scheme = "http" diff --git a/dom/media/webrtc/tests/mochitests/network.js b/dom/media/webrtc/tests/mochitests/network.js new file mode 100644 index 0000000000..223721b111 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/network.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * A stub function for preparing the network if needed + * + */ +async function startNetworkAndTest() {} + +/** + * A stub function to shutdown the network if needed + */ +async function networkTestFinished() {} diff --git a/dom/media/webrtc/tests/mochitests/nonTrickleIce.js b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js new file mode 100644 index 0000000000..9361944791 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function removeTrickleOption(desc) { + var sdp = desc.sdp.replace(/\r\na=ice-options:trickle\r\n/, "\r\n"); + return new RTCSessionDescription({ type: desc.type, sdp }); +} + +function makeOffererNonTrickle(chain) { + chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", [ + function PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER(test) { + // We need to install this callback before calling setLocalDescription + // otherwise we might miss callbacks + test.pcLocal.setupIceCandidateHandler(test, () => {}); + // We ignore ICE candidates because we want the full offer + }, + ]); + chain.replace("PC_REMOTE_GET_OFFER", [ + function PC_REMOTE_GET_FULL_OFFER(test) { + return test.pcLocal.endOfTrickleIce.then(() => { + test._local_offer = removeTrickleOption(test.pcLocal.localDescription); + test._offer_constraints = test.pcLocal.constraints; + test._offer_options = test.pcLocal.offerOptions; + }); + }, + ]); + chain.insertAfter("PC_REMOTE_SANE_REMOTE_SDP", [ + function PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES(test) { + info( + "test.pcLocal.localDescription.sdp: " + + JSON.stringify(test.pcLocal.localDescription.sdp) + ); + info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp)); + is( + test.pcRemote._pc.canTrickleIceCandidates, + false, + "Remote thinks that trickle isn't supported" + ); + ok(!test.localRequiresTrickleIce, "Local does NOT require trickle"); + ok( + test._local_offer.sdp.includes("a=candidate"), + "offer has ICE candidates" + ); + ok( + test._local_offer.sdp.includes("a=end-of-candidates"), + "offer has end-of-candidates" + ); + }, + ]); + chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC"); +} + +function makeAnswererNonTrickle(chain) { + chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", [ + function PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER(test) { + // We need to install this callback before calling setLocalDescription + // otherwise we might miss callbacks + test.pcRemote.setupIceCandidateHandler(test, () => {}); + // We ignore ICE candidates because we want the full offer + }, + ]); + chain.replace("PC_LOCAL_GET_ANSWER", [ + function PC_LOCAL_GET_FULL_ANSWER(test) { + return test.pcRemote.endOfTrickleIce.then(() => { + test._remote_answer = removeTrickleOption( + test.pcRemote.localDescription + ); + test._answer_constraints = test.pcRemote.constraints; + }); + }, + ]); + chain.insertAfter("PC_LOCAL_SANE_REMOTE_SDP", [ + function PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES(test) { + info( + "test.pcRemote.localDescription.sdp: " + + JSON.stringify(test.pcRemote.localDescription.sdp) + ); + info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp)); + is( + test.pcLocal._pc.canTrickleIceCandidates, + false, + "Local thinks that trickle isn't supported" + ); + ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle"); + ok( + test._remote_answer.sdp.includes("a=candidate"), + "answer has ICE candidates" + ); + ok( + test._remote_answer.sdp.includes("a=end-of-candidates"), + "answer has end-of-candidates" + ); + }, + ]); + chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC"); +} diff --git a/dom/media/webrtc/tests/mochitests/parser_rtp.js b/dom/media/webrtc/tests/mochitests/parser_rtp.js new file mode 100644 index 0000000000..2275c1f787 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/parser_rtp.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* + * Parses an RTP packet + * @param buffer an ArrayBuffer that contains the packet + * @return { type: "rtp", header: {...}, payload: a DataView } + */ +var ParseRtpPacket = buffer => { + // DataView.getFooInt returns big endian numbers by default + let view = new DataView(buffer); + + // Standard Header Fields + // https://tools.ietf.org/html/rfc3550#section-5.1 + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |V=2|P|X| CC |M| PT | sequence number | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | timestamp | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | synchronization source (SSRC) identifier | + // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + // | contributing source (CSRC) identifiers | + // | .... | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + let header = {}; + let offset = 0; + // Note that incrementing the offset happens as close to reading the data as + // possible. This simplifies ensuring that the number of read bytes and the + // offset increment match. Data may be manipulated between when the offset is + // incremented and before the next read. + let byte = view.getUint8(offset); + offset++; + // Version 2 Bit + header.version = (0xc0 & byte) >> 6; + // Padding 1 Bit + header.padding = (0x30 & byte) >> 5; + // Extension 1 Bit + header.extensionsPresent = (0x10 & byte) >> 4 == 1; + // CSRC count 4 Bit + header.csrcCount = 0xf & byte; + + byte = view.getUint8(offset); + offset++; + // Marker 1 Bit + header.marker = (0x80 & byte) >> 7; + // Payload Type 7 Bit + header.payloadType = 0x7f & byte; + // Sequence Number 16 Bit + header.sequenceNumber = view.getUint16(offset); + offset += 2; + // Timestamp 32 Bit + header.timestamp = view.getUint32(offset); + offset += 4; + // SSRC 32 Bit + header.ssrc = view.getUint32(offset); + offset += 4; + + // CSRC 32 Bit + header.csrcs = []; + for (let c = 0; c < header.csrcCount; c++) { + header.csrcs.push(view.getUint32(offset)); + offset += 4; + } + + // Extensions + header.extensions = []; + header.extensionPaddingBytes = 0; + header.extensionsTotalLength = 0; + if (header.extensionsPresent) { + // https://tools.ietf.org/html/rfc3550#section-5.3.1 + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | defined by profile | length | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | header extension | + // | .... | + let addExtension = (id, len) => + header.extensions.push({ + id, + data: new DataView(buffer, offset, len), + }); + let extensionId = view.getUint16(offset); + offset += 2; + // len is in 32 bit units, not bytes + header.extensionsTotalLength = view.getUint16(offset) * 4; + offset += 2; + // Check for https://tools.ietf.org/html/rfc5285 + if (extensionId != 0xbede) { + // No rfc5285 + addExtension(extensionId, header.extensionsTotalLength); + offset += header.extensionsTotalLength; + } else { + let expectedEnd = offset + header.extensionsTotalLength; + while (offset < expectedEnd) { + // We only support "one-byte" extension headers ATM + // https://tools.ietf.org/html/rfc5285#section-4.2 + // 0 + // 0 1 2 3 4 5 6 7 + // +-+-+-+-+-+-+-+-+ + // | ID | len | + // +-+-+-+-+-+-+-+-+ + byte = view.getUint8(offset); + offset++; + // Check for padding which can occur between extensions or at the end + if (byte == 0) { + header.extensionPaddingBytes++; + continue; + } + let id = (byte & 0xf0) >> 4; + // Check for the FORBIDDEN id (15), dun dun dun + if (id == 15) { + // Ignore bytes until until the end of extensions + offset = expectedEnd; + break; + } + // the length of the extention is len + 1 + let len = (byte & 0x0f) + 1; + addExtension(id, len); + offset += len; + } + } + } + return { type: "rtp", header, payload: new DataView(buffer, offset) }; +}; diff --git a/dom/media/webrtc/tests/mochitests/pc.js b/dom/media/webrtc/tests/mochitests/pc.js new file mode 100644 index 0000000000..73e1b2c2f0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/pc.js @@ -0,0 +1,2496 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const LOOPBACK_ADDR = "127.0.0."; + +const iceStateTransitions = { + new: ["checking", "closed"], //Note: 'failed' might need to added here + // even though it is not in the standard + checking: ["new", "connected", "failed", "closed"], //Note: do we need to + // allow 'completed' in + // here as well? + connected: ["new", "completed", "disconnected", "closed"], + completed: ["new", "disconnected", "closed"], + disconnected: ["new", "connected", "completed", "failed", "closed"], + failed: ["new", "disconnected", "closed"], + closed: [], +}; + +const signalingStateTransitions = { + stable: ["have-local-offer", "have-remote-offer", "closed"], + "have-local-offer": [ + "have-remote-pranswer", + "stable", + "closed", + "have-local-offer", + ], + "have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"], + "have-remote-offer": [ + "have-local-pranswer", + "stable", + "closed", + "have-remote-offer", + ], + "have-local-pranswer": ["stable", "closed", "have-local-pranswer"], + closed: [], +}; + +var makeDefaultCommands = () => { + return [].concat( + commandsPeerConnectionInitial, + commandsGetUserMedia, + commandsPeerConnectionOfferAnswer + ); +}; + +/** + * This class handles tests for peer connections. + * + * @constructor + * @param {object} [options={}] + * Optional options for the peer connection test + * @param {object} [options.commands=commandsPeerConnection] + * Commands to run for the test + * @param {bool} [options.is_local=true] + * true if this test should run the tests for the "local" side. + * @param {bool} [options.is_remote=true] + * true if this test should run the tests for the "remote" side. + * @param {object} [options.config_local=undefined] + * Configuration for the local peer connection instance + * @param {object} [options.config_remote=undefined] + * Configuration for the remote peer connection instance. If not defined + * the configuration from the local instance will be used + */ +function PeerConnectionTest(options) { + // If no options are specified make it an empty object + options = options || {}; + options.commands = options.commands || makeDefaultCommands(); + options.is_local = "is_local" in options ? options.is_local : true; + options.is_remote = "is_remote" in options ? options.is_remote : true; + + options.h264 = "h264" in options ? options.h264 : false; + options.bundle = "bundle" in options ? options.bundle : true; + options.rtcpmux = "rtcpmux" in options ? options.rtcpmux : true; + options.opus = "opus" in options ? options.opus : true; + options.ssrc = "ssrc" in options ? options.ssrc : true; + + options.config_local = options.config_local || {}; + options.config_remote = options.config_remote || {}; + + if (!options.bundle) { + // Make sure neither end tries to use bundle-only! + options.config_local.bundlePolicy = "max-compat"; + options.config_remote.bundlePolicy = "max-compat"; + } + + if (iceServersArray.length) { + if (!options.turn_disabled_local && !options.config_local.iceServers) { + options.config_local.iceServers = iceServersArray; + } + if (!options.turn_disabled_remote && !options.config_remote.iceServers) { + options.config_remote.iceServers = iceServersArray; + } + } else if (typeof turnServers !== "undefined") { + if (!options.turn_disabled_local && turnServers.local) { + if (!options.config_local.hasOwnProperty("iceServers")) { + options.config_local.iceServers = turnServers.local.iceServers; + } + } + if (!options.turn_disabled_remote && turnServers.remote) { + if (!options.config_remote.hasOwnProperty("iceServers")) { + options.config_remote.iceServers = turnServers.remote.iceServers; + } + } + } + + if (options.is_local) { + this.pcLocal = new PeerConnectionWrapper("pcLocal", options.config_local); + } else { + this.pcLocal = null; + } + + if (options.is_remote) { + this.pcRemote = new PeerConnectionWrapper( + "pcRemote", + options.config_remote || options.config_local + ); + } else { + this.pcRemote = null; + } + + // Create command chain instance and assign default commands + this.chain = new CommandChain(this, options.commands); + + this.testOptions = options; +} + +/** TODO: consider removing this dependency on timeouts */ +function timerGuard(p, time, message) { + return Promise.race([ + p, + wait(time).then(() => { + throw new Error("timeout after " + time / 1000 + "s: " + message); + }), + ]); +} + +/** + * Closes the peer connection if it is active + */ +PeerConnectionTest.prototype.closePC = function () { + info("Closing peer connections"); + + var closeIt = pc => { + if (!pc || pc.signalingState === "closed") { + return Promise.resolve(); + } + + var promise = Promise.all([ + Promise.all( + pc._pc + .getReceivers() + .filter(receiver => receiver.track.readyState == "live") + .map(receiver => { + info( + "Waiting for track " + + receiver.track.id + + " (" + + receiver.track.kind + + ") to end." + ); + return haveEvent(receiver.track, "ended", wait(50000)).then( + event => { + is( + event.target, + receiver.track, + "Event target should be the correct track" + ); + info(pc + " ended fired for track " + receiver.track.id); + }, + e => + e + ? Promise.reject(e) + : ok( + false, + "ended never fired for track " + receiver.track.id + ) + ); + }) + ), + ]); + pc.close(); + return promise; + }; + + return timerGuard( + Promise.all([closeIt(this.pcLocal), closeIt(this.pcRemote)]), + 60000, + "failed to close peer connection" + ); +}; + +/** + * Close the open data channels, followed by the underlying peer connection + */ +PeerConnectionTest.prototype.close = function () { + var allChannels = (this.pcLocal || this.pcRemote).dataChannels; + return timerGuard( + Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))), + 120000, + "failed to close data channels" + ).then(() => this.closePC()); +}; + +/** + * Close the specified data channels + * + * @param {Number} index + * Index of the data channels to close on both sides + */ +PeerConnectionTest.prototype.closeDataChannels = function (index) { + info("closeDataChannels called with index: " + index); + var localChannel = null; + if (this.pcLocal) { + localChannel = this.pcLocal.dataChannels[index]; + } + var remoteChannel = null; + if (this.pcRemote) { + remoteChannel = this.pcRemote.dataChannels[index]; + } + + // We need to setup all the close listeners before calling close + var setupClosePromise = channel => { + if (!channel) { + return Promise.resolve(); + } + return new Promise(resolve => { + channel.onclose = () => { + is( + channel.readyState, + "closed", + name + " channel " + index + " closed" + ); + resolve(); + }; + }); + }; + + // make sure to setup close listeners before triggering any actions + var allClosed = Promise.all([ + setupClosePromise(localChannel), + setupClosePromise(remoteChannel), + ]); + var complete = timerGuard( + allClosed, + 120000, + "failed to close data channel pair" + ); + + // triggering close on one side should suffice + if (remoteChannel) { + remoteChannel.close(); + } else if (localChannel) { + localChannel.close(); + } + + return complete; +}; + +/** + * Send data (message or blob) to the other peer + * + * @param {String|Blob} data + * Data to send to the other peer. For Blobs the MIME type will be lost. + * @param {Object} [options={ }] + * Options to specify the data channels to be used + * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]] + * Data channel to use for sending the message + * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]] + * Data channel to use for receiving the message + */ +PeerConnectionTest.prototype.send = async function (data, options) { + options = options || {}; + const source = + options.sourceChannel || + this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1]; + const target = + options.targetChannel || + this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1]; + source.bufferedAmountLowThreshold = options.bufferedAmountLowThreshold || 0; + + const getSizeInBytes = d => { + if (d instanceof Blob) { + return d.size; + } else if (d instanceof ArrayBuffer) { + return d.byteLength; + } else if (d instanceof String || typeof d === "string") { + return new TextEncoder().encode(d).length; + } else { + ok(false); + throw new Error("Could not get size"); + } + }; + + const expectedSizeInBytes = getSizeInBytes(data); + const bufferedAmount = source.bufferedAmount; + + source.send(data); + is( + source.bufferedAmount, + expectedSizeInBytes + bufferedAmount, + `Buffered amount should be ${expectedSizeInBytes}` + ); + + await new Promise(resolve => (source.onbufferedamountlow = resolve)); + + return new Promise(resolve => { + // Register event handler for the target channel + target.onmessage = e => { + is( + getSizeInBytes(e.data), + expectedSizeInBytes, + `Expected to receive the same number of bytes as we sent (${expectedSizeInBytes})` + ); + resolve({ channel: target, data: e.data }); + }; + }); +}; + +/** + * Create a data channel + * + * @param {Dict} options + * Options for the data channel (see nsIPeerConnection) + */ +PeerConnectionTest.prototype.createDataChannel = function (options) { + var remotePromise; + if (!options.negotiated) { + this.pcRemote.expectDataChannel("pcRemote expected data channel"); + remotePromise = this.pcRemote.nextDataChannel; + } + + // Create the datachannel + var localChannel = this.pcLocal.createDataChannel(options); + var localPromise = localChannel.opened; + + if (options.negotiated) { + remotePromise = localPromise.then(localChannel => { + // externally negotiated - we need to open from both ends + options.id = options.id || channel.id; // allow for no id on options + var remoteChannel = this.pcRemote.createDataChannel(options); + return remoteChannel.opened; + }); + } + + // pcRemote.observedNegotiationNeeded might be undefined if + // !options.negotiated, which means we just wait on pcLocal + return Promise.all([ + this.pcLocal.observedNegotiationNeeded, + this.pcRemote.observedNegotiationNeeded, + ]).then(() => { + return Promise.all([localPromise, remotePromise]).then(result => { + return { local: result[0], remote: result[1] }; + }); + }); +}; + +/** + * Creates an answer for the specified peer connection instance + * and automatically handles the failure case. + * + * @param {PeerConnectionWrapper} peer + * The peer connection wrapper to run the command on + */ +PeerConnectionTest.prototype.createAnswer = function (peer) { + return peer.createAnswer().then(answer => { + // make a copy so this does not get updated with ICE candidates + this.originalAnswer = new RTCSessionDescription( + JSON.parse(JSON.stringify(answer)) + ); + return answer; + }); +}; + +/** + * Creates an offer for the specified peer connection instance + * and automatically handles the failure case. + * + * @param {PeerConnectionWrapper} peer + * The peer connection wrapper to run the command on + */ +PeerConnectionTest.prototype.createOffer = function (peer) { + return peer.createOffer().then(offer => { + // make a copy so this does not get updated with ICE candidates + this.originalOffer = new RTCSessionDescription( + JSON.parse(JSON.stringify(offer)) + ); + return offer; + }); +}; + +/** + * Sets the local description for the specified peer connection instance + * and automatically handles the failure case. + * + * @param {PeerConnectionWrapper} peer + The peer connection wrapper to run the command on + * @param {RTCSessionDescriptionInit} desc + * Session description for the local description request + */ +PeerConnectionTest.prototype.setLocalDescription = function ( + peer, + desc, + stateExpected +) { + var eventFired = new Promise(resolve => { + peer.onsignalingstatechange = e => { + info(peer + ": 'signalingstatechange' event received"); + var state = e.target.signalingState; + if (stateExpected === state) { + peer.setLocalDescStableEventDate = new Date(); + resolve(); + } else { + ok( + false, + "This event has either already fired or there has been a " + + "mismatch between event received " + + state + + " and event expected " + + stateExpected + ); + } + }; + }); + + var stateChanged = peer.setLocalDescription(desc).then(() => { + peer.setLocalDescDate = new Date(); + }); + + peer.endOfTrickleSdp = peer.endOfTrickleIce + .then(() => { + return peer._pc.localDescription; + }) + .catch(e => ok(false, "Sending EOC message failed: " + e)); + + return Promise.all([eventFired, stateChanged]); +}; + +/** + * Sets the media constraints for both peer connection instances. + * + * @param {object} constraintsLocal + * Media constrains for the local peer connection instance + * @param constraintsRemote + */ +PeerConnectionTest.prototype.setMediaConstraints = function ( + constraintsLocal, + constraintsRemote +) { + if (this.pcLocal) { + this.pcLocal.constraints = constraintsLocal; + } + if (this.pcRemote) { + this.pcRemote.constraints = constraintsRemote; + } +}; + +/** + * Sets the media options used on a createOffer call in the test. + * + * @param {object} options the media constraints to use on createOffer + */ +PeerConnectionTest.prototype.setOfferOptions = function (options) { + if (this.pcLocal) { + this.pcLocal.offerOptions = options; + } +}; + +/** + * Sets the remote description for the specified peer connection instance + * and automatically handles the failure case. + * + * @param {PeerConnectionWrapper} peer + The peer connection wrapper to run the command on + * @param {RTCSessionDescriptionInit} desc + * Session description for the remote description request + */ +PeerConnectionTest.prototype.setRemoteDescription = function ( + peer, + desc, + stateExpected +) { + var eventFired = new Promise(resolve => { + peer.onsignalingstatechange = e => { + info(peer + ": 'signalingstatechange' event received"); + var state = e.target.signalingState; + if (stateExpected === state) { + peer.setRemoteDescStableEventDate = new Date(); + resolve(); + } else { + ok( + false, + "This event has either already fired or there has been a " + + "mismatch between event received " + + state + + " and event expected " + + stateExpected + ); + } + }; + }); + + var stateChanged = peer.setRemoteDescription(desc).then(() => { + peer.setRemoteDescDate = new Date(); + peer.checkMediaTracks(); + }); + + return Promise.all([eventFired, stateChanged]); +}; + +/** + * Adds and removes steps to/from the execution chain based on the configured + * testOptions. + */ +PeerConnectionTest.prototype.updateChainSteps = function () { + if (this.testOptions.h264) { + this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [ + PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER, + ]); + } + if (!this.testOptions.bundle) { + this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [ + PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER, + ]); + } + if (!this.testOptions.rtcpmux) { + this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [ + PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER, + ]); + } + if (!this.testOptions.ssrc) { + this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [ + PC_LOCAL_REMOVE_SSRC_FROM_OFFER, + ]); + this.chain.insertAfterEach("PC_REMOTE_CREATE_ANSWER", [ + PC_REMOTE_REMOVE_SSRC_FROM_ANSWER, + ]); + } + if (!this.testOptions.is_local) { + this.chain.filterOut(/^PC_LOCAL/); + } + if (!this.testOptions.is_remote) { + this.chain.filterOut(/^PC_REMOTE/); + } +}; + +/** + * Start running the tests as assigned to the command chain. + */ +PeerConnectionTest.prototype.run = async function () { + /* We have to modify the chain here to allow tests which modify the default + * test chain instantiating a PeerConnectionTest() */ + this.updateChainSteps(); + try { + await this.chain.execute(); + await this.close(); + } catch (e) { + const stack = + typeof e.stack === "string" + ? ` ${e.stack.split("\n").join(" ... ")}` + : ""; + ok(false, `Error in test execution: ${e} (${stack})`); + } +}; + +/** + * Routes ice candidates from one PCW to the other PCW + */ +PeerConnectionTest.prototype.iceCandidateHandler = function ( + caller, + candidate +) { + info("Received: " + JSON.stringify(candidate) + " from " + caller); + + var target = null; + if (caller.includes("pcLocal")) { + if (this.pcRemote) { + target = this.pcRemote; + } + } else if (caller.includes("pcRemote")) { + if (this.pcLocal) { + target = this.pcLocal; + } + } else { + ok(false, "received event from unknown caller: " + caller); + return; + } + + if (target) { + target.storeOrAddIceCandidate(candidate); + } else { + info("sending ice candidate to signaling server"); + send_message({ type: "ice_candidate", ice_candidate: candidate }); + } +}; + +/** + * Installs a polling function for the socket.io client to read + * all messages from the chat room into a message queue. + */ +PeerConnectionTest.prototype.setupSignalingClient = function () { + this.signalingMessageQueue = []; + this.signalingCallbacks = {}; + this.signalingLoopRun = true; + + var queueMessage = message => { + info("Received signaling message: " + JSON.stringify(message)); + var fired = false; + Object.keys(this.signalingCallbacks).forEach(name => { + if (name === message.type) { + info("Invoking callback for message type: " + name); + this.signalingCallbacks[name](message); + fired = true; + } + }); + if (!fired) { + this.signalingMessageQueue.push(message); + info( + "signalingMessageQueue.length: " + this.signalingMessageQueue.length + ); + } + if (this.signalingLoopRun) { + wait_for_message().then(queueMessage); + } else { + info("Exiting signaling message event loop"); + } + }; + wait_for_message().then(queueMessage); +}; + +/** + * Sets a flag to stop reading further messages from the chat room. + */ +PeerConnectionTest.prototype.signalingMessagesFinished = function () { + this.signalingLoopRun = false; +}; + +/** + * Register a callback function to deliver messages from the chat room + * directly instead of storing them in the message queue. + * + * @param {string} messageType + * For which message types should the callback get invoked. + * + * @param {function} onMessage + * The function which gets invoked if a message of the messageType + * has been received from the chat room. + */ +PeerConnectionTest.prototype.registerSignalingCallback = function ( + messageType, + onMessage +) { + this.signalingCallbacks[messageType] = onMessage; +}; + +/** + * Searches the message queue for the first message of a given type + * and invokes the given callback function, or registers the callback + * function for future messages if the queue contains no such message. + * + * @param {string} messageType + * The type of message to search and register for. + */ +PeerConnectionTest.prototype.getSignalingMessage = function (messageType) { + var i = this.signalingMessageQueue.findIndex(m => m.type === messageType); + if (i >= 0) { + info( + "invoking callback on message " + + i + + " from message queue, for message type:" + + messageType + ); + return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]); + } + return new Promise(resolve => + this.registerSignalingCallback(messageType, resolve) + ); +}; + +/** + * This class acts as a wrapper around a DataChannel instance. + * + * @param dataChannel + * @param peerConnectionWrapper + * @constructor + */ +function DataChannelWrapper(dataChannel, peerConnectionWrapper) { + this._channel = dataChannel; + this._pc = peerConnectionWrapper; + + info("Creating " + this); + + /** + * Setup appropriate callbacks + */ + createOneShotEventWrapper(this, this._channel, "close"); + createOneShotEventWrapper(this, this._channel, "error"); + createOneShotEventWrapper(this, this._channel, "message"); + createOneShotEventWrapper(this, this._channel, "bufferedamountlow"); + + this.opened = timerGuard( + new Promise(resolve => { + this._channel.onopen = () => { + this._channel.onopen = unexpectedEvent(this, "onopen"); + is(this.readyState, "open", "data channel is 'open' after 'onopen'"); + resolve(this); + }; + }), + 180000, + "channel didn't open in time" + ); +} + +DataChannelWrapper.prototype = { + /** + * Returns the binary type of the channel + * + * @returns {String} The binary type + */ + get binaryType() { + return this._channel.binaryType; + }, + + /** + * Sets the binary type of the channel + * + * @param {String} type + * The new binary type of the channel + */ + set binaryType(type) { + this._channel.binaryType = type; + }, + + /** + * Returns the label of the underlying data channel + * + * @returns {String} The label + */ + get label() { + return this._channel.label; + }, + + /** + * Returns the protocol of the underlying data channel + * + * @returns {String} The protocol + */ + get protocol() { + return this._channel.protocol; + }, + + /** + * Returns the id of the underlying data channel + * + * @returns {number} The stream id + */ + get id() { + return this._channel.id; + }, + + /** + * Returns the reliable state of the underlying data channel + * + * @returns {bool} The stream's reliable state + */ + get reliable() { + return this._channel.reliable; + }, + + /** + * Returns the ordered attribute of the data channel + * + * @returns {bool} The ordered attribute + */ + get ordered() { + return this._channel.ordered; + }, + + /** + * Returns the maxPacketLifeTime attribute of the data channel + * + * @returns {number} The maxPacketLifeTime attribute + */ + get maxPacketLifeTime() { + return this._channel.maxPacketLifeTime; + }, + + /** + * Returns the maxRetransmits attribute of the data channel + * + * @returns {number} The maxRetransmits attribute + */ + get maxRetransmits() { + return this._channel.maxRetransmits; + }, + + /** + * Returns the readyState bit of the data channel + * + * @returns {String} The state of the channel + */ + get readyState() { + return this._channel.readyState; + }, + + get bufferedAmount() { + return this._channel.bufferedAmount; + }, + + /** + * Sets the bufferlowthreshold of the channel + * + * @param {integer} amoutn + * The new threshold for the chanel + */ + set bufferedAmountLowThreshold(amount) { + this._channel.bufferedAmountLowThreshold = amount; + }, + + /** + * Close the data channel + */ + close() { + info(this + ": Closing channel"); + this._channel.close(); + }, + + /** + * Send data through the data channel + * + * @param {String|Object} data + * Data which has to be sent through the data channel + */ + send(data) { + info(this + ": Sending data '" + data + "'"); + this._channel.send(data); + }, + + /** + * Returns the string representation of the class + * + * @returns {String} The string representation + */ + toString() { + return ( + "DataChannelWrapper (" + this._pc.label + "_" + this._channel.label + ")" + ); + }, +}; + +/** + * This class acts as a wrapper around a PeerConnection instance. + * + * @constructor + * @param {string} label + * Description for the peer connection instance + * @param {object} configuration + * Configuration for the peer connection instance + */ +function PeerConnectionWrapper(label, configuration) { + this.configuration = configuration; + if (configuration && configuration.label_suffix) { + label = label + "_" + configuration.label_suffix; + } + this.label = label; + + this.constraints = []; + this.offerOptions = {}; + + this.dataChannels = []; + + this._local_ice_candidates = []; + this._remote_ice_candidates = []; + this.localRequiresTrickleIce = false; + this.remoteRequiresTrickleIce = false; + this.localMediaElements = []; + this.remoteMediaElements = []; + this.audioElementsOnly = false; + + this._sendStreams = []; + + this.expectedLocalTrackInfo = []; + this.remoteStreamsByTrackId = new Map(); + + this.disableRtpCountChecking = false; + + this.iceConnectedResolve; + this.iceConnectedReject; + this.iceConnected = new Promise((resolve, reject) => { + this.iceConnectedResolve = resolve; + this.iceConnectedReject = reject; + }); + this.iceCheckingRestartExpected = false; + this.iceCheckingIceRollbackExpected = false; + + info("Creating " + this); + this._pc = new RTCPeerConnection(this.configuration); + + /** + * Setup callback handlers + */ + // This allows test to register their own callbacks for ICE connection state changes + this.ice_connection_callbacks = {}; + + this._pc.oniceconnectionstatechange = e => { + isnot( + typeof this._pc.iceConnectionState, + "undefined", + "iceConnectionState should not be undefined" + ); + var iceState = this._pc.iceConnectionState; + info( + this + ": oniceconnectionstatechange fired, new state is: " + iceState + ); + Object.keys(this.ice_connection_callbacks).forEach(name => { + this.ice_connection_callbacks[name](); + }); + if (iceState === "connected") { + this.iceConnectedResolve(); + } else if (iceState === "failed") { + this.iceConnectedReject(new Error("ICE failed")); + } + }; + + this._pc.onicegatheringstatechange = e => { + isnot( + typeof this._pc.iceGatheringState, + "undefined", + "iceGetheringState should not be undefined" + ); + var gatheringState = this._pc.iceGatheringState; + info( + this + + ": onicegatheringstatechange fired, new state is: " + + gatheringState + ); + }; + + createOneShotEventWrapper(this, this._pc, "datachannel"); + this._pc.addEventListener("datachannel", e => { + var wrapper = new DataChannelWrapper(e.channel, this); + this.dataChannels.push(wrapper); + }); + + createOneShotEventWrapper(this, this._pc, "signalingstatechange"); + createOneShotEventWrapper(this, this._pc, "negotiationneeded"); +} + +PeerConnectionWrapper.prototype = { + /** + * Returns the senders + * + * @returns {sequence<RTCRtpSender>} the senders + */ + getSenders() { + return this._pc.getSenders(); + }, + + /** + * Returns the getters + * + * @returns {sequence<RTCRtpReceiver>} the receivers + */ + getReceivers() { + return this._pc.getReceivers(); + }, + + /** + * Returns the local description. + * + * @returns {object} The local description + */ + get localDescription() { + return this._pc.localDescription; + }, + + /** + * Returns the remote description. + * + * @returns {object} The remote description + */ + get remoteDescription() { + return this._pc.remoteDescription; + }, + + /** + * Returns the signaling state. + * + * @returns {object} The local description + */ + get signalingState() { + return this._pc.signalingState; + }, + /** + * Returns the ICE connection state. + * + * @returns {object} The local description + */ + get iceConnectionState() { + return this._pc.iceConnectionState; + }, + + setIdentityProvider(provider, options) { + this._pc.setIdentityProvider(provider, options); + }, + + elementPrefix: direction => { + return [this.label, direction].join("_"); + }, + + getMediaElementForTrack(track, direction) { + var prefix = this.elementPrefix(direction); + return getMediaElementForTrack(track, prefix); + }, + + createMediaElementForTrack(track, direction) { + var prefix = this.elementPrefix(direction); + return createMediaElementForTrack(track, prefix); + }, + + ensureMediaElement(track, direction) { + var prefix = this.elementPrefix(direction); + var element = this.getMediaElementForTrack(track, direction); + if (!element) { + element = this.createMediaElementForTrack(track, direction); + if (direction == "local") { + this.localMediaElements.push(element); + } else if (direction == "remote") { + this.remoteMediaElements.push(element); + } + } + + // We do this regardless, because sometimes we end up with a new stream with + // an old id (ie; the rollback tests cause the same stream to be added + // twice) + element.srcObject = new MediaStream([track]); + element.play(); + }, + + addSendStream(stream) { + // The PeerConnection will not necessarily know about this stream + // automatically, because replaceTrack is not told about any streams the + // new track might be associated with. Only content really knows. + this._sendStreams.push(stream); + }, + + getStreamForSendTrack(track) { + return this._sendStreams.find(str => str.getTrackById(track.id)); + }, + + getStreamForRecvTrack(track) { + return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id)); + }, + + /** + * Attaches a local track to this RTCPeerConnection using + * RTCPeerConnection.addTrack(). + * + * Also creates a media element playing a MediaStream containing all + * tracks that have been added to `stream` using `attachLocalTrack()`. + * + * @param {MediaStreamTrack} track + * MediaStreamTrack to handle + * @param {MediaStream} stream + * MediaStream to use as container for `track` on remote side + */ + attachLocalTrack(track, stream) { + info("Got a local " + track.kind + " track"); + + this.expectNegotiationNeeded(); + var sender = this._pc.addTrack(track, stream); + is(sender.track, track, "addTrack returns sender"); + is( + this._pc.getSenders().pop(), + sender, + "Sender should be the last element in getSenders()" + ); + + ok(track.id, "track has id"); + ok(track.kind, "track has kind"); + ok(stream.id, "stream has id"); + this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id }); + this.addSendStream(stream); + + // This will create one media element per track, which might not be how + // we set up things with the RTCPeerConnection. It's the only way + // we can ensure all sent tracks are flowing however. + this.ensureMediaElement(track, "local"); + + return this.observedNegotiationNeeded; + }, + + /** + * Callback when we get local media. Also an appropriate HTML media element + * will be created and added to the content node. + * + * @param {MediaStream} stream + * Media stream to handle + */ + attachLocalStream(stream, useAddTransceiver) { + info("Got local media stream: (" + stream.id + ")"); + + this.expectNegotiationNeeded(); + if (useAddTransceiver) { + info("Using addTransceiver (on PC)."); + stream.getTracks().forEach(track => { + var transceiver = this._pc.addTransceiver(track, { streams: [stream] }); + is(transceiver.sender.track, track, "addTransceiver returns sender"); + }); + } + // In order to test both the addStream and addTrack APIs, we do half one + // way, half the other, at random. + else if (Math.random() < 0.5) { + info("Using addStream."); + this._pc.addStream(stream); + ok( + this._pc + .getSenders() + .find(sender => sender.track == stream.getTracks()[0]), + "addStream returns sender" + ); + } else { + info("Using addTrack (on PC)."); + stream.getTracks().forEach(track => { + var sender = this._pc.addTrack(track, stream); + is(sender.track, track, "addTrack returns sender"); + }); + } + + this.addSendStream(stream); + + stream.getTracks().forEach(track => { + ok(track.id, "track has id"); + ok(track.kind, "track has kind"); + const sender = this._pc.getSenders().find(s => s.track == track); + ok(sender, "track has a sender"); + this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id }); + this.ensureMediaElement(track, "local"); + }); + + return this.observedNegotiationNeeded; + }, + + removeSender(index) { + var sender = this._pc.getSenders()[index]; + this.expectedLocalTrackInfo = this.expectedLocalTrackInfo.filter( + i => i.sender != sender + ); + this.expectNegotiationNeeded(); + this._pc.removeTrack(sender); + return this.observedNegotiationNeeded; + }, + + senderReplaceTrack(sender, withTrack, stream) { + const info = this.expectedLocalTrackInfo.find(i => i.sender == sender); + if (!info) { + return undefined; // replaceTrack on a null track, probably + } + info.track = withTrack; + this.addSendStream(stream); + this.ensureMediaElement(withTrack, "local"); + return sender.replaceTrack(withTrack); + }, + + async getUserMedia(constraints) { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var stream = await getUserMedia(constraints); + if (constraints.audio) { + stream.getAudioTracks().forEach(track => { + info( + this + + " gUM local stream " + + stream.id + + " with audio track " + + track.id + ); + }); + } + if (constraints.video) { + stream.getVideoTracks().forEach(track => { + info( + this + + " gUM local stream " + + stream.id + + " with video track " + + track.id + ); + }); + } + return stream; + }, + + /** + * Requests all the media streams as specified in the constrains property. + * + * @param {array} constraintsList + * Array of constraints for GUM calls + */ + getAllUserMedia(constraintsList) { + if (constraintsList.length === 0) { + info("Skipping GUM: no UserMedia requested"); + return Promise.resolve(); + } + + info("Get " + constraintsList.length + " local streams"); + return Promise.all( + constraintsList.map(constraints => this.getUserMedia(constraints)) + ); + }, + + async getAllUserMediaAndAddStreams(constraintsList) { + var streams = await this.getAllUserMedia(constraintsList); + if (!streams) { + return undefined; + } + return Promise.all(streams.map(stream => this.attachLocalStream(stream))); + }, + + async getAllUserMediaAndAddTransceivers(constraintsList) { + var streams = await this.getAllUserMedia(constraintsList); + if (!streams) { + return undefined; + } + return Promise.all( + streams.map(stream => this.attachLocalStream(stream, true)) + ); + }, + + /** + * Create a new data channel instance. Also creates a promise called + * `this.nextDataChannel` that resolves when the next data channel arrives. + */ + expectDataChannel(message) { + this.nextDataChannel = new Promise(resolve => { + this.ondatachannel = e => { + ok(e.channel, message); + is( + e.channel.readyState, + "open", + "data channel in 'open' after 'ondatachannel'" + ); + resolve(e.channel); + }; + }); + }, + + /** + * Create a new data channel instance + * + * @param {Object} options + * Options which get forwarded to nsIPeerConnection.createDataChannel + * @returns {DataChannelWrapper} The created data channel + */ + createDataChannel(options) { + var label = "channel_" + this.dataChannels.length; + info(this + ": Create data channel '" + label); + + if (!this.dataChannels.length) { + this.expectNegotiationNeeded(); + } + var channel = this._pc.createDataChannel(label, options); + is(channel.readyState, "connecting", "initial readyState is 'connecting'"); + var wrapper = new DataChannelWrapper(channel, this); + this.dataChannels.push(wrapper); + return wrapper; + }, + + /** + * Creates an offer and automatically handles the failure case. + */ + createOffer() { + return this._pc.createOffer(this.offerOptions).then(offer => { + info("Got offer: " + JSON.stringify(offer)); + // note: this might get updated through ICE gathering + this._latest_offer = offer; + return offer; + }); + }, + + /** + * Creates an answer and automatically handles the failure case. + */ + createAnswer() { + return this._pc.createAnswer().then(answer => { + info(this + ": Got answer: " + JSON.stringify(answer)); + this._last_answer = answer; + return answer; + }); + }, + + /** + * Sets the local description and automatically handles the failure case. + * + * @param {object} desc + * RTCSessionDescriptionInit for the local description request + */ + setLocalDescription(desc) { + this.observedNegotiationNeeded = undefined; + return this._pc.setLocalDescription(desc).then(() => { + info(this + ": Successfully set the local description"); + }); + }, + + /** + * Tries to set the local description and expect failure. Automatically + * causes the test case to fail if the call succeeds. + * + * @param {object} desc + * RTCSessionDescriptionInit for the local description request + * @returns {Promise} + * A promise that resolves to the expected error + */ + setLocalDescriptionAndFail(desc) { + return this._pc + .setLocalDescription(desc) + .then( + generateErrorCallback("setLocalDescription should have failed."), + err => { + info(this + ": As expected, failed to set the local description"); + return err; + } + ); + }, + + /** + * Sets the remote description and automatically handles the failure case. + * + * @param {object} desc + * RTCSessionDescriptionInit for the remote description request + */ + setRemoteDescription(desc) { + this.observedNegotiationNeeded = undefined; + // This has to be done before calling sRD, otherwise a candidate in flight + // could end up in the PC's operations queue before sRD resolves. + if (desc.type == "rollback") { + this.holdIceCandidates = new Promise( + r => (this.releaseIceCandidates = r) + ); + } + return this._pc.setRemoteDescription(desc).then(() => { + info(this + ": Successfully set remote description"); + if (desc.type != "rollback") { + this.releaseIceCandidates(); + } + }); + }, + + /** + * Tries to set the remote description and expect failure. Automatically + * causes the test case to fail if the call succeeds. + * + * @param {object} desc + * RTCSessionDescriptionInit for the remote description request + * @returns {Promise} + * a promise that resolve to the returned error + */ + setRemoteDescriptionAndFail(desc) { + return this._pc + .setRemoteDescription(desc) + .then( + generateErrorCallback("setRemoteDescription should have failed."), + err => { + info(this + ": As expected, failed to set the remote description"); + return err; + } + ); + }, + + /** + * Registers a callback for the signaling state change and + * appends the new state to an array for logging it later. + */ + logSignalingState() { + this.signalingStateLog = [this._pc.signalingState]; + this._pc.addEventListener("signalingstatechange", e => { + var newstate = this._pc.signalingState; + var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1]; + if (Object.keys(signalingStateTransitions).includes(oldstate)) { + ok( + signalingStateTransitions[oldstate].includes(newstate), + this + + ": legal signaling state transition from " + + oldstate + + " to " + + newstate + ); + } else { + ok( + false, + this + + ": old signaling state " + + oldstate + + " missing in signaling transition array" + ); + } + this.signalingStateLog.push(newstate); + }); + }, + + isTrackOnPC(track) { + return !!this.getStreamForRecvTrack(track); + }, + + allExpectedTracksAreObserved(expected, observed) { + return Object.keys(expected).every(trackId => observed[trackId]); + }, + + setupStreamEventHandlers(stream) { + const myTrackIds = new Set(stream.getTracks().map(t => t.id)); + + stream.addEventListener("addtrack", ({ track }) => { + ok( + !myTrackIds.has(track.id), + "Duplicate addtrack callback: " + + `stream id=${stream.id} track id=${track.id}` + ); + myTrackIds.add(track.id); + // addtrack events happen before track events, so the track callback hasn't + // heard about this yet. + let streams = this.remoteStreamsByTrackId.get(track.id); + ok( + !streams || !streams.has(stream.id), + `In addtrack for stream id=${stream.id}` + + `there should not have been a track event for track id=${track.id} ` + + " containing this stream yet." + ); + ok( + stream.getTracks().includes(track), + "In addtrack, stream id=" + + `${stream.id} should already contain track id=${track.id}` + ); + }); + + stream.addEventListener("removetrack", ({ track }) => { + ok( + myTrackIds.has(track.id), + "Duplicate removetrack callback: " + + `stream id=${stream.id} track id=${track.id}` + ); + myTrackIds.delete(track.id); + // Also remove the association from remoteStreamsByTrackId + const streams = this.remoteStreamsByTrackId.get(track.id); + ok( + streams, + `In removetrack for stream id=${stream.id}, track id=` + + `${track.id} should have had a track callback for the stream.` + ); + streams.delete(stream.id); + ok( + !stream.getTracks().includes(track), + "In removetrack, stream id=" + + `${stream.id} should not contain track id=${track.id}` + ); + }); + }, + + setupTrackEventHandler() { + this._pc.addEventListener("track", ({ track, streams }) => { + info(`${this}: 'ontrack' event fired for ${track.id}`); + ok(this.isTrackOnPC(track), `Found track ${track.id}`); + + let gratuitousEvent = true; + let streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id); + if (!streamsContainingTrack) { + gratuitousEvent = false; // Told us about a new track + this.remoteStreamsByTrackId.set(track.id, new Set()); + streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id); + } + + for (const stream of streams) { + ok( + stream.getTracks().includes(track), + `In track event, track id=${track.id}` + + ` should already be in stream id=${stream.id}` + ); + + if (!streamsContainingTrack.has(stream.id)) { + gratuitousEvent = false; // Told us about a new stream + streamsContainingTrack.add(stream.id); + this.setupStreamEventHandlers(stream); + } + } + + ok(!gratuitousEvent, "track event told us something new"); + + // So far, we've verified consistency between the current state of the + // streams, addtrack/removetrack events on the streams, and track events + // on the peerconnection. We have also verified that we have not gotten + // any gratuitous events. We have not done anything to verify that the + // current state of affairs matches what we were expecting it to. + + this.ensureMediaElement(track, "remote"); + }); + }, + + /** + * Either adds a given ICE candidate right away or stores it to be added + * later, depending on the state of the PeerConnection. + * + * @param {object} candidate + * The RTCIceCandidate to be added or stored + */ + storeOrAddIceCandidate(candidate) { + this._remote_ice_candidates.push(candidate); + if (this.signalingState === "closed") { + info("Received ICE candidate for closed PeerConnection - discarding"); + return; + } + this.holdIceCandidates + .then(() => { + info(this + ": adding ICE candidate " + JSON.stringify(candidate)); + return this._pc.addIceCandidate(candidate); + }) + .then(() => ok(true, this + " successfully added an ICE candidate")) + .catch(e => + // The onicecandidate callback runs independent of the test steps + // and therefore errors thrown from in there don't get caught by the + // race of the Promises around our test steps. + // Note: as long as we are queuing ICE candidates until the success + // of sRD() this should never ever happen. + ok(false, this + " adding ICE candidate failed with: " + e.message) + ); + }, + + /** + * Registers a callback for the ICE connection state change and + * appends the new state to an array for logging it later. + */ + logIceConnectionState() { + this.iceConnectionLog = [this._pc.iceConnectionState]; + this.ice_connection_callbacks.logIceStatus = () => { + var newstate = this._pc.iceConnectionState; + var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1]; + if (Object.keys(iceStateTransitions).includes(oldstate)) { + if (this.iceCheckingRestartExpected) { + is( + newstate, + "checking", + "iceconnectionstate event '" + + newstate + + "' matches expected state 'checking'" + ); + this.iceCheckingRestartExpected = false; + } else if (this.iceCheckingIceRollbackExpected) { + is( + newstate, + "connected", + "iceconnectionstate event '" + + newstate + + "' matches expected state 'connected'" + ); + this.iceCheckingIceRollbackExpected = false; + } else { + ok( + iceStateTransitions[oldstate].includes(newstate), + this + + ": legal ICE state transition from " + + oldstate + + " to " + + newstate + ); + } + } else { + ok( + false, + this + + ": old ICE state " + + oldstate + + " missing in ICE transition array" + ); + } + this.iceConnectionLog.push(newstate); + }; + }, + + /** + * Resets the ICE connected Promise and allows ICE connection state monitoring + * to go backwards to 'checking'. + */ + expectIceChecking() { + this.iceCheckingRestartExpected = true; + this.iceConnected = new Promise((resolve, reject) => { + this.iceConnectedResolve = resolve; + this.iceConnectedReject = reject; + }); + }, + + /** + * Waits for ICE to either connect or fail. + * + * @returns {Promise} + * resolves when connected, rejects on failure + */ + waitForIceConnected() { + return this.iceConnected; + }, + + /** + * Setup a onicecandidate handler + * + * @param {object} test + * A PeerConnectionTest object to which the ice candidates gets + * forwarded. + */ + setupIceCandidateHandler(test, candidateHandler) { + candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test); + + var resolveEndOfTrickle; + this.endOfTrickleIce = new Promise(r => (resolveEndOfTrickle = r)); + this.holdIceCandidates = new Promise(r => (this.releaseIceCandidates = r)); + + this._pc.onicecandidate = anEvent => { + if (!anEvent.candidate) { + this._pc.onicecandidate = () => + ok( + false, + this.label + " received ICE candidate after end of trickle" + ); + info(this.label + ": received end of trickle ICE event"); + ok( + this._pc.iceGatheringState === "complete", + "ICE gathering state has reached complete" + ); + resolveEndOfTrickle(this.label); + return; + } + + info( + this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate) + ); + ok(anEvent.candidate.sdpMid.length, "SDP mid not empty"); + ok( + anEvent.candidate.usernameFragment.length, + "usernameFragment not empty" + ); + + ok( + typeof anEvent.candidate.sdpMLineIndex === "number", + "SDP MLine Index needs to exist" + ); + this._local_ice_candidates.push(anEvent.candidate); + candidateHandler(this.label, anEvent.candidate); + }; + }, + + checkLocalMediaTracks() { + info( + `${this}: Checking local tracks ${JSON.stringify( + this.expectedLocalTrackInfo + )}` + ); + const sendersWithTrack = this._pc.getSenders().filter(({ track }) => track); + is( + sendersWithTrack.length, + this.expectedLocalTrackInfo.length, + "The number of senders with a track should be equal to the number of " + + "expected local tracks." + ); + + // expectedLocalTrackInfo is in the same order that the tracks were added, and + // so should the output of getSenders. + this.expectedLocalTrackInfo.forEach((info, i) => { + const sender = sendersWithTrack[i]; + is(sender, info.sender, `Sender ${i} should match`); + is(sender.track, info.track, `Track ${i} should match`); + }); + }, + + /** + * Checks that we are getting the media tracks we expect. + */ + checkMediaTracks() { + this.checkLocalMediaTracks(); + }, + + checkLocalMsids() { + const sdp = this.localDescription.sdp; + const msections = sdputils.getMSections(sdp); + const expectedStreamIdCounts = new Map(); + for (const { track, sender, streamId } of this.expectedLocalTrackInfo) { + const transceiver = this._pc + .getTransceivers() + .find(t => t.sender == sender); + ok(transceiver, "There should be a transceiver for each sender"); + if (transceiver.mid) { + const midFinder = new RegExp(`^a=mid:${transceiver.mid}$`, "m"); + const msection = msections.find(m => m.match(midFinder)); + ok( + msection, + `There should be a media section for mid = ${transceiver.mid}` + ); + ok( + msection.startsWith(`m=${track.kind}`), + `Media section should be of type ${track.kind}` + ); + const msidFinder = new RegExp(`^a=msid:${streamId} \\S+$`, "m"); + ok( + msection.match(msidFinder), + `Should find a=msid:${streamId} in media section` + + " (with any track id for now)" + ); + const count = expectedStreamIdCounts.get(streamId) || 0; + expectedStreamIdCounts.set(streamId, count + 1); + } + } + + // Check for any unexpected msids. + const allMsids = sdp.match(new RegExp("^a=msid:\\S+", "mg")); + if (!allMsids) { + return; + } + const allStreamIds = allMsids.map(msidAttr => + msidAttr.replace("a=msid:", "") + ); + allStreamIds.forEach(id => { + const count = expectedStreamIdCounts.get(id); + ok(count, `Unexpected stream id ${id} found in local description.`); + if (count) { + expectedStreamIdCounts.set(id, count - 1); + } + }); + }, + + /** + * Check that media flow is present for the given media element by checking + * that it reaches ready state HAVE_ENOUGH_DATA and progresses time further + * than the start of the check. + * + * This ensures, that the stream being played is producing + * data and, in case it contains a video track, that at least one video frame + * has been displayed. + * + * @param {HTMLMediaElement} track + * The media element to check + * @returns {Promise} + * A promise that resolves when media data is flowing. + */ + waitForMediaElementFlow(element) { + info("Checking data flow for element: " + element.id); + is( + element.ended, + !element.srcObject.active, + "Element ended should be the inverse of the MediaStream's active state" + ); + if (element.ended) { + is( + element.readyState, + element.HAVE_CURRENT_DATA, + "Element " + element.id + " is ended and should have had data" + ); + return Promise.resolve(); + } + + const haveEnoughData = ( + element.readyState == element.HAVE_ENOUGH_DATA + ? Promise.resolve() + : haveEvent( + element, + "canplay", + wait(60000, new Error("Timeout for element " + element.id)) + ) + ).then(_ => info("Element " + element.id + " has enough data.")); + + const startTime = element.currentTime; + const timeProgressed = timeout( + listenUntil(element, "timeupdate", _ => element.currentTime > startTime), + 60000, + "Element " + element.id + " should progress currentTime" + ).then(); + + return Promise.all([haveEnoughData, timeProgressed]); + }, + + /** + * Wait for RTP packet flow for the given MediaStreamTrack. + * + * @param {object} track + * A MediaStreamTrack to wait for data flow on. + * @returns {Promise} + * Returns a promise which yields a StatsReport object with RTP stats. + */ + async _waitForRtpFlow(target, rtpType) { + const { track } = target; + info(`_waitForRtpFlow(${track.id}, ${rtpType})`); + const packets = `packets${rtpType == "outbound-rtp" ? "Sent" : "Received"}`; + + const retryInterval = 500; // Time between stats checks + const timeout = 30000; // Timeout in ms + const retries = timeout / retryInterval; + + for (let i = 0; i < retries; i++) { + info(`Checking ${rtpType} for ${track.kind} track ${track.id} try ${i}`); + for (const rtp of (await target.getStats()).values()) { + if (rtp.type != rtpType) { + continue; + } + if (rtp.kind != track.kind) { + continue; + } + + const numPackets = rtp[packets]; + info(`Track ${track.id} has ${numPackets} ${packets}.`); + if (!numPackets) { + continue; + } + + ok(true, `RTP flowing for ${track.kind} track ${track.id}`); + return; + } + await wait(retryInterval); + } + throw new Error( + `Checking stats for track ${track.id} timed out after ${timeout} ms` + ); + }, + + /** + * Wait for inbound RTP packet flow for the given MediaStreamTrack. + * + * @param {object} receiver + * An RTCRtpReceiver to wait for data flow on. + * @returns {Promise} + * Returns a promise that resolves once data is flowing. + */ + async waitForInboundRtpFlow(receiver) { + return this._waitForRtpFlow(receiver, "inbound-rtp"); + }, + + /** + * Wait for outbound RTP packet flow for the given MediaStreamTrack. + * + * @param {object} sender + * An RTCRtpSender to wait for data flow on. + * @returns {Promise} + * Returns a promise that resolves once data is flowing. + */ + async waitForOutboundRtpFlow(sender) { + return this._waitForRtpFlow(sender, "outbound-rtp"); + }, + + getExpectedActiveReceivers() { + return this._pc + .getTransceivers() + .filter( + t => + !t.stopped && + t.currentDirection && + t.currentDirection != "inactive" && + t.currentDirection != "sendonly" + ) + .filter(({ receiver }) => receiver.track) + .map(({ mid, currentDirection, receiver }) => { + info( + `Found transceiver that should be receiving RTP: mid=${mid}` + + ` currentDirection=${currentDirection}` + + ` kind=${receiver.track.kind} track-id=${receiver.track.id}` + ); + return receiver; + }); + }, + + getExpectedSenders() { + return this._pc.getSenders().filter(({ track }) => track); + }, + + /** + * Wait for presence of video flow on all media elements and rtp flow on + * all sending and receiving track involved in this test. + * + * @returns {Promise} + * A promise that resolves when media flows for all elements and tracks + */ + waitForMediaFlow() { + const receivers = this.getExpectedActiveReceivers(); + return Promise.all([ + ...this.localMediaElements.map(el => this.waitForMediaElementFlow(el)), + ...this.remoteMediaElements + .filter(({ srcObject }) => + receivers.some(({ track }) => + srcObject.getTracks().some(t => t == track) + ) + ) + .map(el => this.waitForMediaElementFlow(el)), + ...receivers.map(receiver => this.waitForInboundRtpFlow(receiver)), + ...this.getExpectedSenders().map(sender => + this.waitForOutboundRtpFlow(sender) + ), + ]); + }, + + /** + * Check that correct audio (typically a flat tone) is flowing to this + * PeerConnection for each transceiver that should be receiving. Uses + * WebAudio AnalyserNodes to compare input and output audio data in the + * frequency domain. + * + * @param {object} from + * A PeerConnectionWrapper whose audio RTPSender we use as source for + * the audio flow check. + * @returns {Promise} + * A promise that resolves when we're receiving the tone/s from |from|. + */ + async checkReceivingToneFrom( + audiocontext, + from, + cancel = wait(60000, new Error("Tone not detected")) + ) { + let localTransceivers = this._pc + .getTransceivers() + .filter(t => t.mid) + .filter(t => t.receiver.track.kind == "audio") + .sort((t1, t2) => t1.mid < t2.mid); + let remoteTransceivers = from._pc + .getTransceivers() + .filter(t => t.mid) + .filter(t => t.receiver.track.kind == "audio") + .sort((t1, t2) => t1.mid < t2.mid); + + is( + localTransceivers.length, + remoteTransceivers.length, + "Same number of associated audio transceivers on remote and local." + ); + + for (let i = 0; i < localTransceivers.length; i++) { + is( + localTransceivers[i].mid, + remoteTransceivers[i].mid, + "Transceivers at index " + i + " have the same mid." + ); + + if (!remoteTransceivers[i].sender.track) { + continue; + } + + if ( + remoteTransceivers[i].currentDirection == "recvonly" || + remoteTransceivers[i].currentDirection == "inactive" + ) { + continue; + } + + let sendTrack = remoteTransceivers[i].sender.track; + let inputElem = from.getMediaElementForTrack(sendTrack, "local"); + ok( + inputElem, + "Remote wrapper should have a media element for track id " + + sendTrack.id + ); + let inputAudioStream = from.getStreamForSendTrack(sendTrack); + ok( + inputAudioStream, + "Remote wrapper should have a stream for track id " + sendTrack.id + ); + let inputAnalyser = new AudioStreamAnalyser( + audiocontext, + inputAudioStream + ); + + let recvTrack = localTransceivers[i].receiver.track; + let outputAudioStream = this.getStreamForRecvTrack(recvTrack); + ok( + outputAudioStream, + "Local wrapper should have a stream for track id " + recvTrack.id + ); + let outputAnalyser = new AudioStreamAnalyser( + audiocontext, + outputAudioStream + ); + + let error = null; + cancel.then(e => (error = e)); + + let indexOfMax = data => + data.reduce((max, val, i) => (val >= data[max] ? i : max), 0); + + await outputAnalyser.waitForAnalysisSuccess(() => { + if (error) { + throw error; + } + + let inputData = inputAnalyser.getByteFrequencyData(); + let outputData = outputAnalyser.getByteFrequencyData(); + + let inputMax = indexOfMax(inputData); + let outputMax = indexOfMax(outputData); + info( + `Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},` + + ` output[${outputMax}] = ${outputData[outputMax]}` + ); + if (!inputData[inputMax] || !outputData[outputMax]) { + return false; + } + + // When the input and output maxima are within reasonable distance (2% of + // total length, which means ~10 for length 512) from each other, we can + // be sure that the input tone has made it through the peer connection. + info(`input data length: ${inputData.length}`); + return Math.abs(inputMax - outputMax) < inputData.length * 0.02; + }); + } + }, + + /** + * Check that stats are present by checking for known stats. + */ + async getStats(selector) { + const stats = await this._pc.getStats(selector); + const dict = {}; + for (const [k, v] of stats.entries()) { + dict[k] = v; + } + info(`${this}: Got stats: ${JSON.stringify(dict)}`); + return stats; + }, + + /** + * Checks that we are getting the media streams we expect. + * + * @param {object} stats + * The stats to check from this PeerConnectionWrapper + */ + checkStats(stats) { + const isRemote = ({ type }) => + ["remote-outbound-rtp", "remote-inbound-rtp"].includes(type); + var counters = {}; + for (let [key, res] of stats) { + info("Checking stats for " + key + " : " + res); + // validate stats + ok(res.id == key, "Coherent stats id"); + const now = performance.timeOrigin + performance.now(); + const minimum = performance.timeOrigin; + const type = isRemote(res) ? "rtcp" : "rtp"; + ok( + res.timestamp >= minimum, + `Valid ${type} timestamp ${res.timestamp} >= ${minimum} ( + ${res.timestamp - minimum} ms)` + ); + ok( + res.timestamp <= now, + `Valid ${type} timestamp ${res.timestamp} <= ${now} ( + ${res.timestamp - now} ms)` + ); + if (isRemote(res)) { + continue; + } + counters[res.type] = (counters[res.type] || 0) + 1; + + switch (res.type) { + case "inbound-rtp": + case "outbound-rtp": + { + // Inbound tracks won't have an ssrc if RTP is not flowing. + // (eg; negotiated inactive) + ok( + res.ssrc || res.type == "inbound-rtp", + "Outbound RTP stats has an ssrc." + ); + + if (res.ssrc) { + // ssrc is a 32 bit number returned as an unsigned long + ok(!/[^0-9]/.test(`${res.ssrc}`), "SSRC is numeric"); + ok(parseInt(res.ssrc) < Math.pow(2, 32), "SSRC is within limits"); + } + + if (res.type == "outbound-rtp") { + ok(res.packetsSent !== undefined, "Rtp packetsSent"); + // We assume minimum payload to be 1 byte (guess from RFC 3550) + ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent"); + } else { + ok(res.packetsReceived !== undefined, "Rtp packetsReceived"); + ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived"); + } + if (res.remoteId) { + var rem = stats.get(res.remoteId); + ok(isRemote(rem), "Remote is rtcp"); + ok(rem.localId == res.id, "Remote backlink match"); + if (res.type == "outbound-rtp") { + ok(rem.type == "remote-inbound-rtp", "Rtcp is inbound"); + if (rem.packetsLost) { + ok( + rem.packetsLost >= 0, + "Rtcp packetsLost " + rem.packetsLost + " >= 0" + ); + ok( + rem.packetsLost < 1000, + "Rtcp packetsLost " + rem.packetsLost + " < 1000" + ); + } + if (!this.disableRtpCountChecking) { + // no guarantee which one is newer! + // Note: this must change when we add a timestamp field to remote RTCP reports + // and make rem.timestamp be the reception time + if (res.timestamp < rem.timestamp) { + info( + "REVERSED timestamps: rec:" + + rem.packetsReceived + + " time:" + + rem.timestamp + + " sent:" + + res.packetsSent + + " time:" + + res.timestamp + ); + } + } + if (rem.jitter) { + ok(rem.jitter >= 0, "Rtcp jitter " + rem.jitter + " >= 0"); + ok(rem.jitter < 5, "Rtcp jitter " + rem.jitter + " < 5 sec"); + } + if (rem.roundTripTime) { + ok( + rem.roundTripTime >= 0, + "Rtcp rtt " + rem.roundTripTime + " >= 0" + ); + ok( + rem.roundTripTime < 60, + "Rtcp rtt " + rem.roundTripTime + " < 1 min" + ); + } + } else { + ok(rem.type == "remote-outbound-rtp", "Rtcp is outbound"); + ok(rem.packetsSent !== undefined, "Rtcp packetsSent"); + ok(rem.bytesSent !== undefined, "Rtcp bytesSent"); + } + ok(rem.ssrc == res.ssrc, "Remote ssrc match"); + } else { + info("No rtcp info received yet"); + } + } + break; + } + } + + var nin = this._pc.getTransceivers().filter(t => { + return ( + !t.stopped && + t.currentDirection != "inactive" && + t.currentDirection != "sendonly" + ); + }).length; + const nout = Object.keys(this.expectedLocalTrackInfo).length; + var ndata = this.dataChannels.length; + + // TODO(Bug 957145): Restore stronger inbound-rtp test once Bug 948249 is fixed + //is((counters["inbound-rtp"] || 0), nin, "Have " + nin + " inbound-rtp stat(s)"); + ok( + (counters["inbound-rtp"] || 0) >= nin, + "Have at least " + nin + " inbound-rtp stat(s) *" + ); + + is( + counters["outbound-rtp"] || 0, + nout, + "Have " + nout + " outbound-rtp stat(s)" + ); + + var numLocalCandidates = counters["local-candidate"] || 0; + var numRemoteCandidates = counters["remote-candidate"] || 0; + // If there are no tracks, there will be no stats either. + if (nin + nout + ndata > 0) { + ok(numLocalCandidates, "Have local-candidate stat(s)"); + ok(numRemoteCandidates, "Have remote-candidate stat(s)"); + } else { + is(numLocalCandidates, 0, "Have no local-candidate stats"); + is(numRemoteCandidates, 0, "Have no remote-candidate stats"); + } + }, + + /** + * Compares the Ice server configured for this PeerConnectionWrapper + * with the ICE candidates received in the RTCP stats. + * + * @param {object} stats + * The stats to be verified for relayed vs. direct connection. + */ + checkStatsIceConnectionType(stats, expectedLocalCandidateType) { + let lId; + let rId; + for (let stat of stats.values()) { + if (stat.type == "candidate-pair" && stat.selected) { + lId = stat.localCandidateId; + rId = stat.remoteCandidateId; + break; + } + } + isnot( + lId, + undefined, + "Got local candidate ID " + lId + " for selected pair" + ); + isnot( + rId, + undefined, + "Got remote candidate ID " + rId + " for selected pair" + ); + let lCand = stats.get(lId); + let rCand = stats.get(rId); + if (!lCand || !rCand) { + ok( + false, + "failed to find candidatepair IDs or stats for local: " + + lId + + " remote: " + + rId + ); + return; + } + + info( + "checkStatsIceConnectionType verifying: local=" + + JSON.stringify(lCand) + + " remote=" + + JSON.stringify(rCand) + ); + expectedLocalCandidateType = expectedLocalCandidateType || "host"; + var candidateType = lCand.candidateType; + if (lCand.relayProtocol === "tcp" && candidateType === "relay") { + candidateType = "relay-tcp"; + } + + if (lCand.relayProtocol === "tls" && candidateType === "relay") { + candidateType = "relay-tls"; + } + + if (expectedLocalCandidateType === "srflx" && candidateType === "prflx") { + // Be forgiving of prflx when expecting srflx, since that can happen due + // to timing. + candidateType = "srflx"; + } + + is( + candidateType, + expectedLocalCandidateType, + "Local candidate type is what we expected for selected pair" + ); + }, + + /** + * Compares amount of established ICE connection according to ICE candidate + * pairs in the stats reporting with the expected amount of connection based + * on the constraints. + * + * @param {object} stats + * The stats to check for ICE candidate pairs + * @param {object} testOptions + * The test options object from the PeerConnectionTest + */ + checkStatsIceConnections(stats, testOptions) { + var numIceConnections = 0; + stats.forEach(stat => { + if (stat.type === "candidate-pair" && stat.selected) { + numIceConnections += 1; + } + }); + info("ICE connections according to stats: " + numIceConnections); + isnot( + numIceConnections, + 0, + "Number of ICE connections according to stats is not zero" + ); + if (testOptions.bundle) { + if (testOptions.rtcpmux) { + is(numIceConnections, 1, "stats reports exactly 1 ICE connection"); + } else { + is( + numIceConnections, + 2, + "stats report exactly 2 ICE connections for media and RTCP" + ); + } + } else { + var numAudioTransceivers = this._pc + .getTransceivers() + .filter(transceiver => { + return ( + !transceiver.stopped && transceiver.receiver.track.kind == "audio" + ); + }).length; + + var numVideoTransceivers = this._pc + .getTransceivers() + .filter(transceiver => { + return ( + !transceiver.stopped && transceiver.receiver.track.kind == "video" + ); + }).length; + + var numExpectedTransports = numAudioTransceivers + numVideoTransceivers; + if (!testOptions.rtcpmux) { + numExpectedTransports *= 2; + } + + if (this.dataChannels.length) { + ++numExpectedTransports; + } + + info( + "expected audio + video + data transports: " + numExpectedTransports + ); + is( + numIceConnections, + numExpectedTransports, + "stats ICE connections matches expected A/V transports" + ); + } + }, + + expectNegotiationNeeded() { + if (!this.observedNegotiationNeeded) { + this.observedNegotiationNeeded = new Promise(resolve => { + this.onnegotiationneeded = resolve; + }); + } + }, + + /** + * Property-matching function for finding a certain stat in passed-in stats + * + * @param {object} stats + * The stats to check from this PeerConnectionWrapper + * @param {object} props + * The properties to look for + * @returns {boolean} Whether an entry containing all match-props was found. + */ + hasStat(stats, props) { + for (let res of stats.values()) { + var match = true; + for (let prop in props) { + if (res[prop] !== props[prop]) { + match = false; + break; + } + } + if (match) { + return true; + } + } + return false; + }, + + /** + * Closes the connection + */ + close() { + this._pc.close(); + this.localMediaElements.forEach(e => e.pause()); + info(this + ": Closed connection."); + }, + + /** + * Returns the string representation of the class + * + * @returns {String} The string representation + */ + toString() { + return "PeerConnectionWrapper (" + this.label + ")"; + }, +}; + +// haxx to prevent SimpleTest from failing at window.onload +function addLoadEvent() {} + +function loadScript(...scripts) { + return Promise.all( + scripts.map(script => { + var el = document.createElement("script"); + if (typeof scriptRelativePath === "string" && script.charAt(0) !== "/") { + script = scriptRelativePath + script; + } + el.src = script; + document.head.appendChild(el); + return new Promise(r => { + el.onload = r; + el.onerror = r; + }); + }) + ); +} + +// Ensure SimpleTest.js is loaded before other scripts. +/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */ +/* import-globals-from head.js */ +/* import-globals-from templates.js */ +/* import-globals-from turnConfig.js */ +/* import-globals-from dataChannel.js */ +/* import-globals-from network.js */ +/* import-globals-from sdpUtils.js */ + +var scriptsReady = loadScript("/tests/SimpleTest/SimpleTest.js").then(() => { + return loadScript( + "head.js", + "templates.js", + "turnConfig.js", + "dataChannel.js", + "network.js", + "sdpUtils.js" + ); +}); + +function createHTML(options) { + return scriptsReady.then(() => realCreateHTML(options)); +} + +var iceServerWebsocket; +var iceServersArray = []; + +var addTurnsSelfsignedCerts = () => { + var gUrl = SimpleTest.getTestFileURL("addTurnsSelfsignedCert.js"); + var gScript = SpecialPowers.loadChromeScript(gUrl); + var certs = []; + // If the ICE server is running TURNS, and includes a "cert" attribute in + // its JSON, we set up an override that will forgive things like + // self-signed for it. + iceServersArray.forEach(iceServer => { + if (iceServer.hasOwnProperty("cert")) { + iceServer.urls.forEach(url => { + if (url.startsWith("turns:")) { + // Assumes no port or params! + certs.push({ cert: iceServer.cert, hostname: url.substr(6) }); + } + }); + } + }); + + return new Promise((resolve, reject) => { + gScript.addMessageListener("certs-added", () => { + resolve(); + }); + + gScript.sendAsyncMessage("add-turns-certs", certs); + }); +}; + +var setupIceServerConfig = useIceServer => { + // We disable ICE support for HTTP proxy when using a TURN server, because + // mochitest uses a fake HTTP proxy to serve content, which will eat our STUN + // packets for TURN TCP. + var enableHttpProxy = enable => + SpecialPowers.pushPrefEnv({ + set: [["media.peerconnection.disable_http_proxy", !enable]], + }); + + var spawnIceServer = () => + new Promise((resolve, reject) => { + iceServerWebsocket = new WebSocket("ws://localhost:8191/"); + iceServerWebsocket.onopen = event => { + info("websocket/process bridge open, starting ICE Server..."); + iceServerWebsocket.send("iceserver"); + }; + + iceServerWebsocket.onmessage = event => { + // The first message will contain the iceServers configuration, subsequent + // messages are just logging. + info("ICE Server: " + event.data); + resolve(event.data); + }; + + iceServerWebsocket.onerror = () => { + reject("ICE Server error: Is the ICE server websocket up?"); + }; + + iceServerWebsocket.onclose = () => { + info("ICE Server websocket closed"); + reject("ICE Server gone before getting configuration"); + }; + }); + + if (!useIceServer) { + info("Skipping ICE Server for this test"); + return enableHttpProxy(true); + } + + return enableHttpProxy(false) + .then(spawnIceServer) + .then(iceServersStr => { + iceServersArray = JSON.parse(iceServersStr); + }) + .then(addTurnsSelfsignedCerts); +}; + +async function runNetworkTest(testFunction, fixtureOptions = {}) { + let { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + await scriptsReady; + await runTestWhenReady(async options => { + await startNetworkAndTest(); + await setupIceServerConfig(fixtureOptions.useIceServer); + await testFunction(options); + await networkTestFinished(); + }); +} diff --git a/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js new file mode 100644 index 0000000000..d0c647be0d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js @@ -0,0 +1,32 @@ +// This function takes a sample-rate, and tests that audio flows correctly when +// the sampling-rate at which the MTG runs is not one of the sampling-rates that +// the MediaPipeline can work with. +// It is in a separate file because we have an MTG per document, and we want to +// test multiple sample-rates, so we include it in multiple HTML mochitest +// files. +async function test_peerconnection_audio_forced_sample_rate(forcedSampleRate) { + await scriptsReady; + await pushPrefs(["media.cubeb.force_sample_rate", forcedSampleRate]); + await runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + const ac = new AudioContext(); + test.setMediaConstraints([{ audio: true }], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_WEBAUDIO_SOURCE(test) { + const oscillator = ac.createOscillator(); + oscillator.type = "sine"; + oscillator.frequency.value = 700; + oscillator.start(); + const dest = ac.createMediaStreamDestination(); + oscillator.connect(dest); + test.pcLocal.attachLocalStream(dest.stream); + }, + ]); + test.chain.append([ + function CHECK_REMOTE_AUDIO_FLOW(test) { + return test.pcRemote.checkReceivingToneFrom(ac, test.pcLocal); + }, + ]); + return test.run(); + }); +} diff --git a/dom/media/webrtc/tests/mochitests/sdpUtils.js b/dom/media/webrtc/tests/mochitests/sdpUtils.js new file mode 100644 index 0000000000..51cae10dba --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/sdpUtils.js @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var sdputils = { + // Finds the codec id / payload type given a codec format + // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of + // multiple matches. + findCodecId(sdp, format, offset = 0) { + let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi"); + let match; + for (let i = 0; i <= offset; ++i) { + match = regex.exec(sdp); + if (!match) { + throw new Error( + "Couldn't find offset " + + i + + " of codec " + + format + + " while looking for offset " + + offset + + " in sdp:\n" + + sdp + ); + } + } + // match[0] is the full matched string + // match[1] is the first parenthesis group + return match[1]; + }, + + // Returns a list of all payload types, excluding rtx, in an sdp. + getPayloadTypes(sdp) { + const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim; + const pts = []; + for (const [line, pt] of sdp.matchAll(regex)) { + pts.push(pt); + } + return pts; + }, + + // Finds all the extmap ids in the given sdp. Note that this does NOT + // consider m-sections, so a more generic version would need to + // look at each m-section separately. + findExtmapIds(sdp) { + var sdpExtmapIds = []; + extmapRegEx = /^a=extmap:([0-9+])/gm; + // must call exec on the regex to get each match in the string + while ((searchResults = extmapRegEx.exec(sdp)) !== null) { + // returned array has the matched text as the first item, + // and then one item for each capturing parenthesis that + // matched containing the text that was captured. + sdpExtmapIds.push(searchResults[1]); + } + return sdpExtmapIds; + }, + + findExtmapIdsUrnsDirections(sdp) { + var sdpExtmap = []; + extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:\-\/\.]+)/gm; + // must call exec on the regex to get each match in the string + while ((searchResults = extmapRegEx.exec(sdp)) !== null) { + // returned array has the matched text as the first item, + // and then one item for each capturing parenthesis that + // matched containing the text that was captured. + var idUrn = []; + idUrn.push(searchResults[1]); + idUrn.push(searchResults[3]); + idUrn.push(searchResults[2].slice(1)); + sdpExtmap.push(idUrn); + } + return sdpExtmap; + }, + + verify_unique_extmap_ids(sdp) { + const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp); + + return sdpExtmapIds.reduce(function (result, item, index) { + const [id, urn, dir] = item; + ok( + !(id in result) || (result[id][0] === urn && result[id][1] === dir), + "ID " + id + " is unique ID for " + urn + " and direction " + dir + ); + result[id] = [urn, dir]; + return result; + }, {}); + }, + + getMSections(sdp) { + return sdp + .split(new RegExp("^m=", "gm")) + .slice(1) + .map(s => "m=" + s); + }, + + getAudioMSections(sdp) { + return this.getMSections(sdp).filter(section => + section.startsWith("m=audio") + ); + }, + + getVideoMSections(sdp) { + return this.getMSections(sdp).filter(section => + section.startsWith("m=video") + ); + }, + + checkSdpAfterEndOfTrickle(description, testOptions, label) { + info("EOC-SDP: " + JSON.stringify(description)); + + const checkForTransportAttributes = msection => { + info("Checking msection: " + msection); + ok( + msection.includes("a=end-of-candidates"), + label + ": SDP contains end-of-candidates" + ); + + if (!msection.startsWith("m=application")) { + if (testOptions.rtcpmux) { + ok( + msection.includes("a=rtcp-mux"), + label + ": SDP contains rtcp-mux" + ); + } else { + ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port"); + } + } + }; + + const hasOwnTransport = msection => { + const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection); + if (port0Check) { + return false; + } + const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection); + if (!midMatch) { + return true; + } + const mid = midMatch[1]; + const bundleGroupMatch = new RegExp( + "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+" + ).exec(description.sdp); + return bundleGroupMatch == null; + }; + + const msectionsWithOwnTransports = this.getMSections( + description.sdp + ).filter(hasOwnTransport); + + ok( + msectionsWithOwnTransports.length, + "SDP should contain at least one msection with a transport" + ); + msectionsWithOwnTransports.forEach(checkForTransportAttributes); + + if (testOptions.ssrc) { + ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc"); + } else { + ok( + !description.sdp.includes("a=ssrc"), + label + ": SDP does not contain a=ssrc" + ); + } + }, + + // Note, we don't bother removing the fmtp lines, which makes a good test + // for some SDP parsing issues. + removeCodec(sdp, codec) { + var updated_sdp = sdp.replace( + new RegExp("a=rtpmap:" + codec + ".*\\/90000\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""), + "$1$3" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""), + "" + ); + return updated_sdp; + }, + + removeAllButPayloadType(sdp, pt) { + return sdp.replace( + new RegExp("m=(\\w+ \\w+) UDP/TLS/RTP/SAVPF .*" + pt + ".*\\r\\n", "gi"), + "m=$1 UDP/TLS/RTP/SAVPF " + pt + "\r\n" + ); + }, + + removeRtpMapForPayloadType(sdp, pt) { + return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), ""); + }, + + removeRtcpMux(sdp) { + return sdp.replace(/a=rtcp-mux\r\n/g, ""); + }, + + removeSSRCs(sdp) { + return sdp.replace(/a=ssrc.*\r\n/g, ""); + }, + + removeBundle(sdp) { + return sdp.replace(/a=group:BUNDLE .*\r\n/g, ""); + }, + + reduceAudioMLineToPcmuPcma(sdp) { + return sdp.replace( + /m=audio .*\r\n/g, + "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n" + ); + }, + + setAllMsectionsInactive(sdp) { + return sdp + .replace(/\r\na=sendrecv/g, "\r\na=inactive") + .replace(/\r\na=sendonly/g, "\r\na=inactive") + .replace(/\r\na=recvonly/g, "\r\na=inactive"); + }, + + removeAllRtpMaps(sdp) { + return sdp.replace(/a=rtpmap:.*\r\n/g, ""); + }, + + reduceAudioMLineToDynamicPtAndOpus(sdp) { + return sdp.replace( + /m=audio .*\r\n/g, + "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n" + ); + }, + + addTiasBps(sdp, bps) { + return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n"); + }, + + removeSimulcastProperties(sdp) { + return sdp + .replace(/a=simulcast:.*\r\n/g, "") + .replace(/a=rid:.*\r\n/g, "") + .replace( + /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g, + "" + ) + .replace( + /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g, + "" + ); + }, + + transferSimulcastProperties(offer_sdp, answer_sdp) { + if (!offer_sdp.includes("a=simulcast:")) { + return answer_sdp; + } + ok( + offer_sdp.includes("a=simulcast:send "), + "Offer contains simulcast attribute" + ); + var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i); + var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n"; + ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute"); + var o_rids = offer_sdp.match(/a=rid:(.*)/gi); + o_rids.forEach(o_rid => { + new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n"; + }); + var extmap_id = offer_sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id" + ); + ok(extmap_id != null, "Offer contains RID RTP header extension"); + new_answer_sdp = + new_answer_sdp + + "a=extmap:" + + extmap_id[1] + + "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n"; + var extmap_id = offer_sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" + ); + if (extmap_id != null) { + new_answer_sdp = + new_answer_sdp + + "a=extmap:" + + extmap_id[1] + + "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n"; + } + + return new_answer_sdp; + }, + + verifySdp( + desc, + expectedType, + offerConstraintsList, + offerOptions, + testOptions + ) { + info("Examining this SessionDescription: " + JSON.stringify(desc)); + info("offerConstraintsList: " + JSON.stringify(offerConstraintsList)); + info("offerOptions: " + JSON.stringify(offerOptions)); + ok(desc, "SessionDescription is not null"); + is(desc.type, expectedType, "SessionDescription type is " + expectedType); + ok(desc.sdp.length > 10, "SessionDescription body length is plausible"); + ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP"); + ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP"); + ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP"); + //TODO: update this for loopback support bug 1027350 + ok( + !desc.sdp.includes(LOOPBACK_ADDR), + "loopback interface is absent from SDP" + ); + var requiresTrickleIce = !desc.sdp.includes("a=candidate"); + if (requiresTrickleIce) { + info("No ICE candidate in SDP -> requiring trickle ICE"); + } else { + info("at least one ICE candidate is present in SDP"); + } + + //TODO: how can we check for absence/presence of m=application? + + var audioTracks = + sdputils.countTracksInConstraint("audio", offerConstraintsList) || + (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0); + + info("expected audio tracks: " + audioTracks); + if (audioTracks == 0) { + ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP"); + } else { + ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP"); + is( + testOptions.opus, + desc.sdp.includes("a=rtpmap:109 opus/48000/2"), + "OPUS codec is present in SDP" + ); + //TODO: ideally the rtcp-mux should be for the m=audio, and not just + // anywhere in the SDP (JS SDP parser bug 1045429) + is( + testOptions.rtcpmux, + desc.sdp.includes("a=rtcp-mux"), + "RTCP Mux is offered in SDP" + ); + } + + var videoTracks = + sdputils.countTracksInConstraint("video", offerConstraintsList) || + (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0); + + info("expected video tracks: " + videoTracks); + if (videoTracks == 0) { + ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP"); + } else { + ok(desc.sdp.includes("m=video"), "video m-line is present in SDP"); + if (testOptions.h264) { + ok( + desc.sdp.includes("a=rtpmap:126 H264/90000") || + desc.sdp.includes("a=rtpmap:97 H264/90000"), + "H.264 codec is present in SDP" + ); + } else { + ok( + desc.sdp.includes("a=rtpmap:120 VP8/90000") || + desc.sdp.includes("a=rtpmap:121 VP9/90000"), + "VP8 or VP9 codec is present in SDP" + ); + } + is( + testOptions.rtcpmux, + desc.sdp.includes("a=rtcp-mux"), + "RTCP Mux is offered in SDP" + ); + is( + testOptions.ssrc, + desc.sdp.includes("a=ssrc"), + "a=ssrc signaled in SDP" + ); + } + + return requiresTrickleIce; + }, + + /** + * Counts the amount of audio tracks in a given media constraint. + * + * @param constraints + * The contraint to be examined. + */ + countTracksInConstraint(type, constraints) { + if (!Array.isArray(constraints)) { + return 0; + } + return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0); + }, +}; diff --git a/dom/media/webrtc/tests/mochitests/simulcast.js b/dom/media/webrtc/tests/mochitests/simulcast.js new file mode 100644 index 0000000000..2662c5dbca --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/simulcast.js @@ -0,0 +1,233 @@ +"use strict"; +/* Helper functions to munge SDP and split the sending track into + * separate tracks on the receiving end. This can be done in a number + * of ways, the one used here uses the fact that the MID and RID header + * extensions which are used for packet routing share the same wire + * format. The receiver interprets the rids from the sender as mids + * which allows receiving the different spatial resolutions on separate + * m-lines and tracks. + */ + +// Borrowed from wpt, with some dependencies removed. + +const ridExtensions = [ + "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", +]; + +function ridToMid(description, rids) { + const sections = SDPUtils.splitSections(description.sdp); + const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); + const ice = SDPUtils.getIceParameters(sections[1], sections[0]); + const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); + const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; + const directionValue = + description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || + "a=sendrecv"; + const mline = SDPUtils.parseMLine(sections[1]); + + // Skip mid extension; we are replacing it with the rid extmap + rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( + ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid" + ); + + for (const ext of rtpParameters.headerExtensions) { + if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") { + ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid"; + } + } + + // Filter rtx as we have no way to (re)interpret rrid. + // Not doing this makes probing use RTX, it's not understood and ramp-up is slower. + rtpParameters.codecs = rtpParameters.codecs.filter( + c => c.name.toUpperCase() !== "RTX" + ); + + if (!rids) { + rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map( + r => r[1] + ); + } + + let sdp = + SDPUtils.writeSessionBoilerplate() + + SDPUtils.writeDtlsParameters(dtls, setupValue) + + SDPUtils.writeIceParameters(ice) + + "a=group:BUNDLE " + + rids.join(" ") + + "\r\n"; + const baseRtpDescription = SDPUtils.writeRtpDescription( + mline.kind, + rtpParameters + ); + for (const rid of rids) { + sdp += + baseRtpDescription + + "a=mid:" + + rid + + "\r\n" + + "a=msid:rid-" + + rid + + " rid-" + + rid + + "\r\n"; + sdp += directionValue + "\r\n"; + } + return sdp; +} + +function midToRid(description, localDescription, rids) { + const sections = SDPUtils.splitSections(description.sdp); + const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); + const ice = SDPUtils.getIceParameters(sections[1], sections[0]); + const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); + const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; + const directionValue = + description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || + "a=sendrecv"; + const mline = SDPUtils.parseMLine(sections[1]); + + // Skip rid extensions; we are replacing them with the mid extmap + rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( + ext => !ridExtensions.includes(ext.uri) + ); + + for (const ext of rtpParameters.headerExtensions) { + if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") { + ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; + } + } + + const localMid = localDescription + ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) + : "0"; + + if (!rids) { + rids = []; + for (let i = 1; i < sections.length; i++) { + rids.push(SDPUtils.getMid(sections[i])); + } + } + + let sdp = + SDPUtils.writeSessionBoilerplate() + + SDPUtils.writeDtlsParameters(dtls, setupValue) + + SDPUtils.writeIceParameters(ice) + + "a=group:BUNDLE " + + localMid + + "\r\n"; + sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters); + // Although we are converting mids to rids, we still need a mid. + // The first one will be consistent with trickle ICE candidates. + sdp += "a=mid:" + localMid + "\r\n"; + sdp += directionValue + "\r\n"; + + for (const rid of rids) { + const stringrid = String(rid); // allow integers + const choices = stringrid.split(","); + choices.forEach(choice => { + sdp += "a=rid:" + choice + " recv\r\n"; + }); + } + if (rids.length) { + sdp += "a=simulcast:recv " + rids.join(";") + "\r\n"; + } + + return sdp; +} + +async function doOfferToSendSimulcast(offerer, answerer) { + await offerer.setLocalDescription(); + + // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids, + // even if some rids have been removed or reordered. + let mids = []; + if (answerer.localDescription) { + // Renegotiation. Mids must be the same as before, because renegotiation + // can never remove or reorder mids, nor can it expand the simulcast + // envelope. + mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( + e => e[1] + ); + } else { + // First negotiation; the mids will be exactly the same as the rids + const simulcastAttr = offerer.localDescription.sdp.match( + /a=simulcast:send (.*)/ + ); + if (simulcastAttr) { + mids = simulcastAttr[1].split(";"); + } + } + + const nonSimulcastOffer = ridToMid(offerer.localDescription, mids); + await answerer.setRemoteDescription({ + type: "offer", + sdp: nonSimulcastOffer, + }); +} + +async function doAnswerToRecvSimulcast(offerer, answerer, rids) { + await answerer.setLocalDescription(); + const simulcastAnswer = midToRid( + answerer.localDescription, + offerer.localDescription, + rids + ); + await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer }); +} + +async function doOfferToRecvSimulcast(offerer, answerer, rids) { + await offerer.setLocalDescription(); + const simulcastOffer = midToRid( + offerer.localDescription, + answerer.localDescription, + rids + ); + await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer }); +} + +async function doAnswerToSendSimulcast(offerer, answerer) { + await answerer.setLocalDescription(); + + // See which mids the offerer had; it will barf if we remove or reorder them + const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( + e => e[1] + ); + + const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); + await offerer.setRemoteDescription({ + type: "answer", + sdp: nonSimulcastAnswer, + }); +} + +async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) { + await doOfferToSendSimulcast(offerer, answerer); + await doAnswerToRecvSimulcast(offerer, answerer, rids); +} + +async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) { + await doOfferToRecvSimulcast(offerer, answerer, rids); + await doAnswerToSendSimulcast(offerer, answerer); +} + +// This would be useful for cases other than simulcast, but we do not use it +// anywhere else right now, nor do we have a place for wpt-friendly helpers at +// the moment. +function createPlaybackElement(track) { + const elem = document.createElement(track.kind); + elem.autoplay = true; + elem.srcObject = new MediaStream([track]); + elem.id = track.id; + document.body.appendChild(elem); + return elem; +} + +async function getPlaybackWithLoadedMetadata(track) { + const elem = createPlaybackElement(track); + return new Promise(resolve => { + elem.addEventListener("loadedmetadata", () => { + resolve(elem); + }); + }); +} diff --git a/dom/media/webrtc/tests/mochitests/stats.js b/dom/media/webrtc/tests/mochitests/stats.js new file mode 100644 index 0000000000..bbf62d7d14 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/stats.js @@ -0,0 +1,1647 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const statsExpectedByType = { + "inbound-rtp": { + expected: [ + "trackIdentifier", + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "codecId", + "packetsReceived", + "packetsLost", + "packetsDiscarded", + "bytesReceived", + "jitter", + "lastPacketReceivedTimestamp", + "headerBytesReceived", + // Always missing from libwebrtc stats + // "estimatedPlayoutTimestamp", + "jitterBufferDelay", + "jitterBufferEmittedCount", + ], + optional: ["remoteId", "nackCount", "qpSum"], + localVideoOnly: [ + "firCount", + "pliCount", + "framesDecoded", + "framesDropped", + "discardedPackets", + "framesPerSecond", + "frameWidth", + "frameHeight", + "framesReceived", + "totalDecodeTime", + "totalInterFrameDelay", + "totalProcessingDelay", + "totalSquaredInterFrameDelay", + ], + localAudioOnly: [ + "totalSamplesReceived", + // libwebrtc doesn't seem to do FEC for video + "fecPacketsReceived", + "fecPacketsDiscarded", + "concealedSamples", + "silentConcealedSamples", + "concealmentEvents", + "insertedSamplesForDeceleration", + "removedSamplesForAcceleration", + "audioLevel", + "totalAudioEnergy", + "totalSamplesDuration", + ], + unimplemented: [ + "mediaTrackId", + "transportId", + "associateStatsId", + "sliCount", + "packetsRepaired", + "fractionLost", + "burstPacketsLost", + "burstLossCount", + "burstDiscardCount", + "gapDiscardRate", + "gapLossRate", + ], + deprecated: ["mozRtt", "isRemote"], + }, + "outbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "codecId", + "packetsSent", + "bytesSent", + "remoteId", + "headerBytesSent", + "retransmittedPacketsSent", + "retransmittedBytesSent", + ], + optional: ["nackCount", "qpSum"], + localAudioOnly: [], + localVideoOnly: [ + "framesEncoded", + "firCount", + "pliCount", + "frameWidth", + "frameHeight", + "framesPerSecond", + "framesSent", + "hugeFramesSent", + "totalEncodeTime", + "totalEncodedBytesTarget", + ], + unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"], + deprecated: ["isRemote"], + }, + "remote-inbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "codecId", + "packetsLost", + "jitter", + "localId", + "totalRoundTripTime", + "fractionLost", + "roundTripTimeMeasurements", + ], + optional: ["roundTripTime", "nackCount", "packetsReceived"], + unimplemented: [ + "mediaTrackId", + "transportId", + "packetsDiscarded", + "associateStatsId", + "sliCount", + "packetsRepaired", + "burstPacketsLost", + "burstLossCount", + "burstDiscardCount", + "gapDiscardRate", + "gapLossRate", + ], + deprecated: ["mozRtt", "isRemote"], + }, + "remote-outbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "codecId", + "packetsSent", + "bytesSent", + "localId", + "remoteTimestamp", + ], + optional: ["nackCount"], + unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"], + deprecated: ["isRemote"], + }, + "media-source": { + expected: ["id", "timestamp", "type", "trackIdentifier", "kind"], + unimplemented: [ + "audioLevel", + "totalAudioEnergy", + "totalSamplesDuration", + "echoReturnLoss", + "echoReturnLossEnhancement", + "droppedSamplesDuration", + "droppedSamplesEvents", + "totalCaptureDelay", + "totalSamplesCaptured", + ], + localAudioOnly: [], + localVideoOnly: ["frames", "framesPerSecond", "width", "height"], + optional: [], + deprecated: [], + }, + csrc: { skip: true }, + codec: { + expected: [ + "timestamp", + "type", + "id", + "payloadType", + "transportId", + "mimeType", + "clockRate", + "sdpFmtpLine", + ], + optional: ["codecType", "channels"], + unimplemented: [], + deprecated: [], + }, + "peer-connection": { skip: true }, + "data-channel": { skip: true }, + track: { skip: true }, + transport: { skip: true }, + "candidate-pair": { + expected: [ + "id", + "timestamp", + "type", + "transportId", + "localCandidateId", + "remoteCandidateId", + "state", + "priority", + "nominated", + "writable", + "readable", + "bytesSent", + "bytesReceived", + "lastPacketSentTimestamp", + "lastPacketReceivedTimestamp", + ], + optional: ["selected"], + unimplemented: [ + "totalRoundTripTime", + "currentRoundTripTime", + "availableOutgoingBitrate", + "availableIncomingBitrate", + "requestsReceived", + "requestsSent", + "responsesReceived", + "responsesSent", + "retransmissionsReceived", + "retransmissionsSent", + "consentRequestsSent", + ], + deprecated: [], + }, + "local-candidate": { + expected: [ + "id", + "timestamp", + "type", + "address", + "protocol", + "port", + "candidateType", + "priority", + ], + optional: ["relayProtocol", "proxied"], + unimplemented: ["networkType", "url", "transportId"], + deprecated: [ + "candidateId", + "portNumber", + "ipAddress", + "componentId", + "mozLocalTransport", + "transport", + ], + }, + "remote-candidate": { + expected: [ + "id", + "timestamp", + "type", + "address", + "protocol", + "port", + "candidateType", + "priority", + ], + optional: ["relayProtocol", "proxied"], + unimplemented: ["networkType", "url", "transportId"], + deprecated: [ + "candidateId", + "portNumber", + "ipAddress", + "componentId", + "mozLocalTransport", + "transport", + ], + }, + certificate: { skip: true }, +}; + +["inbound-rtp", "outbound-rtp", "media-source"].forEach(type => { + let s = statsExpectedByType[type]; + s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly]; +}); + +// +// Checks that the fields in a report conform to the expectations in +// statExpectedByType +// +function checkExpectedFields(report) { + report.forEach(stat => { + let expectations = statsExpectedByType[stat.type]; + ok(expectations, "Stats type " + stat.type + " was expected"); + // If the type is not expected or if it is flagged for skipping continue to + // the next + if (!expectations || expectations.skip) { + return; + } + // Check that all required fields exist + expectations.expected.forEach(field => { + ok( + field in stat, + "Expected stat field " + stat.type + "." + field + " exists" + ); + }); + // Check that each field is either expected or optional + let allowed = [...expectations.expected, ...expectations.optional]; + Object.keys(stat).forEach(field => { + ok( + allowed.includes(field), + "Stat field " + + stat.type + + "." + + field + + ` is allowed. ${JSON.stringify(stat)}` + ); + }); + + // + // Ensure that unimplemented fields are not implemented + // note: if a field is implemented it should be moved to expected or + // optional. + // + expectations.unimplemented.forEach(field => { + ok( + !Object.keys(stat).includes(field), + "Unimplemented field " + stat.type + "." + field + " does not exist." + ); + }); + + // + // Ensure that all deprecated fields are not present + // + expectations.deprecated.forEach(field => { + ok( + !Object.keys(stat).includes(field), + "Deprecated field " + stat.type + "." + field + " does not exist." + ); + }); + }); +} + +function pedanticChecks(report) { + // Check that report is only-maplike + [...report.keys()].forEach(key => + is( + report[key], + undefined, + `Report is not dictionary like, it lacks a property for key ${key}` + ) + ); + report.forEach((statObj, mapKey) => { + info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`); + }); + // eslint-disable-next-line complexity + report.forEach((statObj, mapKey) => { + let tested = {}; + // Record what fields get tested. + // To access a field foo without marking it as tested use stat.inner.foo + let stat = new Proxy(statObj, { + get(stat, key) { + if (key == "inner") { + return stat; + } + tested[key] = true; + return stat[key]; + }, + }); + + let expectations = statsExpectedByType[stat.type]; + + if (expectations.skip) { + return; + } + + // All stats share the following attributes inherited from RTCStats + is(stat.id, mapKey, stat.type + ".id is the same as the report key."); + + // timestamp + ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0"); + // If the timebase for the timestamp is not properly set the timestamp + // will appear relative to the year 1970; Bug 1495446 + const date = new Date(stat.timestamp); + ok( + date.getFullYear() > 1970, + `${stat.type}.timestamp is relative to current time, date=${date}` + ); + // + // RTCStreamStats attributes with common behavior + // + // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp + // inherit from RTCStreamStats + if ( + [ + "inbound-rtp", + "outbound-rtp", + "remote-inbound-rtp", + "remote-outbound-rtp", + ].includes(stat.type) + ) { + const isRemote = stat.type.startsWith("remote-"); + // + // Common RTCStreamStats fields + // + + // SSRC + ok(stat.ssrc, stat.type + ".ssrc has a value"); + + // kind + ok( + ["audio", "video"].includes(stat.kind), + stat.type + ".kind is 'audio' or 'video'" + ); + + // mediaType, renamed to kind but remains for backward compability. + ok( + ["audio", "video"].includes(stat.mediaType), + stat.type + ".mediaType is 'audio' or 'video'" + ); + + ok(stat.kind == stat.mediaType, "kind equals legacy mediaType"); + + // codecId + ok(stat.codecId, `${stat.type}.codecId has a value`); + ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`); + is( + report.get(stat.codecId).type, + "codec", + `codecId ${stat.codecId} in report is codec type` + ); + is( + report.get(stat.codecId).mimeType.slice(0, 5), + stat.kind, + `codecId ${stat.codecId} in report is for a mimeType of the same ` + + `media type as the referencing rtp stream stat` + ); + + if (isRemote) { + // local id + if (stat.localId) { + ok( + report.has(stat.localId), + `localId ${stat.localId} exists in report.` + ); + is( + report.get(stat.localId).ssrc, + stat.ssrc, + "remote ssrc and local ssrc match." + ); + is( + report.get(stat.localId).remoteId, + stat.id, + "local object has remote object as it's own remote object." + ); + } + } else { + // remote id + if (stat.remoteId) { + ok( + report.has(stat.remoteId), + `remoteId ${stat.remoteId} exists in report.` + ); + is( + report.get(stat.remoteId).ssrc, + stat.ssrc, + "remote ssrc and local ssrc match." + ); + is( + report.get(stat.remoteId).localId, + stat.id, + "remote object has local object as it's own local object." + ); + } + } + + // nackCount + if (stat.nackCount) { + ok( + stat.nackCount >= 0, + `${stat.type}.nackCount is sane (${stat.kind}).` + ); + } + + if (!isRemote && stat.inner.kind == "video") { + // firCount + ok( + stat.firCount >= 0 && stat.firCount < 100, + `${stat.type}.firCount is a sane number for a short ` + + `${stat.kind} test. value=${stat.firCount}` + ); + + // pliCount + ok( + stat.pliCount >= 0 && stat.pliCount < 200, + `${stat.type}.pliCount is a sane number for a short ` + + `${stat.kind} test. value=${stat.pliCount}` + ); + + // qpSum + if (stat.qpSum !== undefined) { + ok( + stat.qpSum >= 0, + `${stat.type}.qpSum is at least 0 ` + + `${stat.kind} test. value=${stat.qpSum}` + ); + } + } else { + is( + stat.qpSum, + undefined, + `${stat.type}.qpSum does not exist when stat.kind != video` + ); + } + } + + if (stat.type == "inbound-rtp") { + // + // Required fields + // + + // trackIdentifier + is(typeof stat.trackIdentifier, "string"); + isnot(stat.trackIdentifier, ""); + + // packetsReceived + ok( + stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5, + `${stat.type}.packetsReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsReceived}` + ); + + // packetsDiscarded + ok( + stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100, + `${stat.type}.packetsDiscarded is sane number for a short test. ` + + `value=${stat.packetsDiscarded}` + ); + // bytesReceived + ok( + stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess + `${stat.type}.bytesReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.bytesReceived}` + ); + + // packetsLost + ok( + stat.packetsLost < 100, + `${stat.type}.packetsLost is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsLost}` + ); + + // This should be much lower for audio, TODO: Bug 1330575 + let expectedJitter = stat.kind == "video" ? 0.5 : 1; + // jitter + ok( + stat.jitter < expectedJitter, + `${stat.type}.jitter is sane number for a ${stat.kind} ` + + `local only test. value=${stat.jitter}` + ); + + // lastPacketReceivedTimestamp + ok( + stat.lastPacketReceivedTimestamp !== undefined, + `${stat.type}.lastPacketReceivedTimestamp has a value` + ); + + // headerBytesReceived + ok( + stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 50000, + `${stat.type}.headerBytesReceived is sane for a short test. ` + + `value=${stat.headerBytesReceived}` + ); + + // Always missing from libwebrtc stats + // estimatedPlayoutTimestamp + // ok( + // stat.estimatedPlayoutTimestamp !== undefined, + // `${stat.type}.estimatedPlayoutTimestamp has a value` + // ); + + // jitterBufferEmittedCount + let expectedJitterBufferEmmitedCount = stat.kind == "video" ? 7 : 1000; + ok( + stat.jitterBufferEmittedCount > expectedJitterBufferEmmitedCount, + `${stat.type}.jitterBufferEmittedCount is a sane number for a short ` + + `${stat.kind} test. value=${stat.jitterBufferEmittedCount}` + ); + + // jitterBufferDelay + let avgJitterBufferDelay = + stat.jitterBufferDelay / stat.jitterBufferEmittedCount; + ok( + avgJitterBufferDelay > 0 && avgJitterBufferDelay < 10, + `${stat.type}.jitterBufferDelay is a sane number for a short ` + + `${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}` + ); + + // + // Optional fields + // + + // + // Local audio only stats + // + if (stat.inner.kind != "audio") { + expectations.localAudioOnly.forEach(field => { + ok( + stat[field] === undefined, + `${stat.type} does not have field ${field}` + + ` when kind is not 'audio'` + ); + }); + } else { + expectations.localAudioOnly.forEach(field => { + ok( + stat.inner[field] !== undefined, + stat.type + " has field " + field + " when kind is video" + ); + }); + // totalSamplesReceived + ok( + stat.totalSamplesReceived > 1000, + `${stat.type}.totalSamplesReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalSamplesReceived}` + ); + + // fecPacketsReceived + ok( + stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5, + `${stat.type}.fecPacketsReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.fecPacketsReceived}` + ); + + // fecPacketsDiscarded + ok( + stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100, + `${stat.type}.fecPacketsDiscarded is sane number for a short test. ` + + `value=${stat.fecPacketsDiscarded}` + ); + // concealedSamples + ok( + stat.concealedSamples >= 0 && + stat.concealedSamples <= stat.totalSamplesReceived, + `${stat.type}.concealedSamples is a sane number for a short ` + + `${stat.kind} test. value=${stat.concealedSamples}` + ); + + // silentConcealedSamples + ok( + stat.silentConcealedSamples >= 0 && + stat.silentConcealedSamples <= stat.concealedSamples, + `${stat.type}.silentConcealedSamples is a sane number for a short ` + + `${stat.kind} test. value=${stat.silentConcealedSamples}` + ); + + // concealmentEvents + ok( + stat.concealmentEvents >= 0 && + stat.concealmentEvents <= stat.packetsReceived, + `${stat.type}.concealmentEvents is a sane number for a short ` + + `${stat.kind} test. value=${stat.concealmentEvents}` + ); + + // insertedSamplesForDeceleration + ok( + stat.insertedSamplesForDeceleration >= 0 && + stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived, + `${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` + + `${stat.kind} test. value=${stat.insertedSamplesForDeceleration}` + ); + + // removedSamplesForAcceleration + ok( + stat.removedSamplesForAcceleration >= 0 && + stat.removedSamplesForAcceleration <= stat.totalSamplesReceived, + `${stat.type}.removedSamplesForAcceleration is a sane number for a short ` + + `${stat.kind} test. value=${stat.removedSamplesForAcceleration}` + ); + + // audioLevel + ok( + stat.audioLevel >= 0 && stat.audioLevel <= 128, + `${stat.type}.bytesReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.audioLevel}` + ); + + // totalAudioEnergy + ok( + stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128, + `${stat.type}.totalAudioEnergy is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalAudioEnergy}` + ); + + // totalSamplesDuration + ok( + stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300, + `${stat.type}.totalSamplesDuration is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalSamplesDuration}` + ); + } + + // + // Local video only stats + // + if (stat.inner.kind != "video") { + expectations.localVideoOnly.forEach(field => { + ok( + stat[field] === undefined, + `${stat.type} does not have field ${field}` + + ` when kind is not 'video'` + ); + }); + } else { + expectations.localVideoOnly.forEach(field => { + ok( + stat.inner[field] !== undefined, + stat.type + " has field " + field + " when kind is video" + ); + }); + // discardedPackets + ok( + stat.discardedPackets < 100, + `${stat.type}.discardedPackets is a sane number for a short test. ` + + `value=${stat.discardedPackets}` + ); + // framesPerSecond + ok( + stat.framesPerSecond > 0 && stat.framesPerSecond < 70, + `${stat.type}.framesPerSecond is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesPerSecond}` + ); + + // framesDecoded + ok( + stat.framesDecoded > 0 && stat.framesDecoded < 1000000, + `${stat.type}.framesDecoded is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesDecoded}` + ); + + // framesDropped + ok( + stat.framesDropped >= 0 && stat.framesDropped < 100, + `${stat.type}.framesDropped is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesDropped}` + ); + + // frameWidth + ok( + stat.frameWidth > 0 && stat.frameWidth < 100000, + `${stat.type}.frameWidth is a sane number for a short ` + + `${stat.kind} test. value=${stat.frameWidth}` + ); + + // frameHeight + ok( + stat.frameHeight > 0 && stat.frameHeight < 100000, + `${stat.type}.frameHeight is a sane number for a short ` + + `${stat.kind} test. value=${stat.frameHeight}` + ); + + // totalDecodeTime + ok( + stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300, + `${stat.type}.totalDecodeTime is sane for a short test. ` + + `value=${stat.totalDecodeTime}` + ); + + // totalProcessingDelay + ok( + stat.totalProcessingDelay < 100, + `${stat.type}.totalProcessingDelay is sane number for a short test ` + + `local only test. value=${stat.totalProcessingDelay}` + ); + + // totalInterFrameDelay + ok( + stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 100, + `${stat.type}.totalInterFrameDelay is sane for a short test. ` + + `value=${stat.totalInterFrameDelay}` + ); + + // totalSquaredInterFrameDelay + ok( + stat.totalSquaredInterFrameDelay >= 0 && + stat.totalSquaredInterFrameDelay < 100, + `${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` + + `value=${stat.totalSquaredInterFrameDelay}` + ); + + // framesReceived + ok( + stat.framesReceived >= 0 && stat.framesReceived < 100000, + `${stat.type}.framesReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesReceived}` + ); + } + } else if (stat.type == "remote-inbound-rtp") { + // roundTripTime + ok( + stat.roundTripTime >= 0, + `${stat.type}.roundTripTime is sane with` + + `value of: ${stat.roundTripTime} (${stat.kind})` + ); + // + // Required fields + // + + // packetsLost + ok( + stat.packetsLost < 100, + `${stat.type}.packetsLost is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsLost}` + ); + + // jitter + ok( + stat.jitter >= 0, + `${stat.type}.jitter is sane number (${stat.kind}). ` + + `value=${stat.jitter}` + ); + + // + // Optional fields + // + + // packetsReceived + if (stat.packetsReceived) { + ok( + stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5, + `${stat.type}.packetsReceived is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsReceived}` + ); + } + + // totalRoundTripTime + ok( + stat.totalRoundTripTime < 50000, + `${stat.type}.totalRoundTripTime is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalRoundTripTime}` + ); + + // fractionLost + ok( + stat.fractionLost < 0.2, + `${stat.type}.fractionLost is a sane number for a short ` + + `${stat.kind} test. value=${stat.fractionLost}` + ); + + // roundTripTimeMeasurements + ok( + stat.roundTripTimeMeasurements >= 1 && + stat.roundTripTimeMeasurements < 500, + `${stat.type}.roundTripTimeMeasurements is a sane number for a short ` + + `${stat.kind} test. value=${stat.roundTripTimeMeasurements}` + ); + } else if (stat.type == "outbound-rtp") { + // + // Required fields + // + + // packetsSent + ok( + stat.packetsSent > 0 && stat.packetsSent < 10000, + `${stat.type}.packetsSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsSent}` + ); + + // bytesSent + const audio1Min = 16000 * 60; // 128kbps + const video1Min = 250000 * 60; // 2Mbps + ok( + stat.bytesSent > 0 && + stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min), + `${stat.type}.bytesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.bytesSent}` + ); + + // headerBytesSent + ok( + stat.headerBytesSent > 0 && + stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min), + `${stat.type}.headerBytesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.headerBytesSent}` + ); + + // retransmittedPacketsSent + ok( + stat.retransmittedPacketsSent >= 0 && + stat.retransmittedPacketsSent < + (stat.kind == "video" ? video1Min : audio1Min), + `${stat.type}.retransmittedPacketsSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.retransmittedPacketsSent}` + ); + + // retransmittedBytesSent + ok( + stat.retransmittedBytesSent >= 0 && + stat.retransmittedBytesSent < + (stat.kind == "video" ? video1Min : audio1Min), + `${stat.type}.retransmittedBytesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.retransmittedBytesSent}` + ); + + // + // Optional fields + // + + // qpSum + // This is supported for all of our vpx codecs (on the encode side, see + // bug 1519590) + const mimeType = report.get(stat.codecId).mimeType; + if (mimeType.includes("VP")) { + ok( + stat.qpSum >= 0, + `${stat.type}.qpSum is a sane number (${stat.kind}) ` + + `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}` + ); + } else if (mimeType.includes("H264")) { + // OpenH264 encoder records QP so we check for either condition. + if (!stat.qpSum && !("qpSum" in stat)) { + ok( + !stat.qpSum && !("qpSum" in stat), + `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}` + ); + } else { + ok( + stat.qpSum >= 0, + `${stat.type}.qpSum is a sane number (${stat.kind}) ` + + `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}` + ); + } + } else { + ok( + !stat.qpSum && !("qpSum" in stat), + `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}` + ); + } + + // + // Local video only stats + // + if (stat.inner.kind != "video") { + expectations.localVideoOnly.forEach(field => { + ok( + stat[field] === undefined, + `${stat.type} does not have field ` + + `${field} when kind is not 'video'` + ); + }); + } else { + expectations.localVideoOnly.forEach(field => { + ok( + stat.inner[field] !== undefined, + `${stat.type} has field ` + + `${field} when kind is video and isRemote is false` + ); + }); + + // framesEncoded + ok( + stat.framesEncoded >= 0 && stat.framesEncoded < 100000, + `${stat.type}.framesEncoded is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesEncoded}` + ); + + // frameWidth + ok( + stat.frameWidth >= 0 && stat.frameWidth < 100000, + `${stat.type}.frameWidth is a sane number for a short ` + + `${stat.kind} test. value=${stat.frameWidth}` + ); + + // frameHeight + ok( + stat.frameHeight >= 0 && stat.frameHeight < 100000, + `${stat.type}.frameHeight is a sane number for a short ` + + `${stat.kind} test. value=${stat.frameHeight}` + ); + + // framesPerSecond + ok( + stat.framesPerSecond >= 0 && stat.framesPerSecond < 60, + `${stat.type}.framesPerSecond is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesPerSecond}` + ); + + // framesSent + ok( + stat.framesSent >= 0 && stat.framesSent < 100000, + `${stat.type}.framesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesSent}` + ); + + // hugeFramesSent + ok( + stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000, + `${stat.type}.hugeFramesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.hugeFramesSent}` + ); + + // totalEncodeTime + ok( + stat.totalEncodeTime >= 0, + `${stat.type}.totalEncodeTime is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalEncodeTime}` + ); + + // totalEncodedBytesTarget + ok( + stat.totalEncodedBytesTarget > 1000, + `${stat.type}.totalEncodedBytesTarget is a sane number for a short ` + + `${stat.kind} test. value=${stat.totalEncodedBytesTarget}` + ); + } + } else if (stat.type == "remote-outbound-rtp") { + // + // Required fields + // + + // packetsSent + ok( + stat.packetsSent > 0 && stat.packetsSent < 10000, + `${stat.type}.packetsSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.packetsSent}` + ); + + // bytesSent + const audio1Min = 16000 * 60; // 128kbps + const video1Min = 250000 * 60; // 2Mbps + ok( + stat.bytesSent > 0 && + stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min), + `${stat.type}.bytesSent is a sane number for a short ` + + `${stat.kind} test. value=${stat.bytesSent}` + ); + + ok( + stat.remoteTimestamp !== undefined, + `${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})` + ); + const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000; + // remoteTimestamp is exact (so it can be mapped to a packet), whereas + // timestamp has reduced precision. It is possible that + // remoteTimestamp occurs a millisecond into the future from + // timestamp. We also subtract half a millisecond when reducing + // precision on libwebrtc timestamps, to counteract the potential + // rounding up that libwebrtc may do since it tends to round its + // internal timestamps to whole milliseconds. In the worst case + // remoteTimestamp may therefore occur 2 milliseconds ahead of + // timestamp. + ok( + ageSeconds >= -0.002 && ageSeconds < 30, + `${stat.type}.remoteTimestamp is on the same timeline as ` + + `${stat.type}.timestamp, and no older than 30 seconds. ` + + `difference=${ageSeconds}s` + ); + } else if (stat.type == "media-source") { + // trackIdentifier + is(typeof stat.trackIdentifier, "string"); + isnot(stat.trackIdentifier, ""); + + // kind + is(typeof stat.kind, "string"); + ok(stat.kind == "audio" || stat.kind == "video"); + if (stat.inner.kind == "video") { + expectations.localVideoOnly.forEach(field => { + ok( + stat.inner[field] !== undefined, + `${stat.type} has field ` + + `${field} when kind is video and isRemote is false` + ); + }); + + // frames + ok( + stat.frames >= 0 && stat.frames < 100000, + `${stat.type}.frames is a sane number for a short ` + + `${stat.kind} test. value=${stat.frames}` + ); + + // framesPerSecond + ok( + stat.framesPerSecond >= 0 && stat.framesPerSecond < 100, + `${stat.type}.framesPerSecond is a sane number for a short ` + + `${stat.kind} test. value=${stat.framesPerSecond}` + ); + + // width + ok( + stat.width >= 0 && stat.width < 1000000, + `${stat.type}.width is a sane number for a ` + + `${stat.kind} test. value=${stat.width}` + ); + + // height + ok( + stat.height >= 0 && stat.height < 1000000, + `${stat.type}.height is a sane number for a ` + + `${stat.kind} test. value=${stat.height}` + ); + } else { + expectations.localVideoOnly.forEach(field => { + ok( + stat[field] === undefined, + `${stat.type} does not have field ` + + `${field} when kind is not 'video'` + ); + }); + } + } else if (stat.type == "codec") { + // + // Required fields + // + + // mimeType & payloadType + switch (stat.mimeType) { + case "audio/opus": + is(stat.payloadType, 109, "codec.payloadType for opus"); + break; + case "video/VP8": + is(stat.payloadType, 120, "codec.payloadType for VP8"); + break; + case "video/VP9": + is(stat.payloadType, 121, "codec.payloadType for VP9"); + break; + case "video/H264": + ok( + stat.payloadType == 97 || stat.payloadType == 126, + `codec.payloadType for H264 was ${stat.payloadType}, exp. 97 or 126` + ); + break; + default: + ok( + false, + `Unexpected codec.mimeType ${stat.mimeType} for payloadType ` + + `${stat.payloadType}` + ); + break; + } + + // transportId + // (no transport stats yet) + ok(stat.transportId, "codec.transportId is set"); + + // clockRate + if (stat.mimeType.startsWith("audio")) { + is(stat.clockRate, 48000, "codec.clockRate for audio/opus"); + } else if (stat.mimeType.startsWith("video")) { + is(stat.clockRate, 90000, "codec.clockRate for video"); + } + + // sdpFmtpLine + // (not technically mandated by spec, but expected here) + ok(stat.sdpFmtpLine, "codec.sdpFmtpLine is set"); + const opusParams = [ + "maxplaybackrate", + "maxaveragebitrate", + "usedtx", + "stereo", + "useinbandfec", + "cbr", + "ptime", + "minptime", + "maxptime", + ]; + const vpxParams = ["max-fs", "max-fr"]; + const h264Params = [ + "packetization-mode", + "level-asymmetry-allowed", + "profile-level-id", + "max-fs", + "max-cpb", + "max-dpb", + "max-br", + "max-mbps", + ]; + for (const param of stat.sdpFmtpLine.split(";")) { + const [key, value] = param.split("="); + if (stat.payloadType == 109) { + ok( + opusParams.includes(key), + `codec.sdpFmtpLine param ${key}=${value} for opus` + ); + } else if (stat.payloadType == 120 || stat.payloadType == 121) { + ok( + vpxParams.includes(key), + `codec.sdpFmtpLine param ${key}=${value} for VPx` + ); + } else if (stat.payloadType == 97 || stat.payloadType == 126) { + ok( + h264Params.includes(key), + `codec.sdpFmtpLine param ${key}=${value} for H264` + ); + if (key == "packetization-mode") { + if (stat.payloadType == 97) { + is(value, "0", "codec.sdpFmtpLine: H264 (97) packetization-mode"); + } else if (stat.payloadType == 126) { + is( + value, + "1", + "codec.sdpFmtpLine: H264 (126) packetization-mode" + ); + } + } + if (key == "profile-level-id") { + is(value, "42e01f", "codec.sdpFmtpLine: H264 profile-level-id"); + } + } + } + + // + // Optional fields + // + + // codecType + ok( + !Object.keys(stat).includes("codecType") || + stat.codecType == "encode" || + stat.codecType == "decode", + "codec.codecType (${codec.codecType}) is an expected value or absent" + ); + let numRecvStreams = 0; + let numSendStreams = 0; + const counts = { + "inbound-rtp": 0, + "outbound-rtp": 0, + "remote-inbound-rtp": 0, + "remote-outbound-rtp": 0, + }; + const [kind] = stat.mimeType.split("/"); + report.forEach(other => { + if (other.type == "inbound-rtp" && other.kind == kind) { + numRecvStreams += 1; + } else if (other.type == "outbound-rtp" && other.kind == kind) { + numSendStreams += 1; + } + if (other.codecId == stat.id) { + counts[other.type] += 1; + } + }); + const expectedCounts = { + encode: { + "inbound-rtp": 0, + "outbound-rtp": numSendStreams, + "remote-inbound-rtp": numSendStreams, + "remote-outbound-rtp": 0, + }, + decode: { + "inbound-rtp": numRecvStreams, + "outbound-rtp": 0, + "remote-inbound-rtp": 0, + "remote-outbound-rtp": numRecvStreams, + }, + absent: { + "inbound-rtp": numRecvStreams, + "outbound-rtp": numSendStreams, + "remote-inbound-rtp": numSendStreams, + "remote-outbound-rtp": numRecvStreams, + }, + }; + // Note that the logic above assumes at most one sender and at most one + // receiver was used to generate this stats report. If more senders or + // receivers are present, they'd be referring to not only this codec stat, + // skewing `numSendStreams` and `numRecvStreams` above. + // This could be fixed when we support `senderId` and `receiverId` in + // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively. + for (const [key, value] of Object.entries(counts)) { + is( + value, + expectedCounts[stat.codecType || "absent"][key], + `codec.codecType ${stat.codecType || "absent"} ref from ${key} stat` + ); + } + + // channels + if (stat.mimeType.startsWith("audio")) { + ok(stat.channels, "codec.channels should exist for audio"); + if (stat.channels) { + if (stat.sdpFmtpLine.includes("stereo=1")) { + is(stat.channels, 2, "codec.channels for stereo audio"); + } else { + is(stat.channels, 1, "codec.channels for mono audio"); + } + } + } else { + ok(!stat.channels, "codec.channels should not exist for video"); + } + } else if (stat.type == "candidate-pair") { + info("candidate-pair is: " + JSON.stringify(stat)); + // + // Required fields + // + + // transportId + ok( + stat.transportId, + `${stat.type}.transportId has a value. value=` + + `${stat.transportId} (${stat.kind})` + ); + + // localCandidateId + ok( + stat.localCandidateId, + `${stat.type}.localCandidateId has a value. value=` + + `${stat.localCandidateId} (${stat.kind})` + ); + + // remoteCandidateId + ok( + stat.remoteCandidateId, + `${stat.type}.remoteCandidateId has a value. value=` + + `${stat.remoteCandidateId} (${stat.kind})` + ); + + // priority + ok( + stat.priority, + `${stat.type}.priority has a value. value=` + + `${stat.priority} (${stat.kind})` + ); + + // readable + ok( + stat.readable, + `${stat.type}.readable is true. value=${stat.readable} ` + + `(${stat.kind})` + ); + + // writable + ok( + stat.writable, + `${stat.type}.writable is true. value=${stat.writable} ` + + `(${stat.kind})` + ); + + // state + if ( + stat.state == "succeeded" && + stat.selected !== undefined && + stat.selected + ) { + info("candidate-pair state is succeeded and selected is true"); + // nominated + ok( + stat.nominated, + `${stat.type}.nominated is true. value=${stat.nominated} ` + + `(${stat.kind})` + ); + + // bytesSent + ok( + stat.bytesSent > 100, + `${stat.type}.bytesSent is a sane number (>100) if media is flowing. ` + + `value=${stat.bytesSent}` + ); + + // bytesReceived + ok( + stat.bytesReceived > 100, + `${stat.type}.bytesReceived is a sane number (>100) if media is flowing. ` + + `value=${stat.bytesReceived}` + ); + + // lastPacketSentTimestamp + ok( + stat.lastPacketSentTimestamp, + `${stat.type}.lastPacketSentTimestamp has a value. value=` + + `${stat.lastPacketSentTimestamp} (${stat.kind})` + ); + + // lastPacketReceivedTimestamp + ok( + stat.lastPacketReceivedTimestamp, + `${stat.type}.lastPacketReceivedTimestamp has a value. value=` + + `${stat.lastPacketReceivedTimestamp} (${stat.kind})` + ); + } else { + info("candidate-pair is _not_ both state == succeeded and selected"); + // nominated + ok( + stat.nominated !== undefined, + `${stat.type}.nominated exists. value=${stat.nominated} ` + + `(${stat.kind})` + ); + ok( + stat.bytesSent !== undefined, + `${stat.type}.bytesSent exists. value=${stat.bytesSent} ` + + `(${stat.kind})` + ); + ok( + stat.bytesReceived !== undefined, + `${stat.type}.bytesReceived exists. value=${stat.bytesReceived} ` + + `(${stat.kind})` + ); + ok( + stat.lastPacketSentTimestamp !== undefined, + `${stat.type}.lastPacketSentTimestamp exists. value=` + + `${stat.lastPacketSentTimestamp} (${stat.kind})` + ); + ok( + stat.lastPacketReceivedTimestamp !== undefined, + `${stat.type}.lastPacketReceivedTimestamp exists. value=` + + `${stat.lastPacketReceivedTimestamp} (${stat.kind})` + ); + } + + // + // Optional fields + // + // selected + ok( + stat.selected === undefined || + (stat.state == "succeeded" && stat.selected) || + !stat.selected, + `${stat.type}.selected is undefined, true when state is succeeded, ` + + `or false. value=${stat.selected} (${stat.kind})` + ); + } else if ( + stat.type == "local-candidate" || + stat.type == "remote-candidate" + ) { + info(`candidate is ${JSON.stringify(stat)}`); + + // address + ok( + stat.address, + `${stat.type} has address. value=${stat.address} ` + `(${stat.kind})` + ); + + // protocol + ok( + stat.protocol, + `${stat.type} has protocol. value=${stat.protocol} ` + `(${stat.kind})` + ); + + // port + ok( + stat.port >= 0, + `${stat.type} has port >= 0. value=${stat.port} ` + `(${stat.kind})` + ); + ok( + stat.port <= 65535, + `${stat.type} has port <= 65535. value=${stat.port} ` + `(${stat.kind})` + ); + + // candidateType + ok( + stat.candidateType, + `${stat.type} has candidateType. value=${stat.candidateType} ` + + `(${stat.kind})` + ); + + // priority + ok( + stat.priority > 0 && stat.priority < 2 ** 32 - 1, + `${stat.type} has priority between 1 and 2^32 - 1 inc. ` + + `value=${stat.priority} (${stat.kind})` + ); + + // relayProtocol + if (stat.type == "local-candidate" && stat.candidateType == "relay") { + ok( + stat.relayProtocol, + `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol} ` + + `(${stat.kind})` + ); + } else { + is( + stat.relayProtocol, + undefined, + `relayProtocol is undefined for candidates that are not relay and ` + + `local. value=${stat.relayProtocol} (${stat.kind})` + ); + } + + // proxied + if (stat.proxied) { + ok( + stat.proxied == "proxied" || stat.proxied == "non-proxied", + `${stat.type} has proxied. value=${stat.proxied} (${stat.kind})` + ); + } + } + + // + // Ensure everything was tested + // + [...expectations.expected, ...expectations.optional].forEach(field => { + ok( + Object.keys(tested).includes(field), + `${stat.type}.${field} was tested.` + ); + }); + }); +} + +function dumpStats(stats) { + const dict = {}; + for (const [k, v] of stats.entries()) { + dict[k] = v; + } + info(`Got stats: ${JSON.stringify(dict)}`); +} + +async function waitForSyncedRtcp(pc) { + // Ensures that RTCP is present + let ensureSyncedRtcp = async () => { + let report = await pc.getStats(); + for (const v of report.values()) { + if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) { + info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`); + return null; + } + if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) { + info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`); + return null; + } + } + return report; + }; + // Returns true if there is proof in aStats of rtcp flow for all remote stats + // objects, compared to baseStats. + const hasAllRtcpUpdated = (baseStats, stats) => { + let hasRtcpStats = false; + for (const v of stats.values()) { + if (v.type == "remote-outbound-rtp") { + hasRtcpStats = true; + if (!v.remoteTimestamp) { + // `remoteTimestamp` is 0 or not present. + return false; + } + if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) { + // `remoteTimestamp` has not advanced further than the base stats, + // i.e., no new sender report has been received. + return false; + } + } else if (v.type == "remote-inbound-rtp") { + hasRtcpStats = true; + // The ideal thing here would be to check `reportsReceived`, but it's + // not yet implemented. + if (!v.packetsReceived) { + // `packetsReceived` is 0 or not present. + return false; + } + if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) { + // `packetsReceived` has not advanced further than the base stats, + // i.e., no new receiver report has been received. + return false; + } + } + } + return hasRtcpStats; + }; + let attempts = 0; + const baseStats = await pc.getStats(); + // Time-units are MS + const waitPeriod = 100; + const maxTime = 20000; + for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) { + try { + let syncedStats = await ensureSyncedRtcp(); + if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) { + dumpStats(syncedStats); + return syncedStats; + } + } catch (e) { + info(e); + info(e.stack); + throw e; + } + attempts += 1; + info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`); + await wait(waitPeriod); + } + throw Error( + "Waiting for synced RTCP timed out after at least " + maxTime + "ms" + ); +} + +function checkSenderStats(senderStats, streamCount) { + const outboundRtpReports = []; + const remoteInboundRtpReports = []; + for (const v of senderStats.values()) { + if (v.type == "outbound-rtp") { + outboundRtpReports.push(v); + } else if (v.type == "remote-inbound-rtp") { + remoteInboundRtpReports.push(v); + } + } + is( + outboundRtpReports.length, + streamCount, + `Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports` + ); + is( + remoteInboundRtpReports.length, + streamCount, + `Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports` + ); + for (const outboundRtpReport of outboundRtpReports) { + is( + outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length, + 1, + "Simulcast send track SSRCs are distinct" + ); + const remoteReports = remoteInboundRtpReports.filter( + r => r.id == outboundRtpReport.remoteId + ); + is( + remoteReports.length, + 1, + "Simulcast send tracks have exactly one remote counterpart" + ); + const remoteInboundRtpReport = remoteReports[0]; + is( + outboundRtpReport.ssrc, + remoteInboundRtpReport.ssrc, + "SSRC matches for outbound-rtp and remote-inbound-rtp" + ); + } +} + +function PC_LOCAL_TEST_LOCAL_STATS(test) { + return waitForSyncedRtcp(test.pcLocal._pc).then(stats => { + checkExpectedFields(stats); + pedanticChecks(stats); + return Promise.all([ + test.pcLocal._pc.getSenders().map(async s => { + checkSenderStats( + await s.getStats(), + Math.max(1, s.getParameters()?.encodings?.length ?? 0) + ); + }), + ]); + }); +} + +function PC_REMOTE_TEST_REMOTE_STATS(test) { + return waitForSyncedRtcp(test.pcRemote._pc).then(stats => { + checkExpectedFields(stats); + pedanticChecks(stats); + return Promise.all([ + test.pcRemote._pc.getSenders().map(async s => { + checkSenderStats( + await s.getStats(), + s.track ? Math.max(1, s.getParameters()?.encodings?.length ?? 0) : 0 + ); + }), + ]); + }); +} diff --git a/dom/media/webrtc/tests/mochitests/templates.js b/dom/media/webrtc/tests/mochitests/templates.js new file mode 100644 index 0000000000..6b7750fd2c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/templates.js @@ -0,0 +1,615 @@ +/** + * Default list of commands to execute for a PeerConnection test. + */ + +const STABLE = "stable"; +const HAVE_LOCAL_OFFER = "have-local-offer"; +const HAVE_REMOTE_OFFER = "have-remote-offer"; +const CLOSED = "closed"; + +const ICE_NEW = "new"; +const GATH_NEW = "new"; +const GATH_GATH = "gathering"; +const GATH_COMPLETE = "complete"; + +function deltaSeconds(date1, date2) { + return (date2.getTime() - date1.getTime()) / 1000; +} + +function dumpSdp(test) { + if (typeof test._local_offer !== "undefined") { + dump("ERROR: SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, "")); + } + if (typeof test._remote_answer !== "undefined") { + dump("ERROR: SDP answer: " + test._remote_answer.sdp.replace(/[\r]/g, "")); + } + + if ( + test.pcLocal && + typeof test.pcLocal._local_ice_candidates !== "undefined" + ) { + dump( + "pcLocal._local_ice_candidates: " + + JSON.stringify(test.pcLocal._local_ice_candidates) + + "\n" + ); + dump( + "pcLocal._remote_ice_candidates: " + + JSON.stringify(test.pcLocal._remote_ice_candidates) + + "\n" + ); + dump( + "pcLocal._ice_candidates_to_add: " + + JSON.stringify(test.pcLocal._ice_candidates_to_add) + + "\n" + ); + } + if ( + test.pcRemote && + typeof test.pcRemote._local_ice_candidates !== "undefined" + ) { + dump( + "pcRemote._local_ice_candidates: " + + JSON.stringify(test.pcRemote._local_ice_candidates) + + "\n" + ); + dump( + "pcRemote._remote_ice_candidates: " + + JSON.stringify(test.pcRemote._remote_ice_candidates) + + "\n" + ); + dump( + "pcRemote._ice_candidates_to_add: " + + JSON.stringify(test.pcRemote._ice_candidates_to_add) + + "\n" + ); + } + + if (test.pcLocal && typeof test.pcLocal.iceConnectionLog !== "undefined") { + dump( + "pcLocal ICE connection state log: " + + test.pcLocal.iceConnectionLog + + "\n" + ); + } + if (test.pcRemote && typeof test.pcRemote.iceConnectionLog !== "undefined") { + dump( + "pcRemote ICE connection state log: " + + test.pcRemote.iceConnectionLog + + "\n" + ); + } + + if ( + test.pcLocal && + test.pcRemote && + typeof test.pcLocal.setRemoteDescDate !== "undefined" && + typeof test.pcRemote.setLocalDescDate !== "undefined" + ) { + var delta = deltaSeconds( + test.pcLocal.setRemoteDescDate, + test.pcRemote.setLocalDescDate + ); + dump( + "Delay between pcLocal.setRemote <-> pcRemote.setLocal: " + delta + "\n" + ); + } + if ( + test.pcLocal && + test.pcRemote && + typeof test.pcLocal.setRemoteDescDate !== "undefined" && + typeof test.pcLocal.setRemoteDescStableEventDate !== "undefined" + ) { + var delta = deltaSeconds( + test.pcLocal.setRemoteDescDate, + test.pcLocal.setRemoteDescStableEventDate + ); + dump( + "Delay between pcLocal.setRemote <-> pcLocal.signalingStateStable: " + + delta + + "\n" + ); + } + if ( + test.pcLocal && + test.pcRemote && + typeof test.pcRemote.setLocalDescDate !== "undefined" && + typeof test.pcRemote.setLocalDescStableEventDate !== "undefined" + ) { + var delta = deltaSeconds( + test.pcRemote.setLocalDescDate, + test.pcRemote.setLocalDescStableEventDate + ); + dump( + "Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " + + delta + + "\n" + ); + } +} + +// We need to verify that at least one candidate has been (or will be) gathered. +function waitForAnIceCandidate(pc) { + return new Promise(resolve => { + if (!pc.localRequiresTrickleIce || pc._local_ice_candidates.length) { + resolve(); + } else { + // In some circumstances, especially when both PCs are on the same + // browser, even though we are connected, the connection can be + // established without receiving a single candidate from one or other + // peer. So we wait for at least one... + pc._pc.addEventListener("icecandidate", resolve); + } + }).then(() => { + ok( + pc._local_ice_candidates.length, + pc + " received local trickle ICE candidates" + ); + isnot( + pc._pc.iceGatheringState, + GATH_NEW, + pc + " ICE gathering state is not 'new'" + ); + }); +} + +async function checkTrackStats(pc, track, outbound) { + const audio = track.kind == "audio"; + const msg = + `${pc} stats ${outbound ? "outbound " : "inbound "}` + + `${audio ? "audio" : "video"} rtp track id ${track.id}`; + const stats = await pc.getStats(track); + ok( + pc.hasStat(stats, { + type: outbound ? "outbound-rtp" : "inbound-rtp", + kind: audio ? "audio" : "video", + }), + `${msg} - found expected stats` + ); + ok( + !pc.hasStat(stats, { + type: outbound ? "inbound-rtp" : "outbound-rtp", + }), + `${msg} - did not find extra stats with wrong direction` + ); + ok( + !pc.hasStat(stats, { + kind: audio ? "video" : "audio", + }), + `${msg} - did not find extra stats with wrong media type` + ); +} + +function checkAllTrackStats(pc) { + return Promise.all([ + ...pc + .getExpectedActiveReceivers() + .map(({ track }) => checkTrackStats(pc, track, false)), + ...pc + .getExpectedSenders() + .map(({ track }) => checkTrackStats(pc, track, true)), + ]); +} + +// Commands run once at the beginning of each test, even when performing a +// renegotiation test. +var commandsPeerConnectionInitial = [ + function PC_LOCAL_SETUP_ICE_LOGGER(test) { + test.pcLocal.logIceConnectionState(); + }, + + function PC_REMOTE_SETUP_ICE_LOGGER(test) { + test.pcRemote.logIceConnectionState(); + }, + + function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) { + test.pcLocal.logSignalingState(); + }, + + function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) { + test.pcRemote.logSignalingState(); + }, + + function PC_LOCAL_SETUP_TRACK_HANDLER(test) { + test.pcLocal.setupTrackEventHandler(); + }, + + function PC_REMOTE_SETUP_TRACK_HANDLER(test) { + test.pcRemote.setupTrackEventHandler(); + }, + + function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) { + is( + test.pcLocal.signalingState, + STABLE, + "Initial local signalingState is 'stable'" + ); + }, + + function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) { + is( + test.pcRemote.signalingState, + STABLE, + "Initial remote signalingState is 'stable'" + ); + }, + + function PC_LOCAL_CHECK_INITIAL_ICE_STATE(test) { + is( + test.pcLocal.iceConnectionState, + ICE_NEW, + "Initial local ICE connection state is 'new'" + ); + }, + + function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) { + is( + test.pcRemote.iceConnectionState, + ICE_NEW, + "Initial remote ICE connection state is 'new'" + ); + }, + + function PC_LOCAL_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) { + is( + test.pcLocal._pc.canTrickleIceCandidates, + null, + "Local trickle status should start out unknown" + ); + }, + + function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) { + is( + test.pcRemote._pc.canTrickleIceCandidates, + null, + "Remote trickle status should start out unknown" + ); + }, +]; + +var commandsGetUserMedia = [ + function PC_LOCAL_GUM(test) { + return test.pcLocal.getAllUserMediaAndAddStreams(test.pcLocal.constraints); + }, + + function PC_REMOTE_GUM(test) { + return test.pcRemote.getAllUserMediaAndAddStreams( + test.pcRemote.constraints + ); + }, +]; + +var commandsPeerConnectionOfferAnswer = [ + function PC_LOCAL_SETUP_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test); + }, + + function PC_REMOTE_SETUP_ICE_HANDLER(test) { + test.pcRemote.setupIceCandidateHandler(test); + }, + + function PC_LOCAL_CREATE_OFFER(test) { + return test.createOffer(test.pcLocal).then(offer => { + is( + test.pcLocal.signalingState, + STABLE, + "Local create offer does not change signaling state" + ); + }); + }, + + function PC_LOCAL_SET_LOCAL_DESCRIPTION(test) { + return test + .setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER) + .then(() => { + is( + test.pcLocal.signalingState, + HAVE_LOCAL_OFFER, + "signalingState after local setLocalDescription is 'have-local-offer'" + ); + }); + }, + + function PC_REMOTE_GET_OFFER(test) { + test._local_offer = test.originalOffer; + test._offer_constraints = test.pcLocal.constraints; + test._offer_options = test.pcLocal.offerOptions; + return Promise.resolve(); + }, + + function PC_REMOTE_SET_REMOTE_DESCRIPTION(test) { + return test + .setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER) + .then(() => { + is( + test.pcRemote.signalingState, + HAVE_REMOTE_OFFER, + "signalingState after remote setRemoteDescription is 'have-remote-offer'" + ); + }); + }, + + function PC_REMOTE_CHECK_CAN_TRICKLE_SYNC(test) { + is( + test.pcRemote._pc.canTrickleIceCandidates, + true, + "Remote thinks that local can trickle" + ); + }, + + function PC_LOCAL_SANE_LOCAL_SDP(test) { + test.pcLocal.localRequiresTrickleIce = sdputils.verifySdp( + test._local_offer, + "offer", + test._offer_constraints, + test._offer_options, + test.testOptions + ); + }, + + function PC_REMOTE_SANE_REMOTE_SDP(test) { + test.pcRemote.remoteRequiresTrickleIce = sdputils.verifySdp( + test._local_offer, + "offer", + test._offer_constraints, + test._offer_options, + test.testOptions + ); + }, + + function PC_REMOTE_CREATE_ANSWER(test) { + return test.createAnswer(test.pcRemote).then(answer => { + is( + test.pcRemote.signalingState, + HAVE_REMOTE_OFFER, + "Remote createAnswer does not change signaling state" + ); + }); + }, + + function PC_REMOTE_SET_LOCAL_DESCRIPTION(test) { + return test + .setLocalDescription(test.pcRemote, test.originalAnswer, STABLE) + .then(() => { + is( + test.pcRemote.signalingState, + STABLE, + "signalingState after remote setLocalDescription is 'stable'" + ); + }); + }, + + function PC_LOCAL_GET_ANSWER(test) { + test._remote_answer = test.originalAnswer; + test._answer_constraints = test.pcRemote.constraints; + return Promise.resolve(); + }, + + function PC_LOCAL_SET_REMOTE_DESCRIPTION(test) { + return test + .setRemoteDescription(test.pcLocal, test._remote_answer, STABLE) + .then(() => { + is( + test.pcLocal.signalingState, + STABLE, + "signalingState after local setRemoteDescription is 'stable'" + ); + }); + }, + + function PC_REMOTE_SANE_LOCAL_SDP(test) { + test.pcRemote.localRequiresTrickleIce = sdputils.verifySdp( + test._remote_answer, + "answer", + test._offer_constraints, + test._offer_options, + test.testOptions + ); + }, + function PC_LOCAL_SANE_REMOTE_SDP(test) { + test.pcLocal.remoteRequiresTrickleIce = sdputils.verifySdp( + test._remote_answer, + "answer", + test._offer_constraints, + test._offer_options, + test.testOptions + ); + }, + + function PC_LOCAL_CHECK_CAN_TRICKLE_SYNC(test) { + is( + test.pcLocal._pc.canTrickleIceCandidates, + true, + "Local thinks that remote can trickle" + ); + }, + + function PC_LOCAL_WAIT_FOR_ICE_CONNECTED(test) { + return test.pcLocal.waitForIceConnected().then(() => { + info( + test.pcLocal + + ": ICE connection state log: " + + test.pcLocal.iceConnectionLog + ); + }); + }, + + function PC_REMOTE_WAIT_FOR_ICE_CONNECTED(test) { + return test.pcRemote.waitForIceConnected().then(() => { + info( + test.pcRemote + + ": ICE connection state log: " + + test.pcRemote.iceConnectionLog + ); + }); + }, + + function PC_LOCAL_VERIFY_ICE_GATHERING(test) { + return waitForAnIceCandidate(test.pcLocal); + }, + + function PC_REMOTE_VERIFY_ICE_GATHERING(test) { + return waitForAnIceCandidate(test.pcRemote); + }, + + function PC_LOCAL_WAIT_FOR_MEDIA_FLOW(test) { + return test.pcLocal.waitForMediaFlow(); + }, + + function PC_REMOTE_WAIT_FOR_MEDIA_FLOW(test) { + return test.pcRemote.waitForMediaFlow(); + }, + + function PC_LOCAL_CHECK_STATS(test) { + return test.pcLocal.getStats().then(stats => { + test.pcLocal.checkStats(stats); + }); + }, + + function PC_REMOTE_CHECK_STATS(test) { + return test.pcRemote.getStats().then(stats => { + test.pcRemote.checkStats(stats); + }); + }, + + function PC_LOCAL_CHECK_ICE_CONNECTION_TYPE(test) { + return test.pcLocal.getStats().then(stats => { + test.pcLocal.checkStatsIceConnectionType( + stats, + test.testOptions.expectedLocalCandidateType + ); + }); + }, + + function PC_REMOTE_CHECK_ICE_CONNECTION_TYPE(test) { + return test.pcRemote.getStats().then(stats => { + test.pcRemote.checkStatsIceConnectionType( + stats, + test.testOptions.expectedRemoteCandidateType + ); + }); + }, + + function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) { + return test.pcLocal.getStats().then(stats => { + test.pcLocal.checkStatsIceConnections(stats, test.testOptions); + }); + }, + + function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) { + return test.pcRemote.getStats().then(stats => { + test.pcRemote.checkStatsIceConnections(stats, test.testOptions); + }); + }, + + function PC_LOCAL_CHECK_MSID(test) { + return test.pcLocal.checkLocalMsids(); + }, + function PC_REMOTE_CHECK_MSID(test) { + return test.pcRemote.checkLocalMsids(); + }, + + function PC_LOCAL_CHECK_TRACK_STATS(test) { + return checkAllTrackStats(test.pcLocal); + }, + function PC_REMOTE_CHECK_TRACK_STATS(test) { + return checkAllTrackStats(test.pcRemote); + }, + function PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) { + if (test.pcLocal.endOfTrickleSdp) { + /* In case the endOfTrickleSdp promise is resolved already it will win the + * race because it gets evaluated first. But if endOfTrickleSdp is still + * pending the rejection will win the race. */ + return Promise.race([ + test.pcLocal.endOfTrickleSdp, + Promise.reject("No SDP"), + ]).then( + sdp => + sdputils.checkSdpAfterEndOfTrickle( + sdp, + test.testOptions, + test.pcLocal.label + ), + () => + info( + "pcLocal: Gathering is not complete yet, skipping post-gathering SDP check" + ) + ); + } + }, + function PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) { + if (test.pcRemote.endOfTrickleSdp) { + /* In case the endOfTrickleSdp promise is resolved already it will win the + * race because it gets evaluated first. But if endOfTrickleSdp is still + * pending the rejection will win the race. */ + return Promise.race([ + test.pcRemote.endOfTrickleSdp, + Promise.reject("No SDP"), + ]).then( + sdp => + sdputils.checkSdpAfterEndOfTrickle( + sdp, + test.testOptions, + test.pcRemote.label + ), + () => + info( + "pcRemote: Gathering is not complete yet, skipping post-gathering SDP check" + ) + ); + } + }, +]; + +function PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER(test) { + isnot( + test.originalOffer.sdp.search("H264/90000"), + -1, + "H.264 should be present in the SDP offer" + ); + test.originalOffer.sdp = sdputils.removeCodec( + sdputils.removeCodec( + sdputils.removeCodec(test.originalOffer.sdp, 120), + 121, + 97 + ) + ); + info("Updated H264 only offer: " + JSON.stringify(test.originalOffer)); +} + +function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) { + test.originalOffer.sdp = sdputils.removeBundle(test.originalOffer.sdp); + info("Updated no bundle offer: " + JSON.stringify(test.originalOffer)); +} + +function PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER(test) { + test.originalOffer.sdp = sdputils.removeRtcpMux(test.originalOffer.sdp); + info("Updated no RTCP-Mux offer: " + JSON.stringify(test.originalOffer)); +} + +function PC_LOCAL_REMOVE_SSRC_FROM_OFFER(test) { + test.originalOffer.sdp = sdputils.removeSSRCs(test.originalOffer.sdp); + info("Updated no SSRCs offer: " + JSON.stringify(test.originalOffer)); +} + +function PC_REMOTE_REMOVE_SSRC_FROM_ANSWER(test) { + test.originalAnswer.sdp = sdputils.removeSSRCs(test.originalAnswer.sdp); + info("Updated no SSRCs answer: " + JSON.stringify(test.originalAnswer)); +} + +var addRenegotiation = (chain, commands, checks) => { + chain.append(commands); + chain.append(commandsPeerConnectionOfferAnswer); + if (checks) { + chain.append(checks); + } +}; + +var addRenegotiationAnswerer = (chain, commands, checks) => { + chain.append(function SWAP_PC_LOCAL_PC_REMOTE(test) { + var temp = test.pcLocal; + test.pcLocal = test.pcRemote; + test.pcRemote = temp; + }); + addRenegotiation(chain, commands, checks); +}; diff --git a/dom/media/webrtc/tests/mochitests/test_1488832.html b/dom/media/webrtc/tests/mochitests/test_1488832.html new file mode 100644 index 0000000000..8798994b24 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_1488832.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> +<script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<iframe id="testframe"></iframe> +<script> +"use strict"; + +createHTML({ + title: "gUM shutdown race", + bug: "1488832" +}); + +runTest(async () => { + testframe.srcdoc = ` + <html> + <head> + <script> + function start() { + for (let i = 0; i < 16; i++) { + window.navigator.mediaDevices.getUserMedia({video: true}) + setTimeout('location.reload()', 100) + } + } + document.addEventListener('DOMContentLoaded', start) + </` + `script> + </head> + </html>`; + + await wait(10000); + testframe.srcdoc = ""; +}); +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_1717318.html b/dom/media/webrtc/tests/mochitests/test_1717318.html new file mode 100644 index 0000000000..425bd29e7e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_1717318.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>PC construct with no global object (bug 1717318)</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +"use strict"; + +// nsIArray is not special here, it could be pretty much any interface. +// We do this outside the try block just in case someday the interface is +// removed. +const dummyInterface = SpecialPowers.Components.interfaces.nsIArray; +ok(dummyInterface, "nsIArray should exist"); +try { + // Just don't crash. + SpecialPowers.Components.classes["@mozilla.org/peerconnection;1"] + .createInstance(dummyInterface); +} catch (e) {} + +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_a_noOp.html b/dom/media/webrtc/tests/mochitests/test_a_noOp.html new file mode 100644 index 0000000000..971f5d7666 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_a_noOp.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1264772 +--> +<head> + <title>Test for Bug 1264772</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1264772">Mozilla Bug 1264772</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1264772 **/ +// The WebRTC tests seem to have more problems with intermittents (at +// least on Android) if they run first in a test run. This is a dummy test +// to ensure that the browser is ready prior to running any actual WebRTC +// tests. +// +// Note: mochitests are run in alphabetical order, so it is not sufficient +// for this test to appear first in the manifest. +ok(true, "test passed"); + +</script> +</pre> +</body> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html new file mode 100644 index 0000000000..06ca9562ad --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796895", + title: "Basic data channel audio connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html new file mode 100644 index 0000000000..ea534ca2e7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796891", + title: "Basic data channel audio/video connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html new file mode 100644 index 0000000000..d5409986ec --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796891", + title: "Basic data channel audio/video connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{audio: true, video: true}], + [{audio: true, video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html new file mode 100644 index 0000000000..7dc22d86ad --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1016476", + title: "Basic data channel audio/video connection without bundle" + }); + +var test; +runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html new file mode 100644 index 0000000000..98e72f7a21 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796894", + title: "Basic datachannel only connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html new file mode 100644 index 0000000000..90f2d7caff --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796889", + title: "Basic data channel video connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{video: true}], [{video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html new file mode 100644 index 0000000000..e36caebab4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796895", + title: "Basic data channel audio connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION"); + test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html new file mode 100644 index 0000000000..26767e0865 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1051685", + title: "Verify bufferedAmountLowThreshold works" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.chain.insertAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS', commandsCheckLargeXfer); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html new file mode 100644 index 0000000000..6f0cbc5d3d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1284103", + title: "Test basic data channel audio connection for supported DTLS versions" + }); + + async function testDtlsVersion(options, version) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.peerconnection.dtls.version.min", version], + ["media.peerconnection.dtls.version.max", version] + ] + }); + + const test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + + await test.run(); + } + + runNetworkTest(async (options) => { + // 770 = DTLS 1.0, 771 = DTLS 1.2, 772 = DTLS 1.3 + for (var version = 770; version <= 772; version++) { + await testDtlsVersion(options, version); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html new file mode 100644 index 0000000000..d0790fb9c9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1592620", + title: "Blocklist to disable hostname obfuscation" + }); + + async function testBlocklist(options, blocklistEntry, shouldBeObfuscated) { + let test = new PeerConnectionTest(options); + addInitialDataChannel(test.chain); + + if (blocklistEntry !== null) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.peerconnection.ice.obfuscate_host_addresses.blocklist", + blocklistEntry] + ] + }); + } + + test.chain.insertAfter('PC_LOCAL_WAIT_FOR_ICE_CONNECTED', [ + async function CHECK_LOCAL_CANDIDATES() { + const stats = await test.pcLocal.getStats(); + stats.forEach(s => { + if (s.type === 'local-candidate') { + if (shouldBeObfuscated) { + ok(s.address.includes(".local"), "address should be obfuscated"); + } else { + ok(!s.address.includes(".local"), "address should not be obfuscated"); + } + } + }); + }]); + + await test.run(); + } + + runNetworkTest(async (options) => { + await SpecialPowers.pushPrefEnv({ + set: [["media.peerconnection.ice.obfuscate_host_addresses", true]] + }); + await testBlocklist(options, null, true); + await testBlocklist(options, "", true); + await testBlocklist(options, "example.com", true); + await testBlocklist(options, "mochi.test", false); + await testBlocklist(options, "example.com,mochi.test", false); + await testBlocklist(options, "*.test", false); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html new file mode 100644 index 0000000000..a6e9fa5214 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "856319", + title: "Don't offer m=application unless createDataChannel is called first" + }); + + runNetworkTest(async function () { + const pc = new RTCPeerConnection(); + + // necessary to circumvent bug 864109 + const options = { offerToReceiveAudio: true }; + + const errorCallback = generateErrorCallback(); + try { + const offer = await pc.createOffer(options); + ok(!offer.sdp.includes("m=application"), + "m=application is not contained in the SDP"); + } catch(e) { + errorCallback(e); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html new file mode 100644 index 0000000000..4498e2d23a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1218356", + title: "DataChannel stats" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.chain.remove('PC_LOCAL_CHECK_STATS'); + test.chain.remove('PC_REMOTE_CHECK_STATS'); + addInitialDataChannel(test.chain); + test.chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS"); + test.chain.insertAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS", + async function TEST_DATA_CHANNEL_STATS(test) { + const channel = test.pcLocal.dataChannels[0]; + test.pcRemote.dataChannels[0].onbufferedamountlow = () => {}; + test.pcRemote.dataChannels[0].send(`Sending Message`); + channel.onbufferedamountlow = () => {}; + const event = await new Promise( r => channel.onmessage = r); + info(`Received message: "${event.data}"`); + const report = await test.pcLocal.getStats(); + info(`Received Stats ${JSON.stringify([...report.values()], null, 2)}\n`); + const stats = [...report.values()].find(block => block.type == "data-channel"); + info(`DataChannel stats ${JSON.stringify(stats, null, 2)}`); + is(stats.label, channel.label, 'DataChannel stats has correct label'); + is(stats.protocol, channel.protocol, + 'DataChannel stats has correct protocol'); + is(stats.dataChannelIdentifier, channel.id, + 'DataChannel stats has correct dataChannelIdentifier'); + is(stats.state, channel.readyState, 'DataChannel has correct state'); + is(stats.bytesReceived, 15, 'DataChannel has correct bytesReceived'); + is(stats.bytesSent, 0, 'DataChannel has correct bytesSent'); + is(stats.messagesReceived, 1, + 'DataChannel has correct messagesReceived'); + is(stats.messagesSent, 0, 'DataChannel has correct messagesSent'); + }); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html new file mode 100644 index 0000000000..8e0db48fff --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "Test that the audio constraints that observe at the audio constraints we expect.", + bug: "1509842" +}); + +runTest(async () => { + // We need a real device to get a MediaEngine supporting constraints + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!audioDevice) { + todo(false, "No device set by framework. Try --use-test-media-devices"); + return; + } + + // Get a gUM track with the default settings, check that they are what we + // expect. + let stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + let track = stream.getAudioTracks()[0]; + let defaultSettings = track.getSettings(); + + is(defaultSettings.echoCancellation, true, + "Echo cancellation should be ON by default."); + is(defaultSettings.noiseSuppression, true, + "Noise suppression should be ON by default."); + is(defaultSettings.autoGainControl, true, + "Automatic gain control should be ON by default."); + + track.stop(); + + // This is UA-dependant, and belongs in a Mochitest, not in a WPT. + // When a gUM track has been requested with `echoCancellation` OFF, check that + // `noiseSuppression` and `autoGainControl` are off as well. + stream = + await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false}}); + track = stream.getAudioTracks()[0]; + defaultSettings = track.getSettings(); + + is(defaultSettings.echoCancellation, false, + "Echo cancellation should be OFF when requested."); + is(defaultSettings.noiseSuppression, false, + `Noise suppression should be OFF when echoCancellation is the only + constraint and is OFF.`); + is(defaultSettings.autoGainControl, false, + `Automatic gain control should be OFF when echoCancellation is the only + constraint and is OFF.`); + + track.stop(); + + // When a gUM track has been requested with `echoCancellation` OFF, check that + // `noiseSuppression` and `autoGainControl` are not OFF as well if another + // constraint has been specified. + stream = + await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false, + autoGainControl: true}}); + track = stream.getAudioTracks()[0]; + defaultSettings = track.getSettings(); + + is(defaultSettings.echoCancellation, false, + "Echo cancellation should be OFF when requested."); + is(defaultSettings.noiseSuppression, false, + `Noise suppression should be OFF when echoCancellation is OFF and another + constraint has been specified.`); + is(defaultSettings.autoGainControl, true, + "Auto gain control should be ON when requested."); + + track.stop(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html new file mode 100644 index 0000000000..48bec0006a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html @@ -0,0 +1,141 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Run enumerateDevices code", bug: "1046245" }); +/** + Tests covering enumerateDevices API and deviceId constraint. Exercise code. +*/ + +async function mustSucceedWithStream(msg, f) { + try { + const stream = await f(); + for (const track of stream.getTracks()) { + track.stop(); + } + ok(true, msg + " must succeed"); + } catch (e) { + is(e.name, null, msg + " must succeed: " + e.message); + } +} + +async function mustFailWith(msg, reason, constraint, f) { + try { + await f(); + ok(false, msg + " must fail"); + } catch(e) { + is(e.name, reason, msg + " must fail: " + e.message); + if (constraint) { + is(e.constraint, constraint, msg + " must fail w/correct constraint."); + } + } +} + +const gUM = c => navigator.mediaDevices.getUserMedia(c); + +const kinds = ["videoinput", "audioinput", "audiooutput"]; + +function validateDevice({kind, label, deviceId, groupId}) { + ok(kinds.includes(kind), "Known device kind"); + is(deviceId.length, 44, "deviceId length id as expected for Firefox"); + ok(label.length !== undefined, "Device label: " + label); + isnot(groupId, "", "groupId must be present."); +} + +runTest(async () => { + await pushPrefs(["media.navigator.streams.fake", true]); + + // Validate enumerated devices after gUM. + for (const track of (await gUM({video: true, audio: true})).getTracks()) { + track.stop(); + } + + let devices = await navigator.mediaDevices.enumerateDevices(); + ok(devices.length, "At least one device found"); + const jsoned = JSON.parse(JSON.stringify(devices)); + is(jsoned[0].kind, devices[0].kind, "kind survived serializer"); + is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer"); + for (const device of devices) { + validateDevice(device); + if (device.kind == "audiooutput") continue; + // Test deviceId constraint + let deviceId = device.deviceId; + let constraints = (device.kind == "videoinput") ? { video: { deviceId } } + : { audio: { deviceId } }; + for (const track of (await gUM(constraints)).getTracks()) { + is(typeof(track.label), "string", "Track label is a string"); + is(track.label, device.label, "Track label is the device label"); + track.stop(); + } + } + + const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc="; + + // Check deviceId failure paths for video. + + await mustSucceedWithStream("unknown plain deviceId on video", + () => gUM({ video: { deviceId: unknownId } })); + await mustSucceedWithStream("unknown plain deviceId on audio", + () => gUM({ audio: { deviceId: unknownId } })); + await mustFailWith("unknown exact deviceId on video", + "OverconstrainedError", "deviceId", + () => gUM({ video: { deviceId: { exact: unknownId } } })); + await mustFailWith("unknown exact deviceId on audio", + "OverconstrainedError", "deviceId", + () => gUM({ audio: { deviceId: { exact: unknownId } } })); + + // Check that deviceIds are stable for same origin and differ across origins. + + const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html"; + const origins = ["https://example.com", "https://test1.example.com"]; + info(window.location); + + const haveDevicesMap = new Promise(resolve => { + const map = new Map(); + window.addEventListener("message", ({origin, data}) => { + ok(origins.includes(origin), "Got message from expected origin"); + map.set(origin, JSON.parse(data)); + if (map.size < origins.length) return; + resolve(map); + }); + }); + + await Promise.all(origins.map(origin => { + const iframe = document.createElement("iframe"); + iframe.src = origin + path; + iframe.allow = "camera;microphone;speaker-selection"; + info(iframe.src); + document.documentElement.appendChild(iframe); + return new Promise(resolve => iframe.onload = resolve); + })); + let devicesMap = await haveDevicesMap; + let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o)); + + is(sameOriginDevices.length, devices.length, "same origin same devices"); + is(differentOriginDevices.length, devices.length, "cross origin same devices"); + [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d)); + + for (const device of sameOriginDevices) { + ok(devices.find(d => d.deviceId == device.deviceId), + "Same origin deviceId for " + device.label + " must match"); + } + for (const device of differentOriginDevices) { + ok(!devices.find(d => d.deviceId == device.deviceId), + "Different origin deviceId for " + device.label + " must be different"); + } + + // Check the special case of no devices found. + await pushPrefs(["media.navigator.streams.fake", false], + ["media.audio_loopback_dev", "none"], + ["media.video_loopback_dev", "none"]); + devices = await navigator.mediaDevices.enumerateDevices(); + is(devices.length, 0, "No devices"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html new file mode 100644 index 0000000000..7952bcba1b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> + <script> +"use strict"; + +createHTML({ + title: "Test labeled devices or speakers aren't exposed in enumerateDevices() after fake getUserMedia()", + bug: "1743524" +}); + +runTest(async () => { + await pushPrefs( + ["media.setsinkid.enabled", true], + // This test uses real devices because fake devices are not grouped with + // audiooutput devices. + ["media.navigator.streams.fake", false]); + const devices = navigator.mediaDevices; + { + // `fake:true` means that getUserMedia() resolves without any permission + // check, and so this should not be sufficient to expose real device info. + const stream = await devices.getUserMedia({ audio: true, fake: true }); + stream.getTracks()[0].stop(); + const list = await devices.enumerateDevices(); + const labeledDevices = list.filter(({label}) => label != ""); + is(labeledDevices.length, 0, "must be zero labeled devices after fake gUM"); + const outputDevices = list.filter(({kind}) => kind == "audiooutput"); + is(outputDevices.length, 0, "must be zero output devices after fake gUM"); + } + { + // Check without `fake:true` to verify assumptions about existing devices. + let stream; + try { + stream = await devices.getUserMedia({ audio: true }); + stream.getTracks()[0].stop(); + } catch (e) { + if (e.name == "NotFoundError" && + navigator.userAgent.includes("Mac OS X")) { + todo(false, "Expecting no real audioinput device on Mac test machines"); + return; + } + throw e; + } + { + const list = await devices.enumerateDevices(); + const audioDevices = list.filter(({kind}) => kind.includes("audio")); + ok(audioDevices.length, "have audio devices after real gUM"); + const unlabeledAudioDevices = audioDevices.filter(({label}) => !label); + is(unlabeledAudioDevices.length, 0, + "must be zero unlabeled audio devices after real gUM"); + + const outputDevices = list.filter(({kind}) => kind == "audiooutput"); + isnot(outputDevices.length, 0, "have output devices after real gUM"); + } + } +}); + </script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html new file mode 100644 index 0000000000..beea3a4f97 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<body> +<pre id="test"> +<script type="application/javascript"> +/** + Runs inside iframe in test_enumerateDevices.html. +*/ + +const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p}); +const gUM = c => navigator.mediaDevices.getUserMedia(c); + +(async () => { + await pushPrefs(["media.navigator.streams.fake", true]); + + // Validate enumerated devices after gUM. + for (const track of (await gUM({video: true, audio: true})).getTracks()) { + track.stop(); + } + + const devices = await navigator.mediaDevices.enumerateDevices(); + parent.postMessage(JSON.stringify(devices), "https://example.com:443"); + +})().catch(e => setTimeout(() => { throw e; })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html new file mode 100644 index 0000000000..f2dc2d1f65 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<body> +<pre id="test"> +<script type="application/javascript"> +/** + Runs inside iframe in test_enumerateDevices_legacy.html. +*/ + +const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p}); + +(async () => { + await pushPrefs(["media.navigator.streams.fake", true]); + + const devices = await navigator.mediaDevices.enumerateDevices(); + parent.postMessage(JSON.stringify(devices), "https://example.com:443"); + +})().catch(e => setTimeout(() => { throw e; })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html new file mode 100644 index 0000000000..c599f2b599 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html @@ -0,0 +1,147 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Run enumerateDevices code", bug: "1046245" }); +/** + This is a modified copy of test_enumerateDevices.html testing the + enumerateDevices() legacy version and deviceId constraint. +*/ + +async function mustSucceedWithStream(msg, f) { + try { + const stream = await f(); + for (const track of stream.getTracks()) { + track.stop(); + } + ok(true, msg + " must succeed"); + } catch (e) { + is(e.name, null, msg + " must succeed: " + e.message); + } +} + +async function mustFailWith(msg, reason, constraint, f) { + try { + await f(); + ok(false, msg + " must fail"); + } catch(e) { + is(e.name, reason, msg + " must fail: " + e.message); + if (constraint) { + is(e.constraint, constraint, msg + " must fail w/correct constraint."); + } + } +} + +const gUM = c => navigator.mediaDevices.getUserMedia(c); + +const kinds = ["videoinput", "audioinput", "audiooutput"]; + +function validateDevice({kind, label, deviceId, groupId}) { + ok(kinds.includes(kind), "Known device kind"); + is(deviceId.length, 44, "deviceId length id as expected for Firefox"); + ok(label.length !== undefined, "Device label: " + label); + isnot(groupId, "", "groupId must be present."); +} + +runTest(async () => { + await pushPrefs(["media.navigator.streams.fake", true], + ["media.devices.enumerate.legacy.enabled", true]); + + // Validate enumerated devices before gUM (legacy). + + let devices = await navigator.mediaDevices.enumerateDevices(); + ok(devices.length, "At least one device found"); + const jsoned = JSON.parse(JSON.stringify(devices)); + is(jsoned[0].kind, devices[0].kind, "kind survived serializer"); + is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer"); + for (const device of devices) { + validateDevice(device); + if (device.kind == "audiooutput") continue; + is(device.label, "", "Device label is empty"); + // Test deviceId constraint + let deviceId = device.deviceId; + let constraints = (device.kind == "videoinput") ? { video: { deviceId } } + : { audio: { deviceId } }; + let namedDevices; + for (const track of (await gUM(constraints)).getTracks()) { + is(typeof(track.label), "string", "Track label is a string"); + isnot(track.label.length, 0, "Track label is not empty"); + if (!namedDevices) { + namedDevices = await navigator.mediaDevices.enumerateDevices(); + } + const namedDevice = namedDevices.find(d => d.deviceId == device.deviceId); + is(track.label, namedDevice.label, "Track label is the device label"); + track.stop(); + } + } + + const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc="; + + // Check deviceId failure paths for video. + + await mustSucceedWithStream("unknown plain deviceId on video", + () => gUM({ video: { deviceId: unknownId } })); + await mustSucceedWithStream("unknown plain deviceId on audio", + () => gUM({ audio: { deviceId: unknownId } })); + await mustFailWith("unknown exact deviceId on video", + "OverconstrainedError", "deviceId", + () => gUM({ video: { deviceId: { exact: unknownId } } })); + await mustFailWith("unknown exact deviceId on audio", + "OverconstrainedError", "deviceId", + () => gUM({ audio: { deviceId: { exact: unknownId } } })); + + // Check that deviceIds are stable for same origin and differ across origins. + + const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html"; + const origins = ["https://example.com", "https://test1.example.com"]; + info(window.location); + + const haveDevicesMap = new Promise(resolve => { + const map = new Map(); + window.addEventListener("message", ({origin, data}) => { + ok(origins.includes(origin), "Got message from expected origin"); + map.set(origin, JSON.parse(data)); + if (map.size < origins.length) return; + resolve(map); + }); + }); + + await Promise.all(origins.map(origin => { + const iframe = document.createElement("iframe"); + iframe.src = origin + path; + iframe.allow = "camera;microphone;speaker-selection"; + info(iframe.src); + document.documentElement.appendChild(iframe); + return new Promise(resolve => iframe.onload = resolve); + })); + let devicesMap = await haveDevicesMap; + let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o)); + + is(sameOriginDevices.length, devices.length, "same origin same devices"); + is(differentOriginDevices.length, devices.length, "cross origin same devices"); + [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d)); + + for (const device of sameOriginDevices) { + ok(devices.find(d => d.deviceId == device.deviceId), + "Same origin deviceId for " + device.label + " must match"); + } + for (const device of differentOriginDevices) { + ok(!devices.find(d => d.deviceId == device.deviceId), + "Different origin deviceId for " + device.label + " must be different"); + } + + // Check the special case of no devices found. + await pushPrefs(["media.navigator.streams.fake", false], + ["media.audio_loopback_dev", "none"], + ["media.video_loopback_dev", "none"]); + devices = await navigator.mediaDevices.enumerateDevices(); + is(devices.length, 0, "No devices"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html new file mode 100644 index 0000000000..bf7650223f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<iframe id="iframe" srcdoc="<script> + window.enumerateDevices = () => + navigator.mediaDevices.enumerateDevices(); + </script>" + width="100%" height="50%" frameborder="1"> +</iframe> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Suspend enumerateDevices code ", bug: "1479840" }); +/** + This test covers the case that the enumerateDevices method is suspended by + navigating away the current window. In order to implement that the enumeration + is executed in an iframe which is cleared before the enumeration has been resolved +*/ + +runTest(async () => { + // Run enumerate devices and mesure the time it will take. + const start = new Date().getTime(); + try { + await iframe.contentWindow.enumerateDevices(); + } catch (e) { + info("Failed to enumerate devices, error: " + e); + } + const elapsed = new Date().getTime() - start; + + // Run again and navigate away. Expected to remain pending. + let p = iframe.contentWindow.enumerateDevices() + p.then( devices => { + ok(false, "Enumerate devices promise resolved unexpectedly, found " + devices.length + " devices."); + }) + .catch ( error => { + ok(false, "Enumerate devices promise rejected unexpectedly: " + error); + }); + iframe.srcdoc = ""; + + // Wait enough time. + try { + await timeout(p, 5 * elapsed, "timeout"); + ok(false, "Enumerate devices promise resolved unexpectedly"); + } catch (e) { + is(e.message, "timeout", "We should time out without enumerateDevices rejecting"); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html new file mode 100644 index 0000000000..7e9cd5a219 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<script src="mediaStreamPlayback.js"></script> +</head> +<body> +<script> +/* global SimpleTest SpecialPowers */ + +async function testEnumerateDevices(expectDevices) { + let devices = await navigator.mediaDevices.enumerateDevices(); + if (!expectDevices) { + SimpleTest.is(devices.length, 0, "testEnumerateDevices: No devices"); + return; + } + let cams = devices.filter((device) => device.kind == "videoinput"); + let mics = devices.filter((device) => device.kind == "audioinput"); + SimpleTest.ok((cams.length == 1) && (mics.length == 1), + "testEnumerateDevices: a microphone and a camera"); +} + +async function testGetUserMedia(expectDevices) { + const constraints = [ + {audio: true}, + {video: true}, + {audio: true, video: true}, + {video: {width: {min: 1e9}}}, // impossible + {audio: {channelCount: {exact: 1e3}}}, // impossible + ]; + for (let constraint of constraints) { + let message = "getUserMedia(" + JSON.stringify(constraint) + ")"; + try { + let stream = await navigator.mediaDevices.getUserMedia(constraint); + SimpleTest.ok(expectDevices, message + " resolved"); + if (!expectDevices) { + continue; + } + + // We only do testGetUserMedia(true) when privacy.resistFingerprinting + // is true, test if MediaStreamTrack.label is spoofed. + for (let track of stream.getTracks()) { + switch (track.kind) { + case "audio": + SimpleTest.is(track.label, "Internal Microphone", "AudioStreamTrack.label"); + break; + case "video": + SimpleTest.is(track.label, "Internal Camera", "VideoStreamTrack.label"); + break; + default: + SimpleTest.ok(false, "Unknown kind: " + track.kind); + break; + } + track.stop(); + } + } catch (e) { + if (!expectDevices) { + SimpleTest.is(e.name, "NotAllowedError", message + " throws NotAllowedError"); + } else { + SimpleTest.ok(false, message + " failed: " + e); + } + } + } +} + +async function testDevices() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["media.navigator.streams.fake", true] + ] + }); + await testEnumerateDevices(true); // should list a microphone and a camera + await testGetUserMedia(true); // should get audio and video streams +} + +async function testNoDevices() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", false], + ["media.navigator.permission.device", false], + ["media.navigator.streams.fake", false], + ["media.audio_loopback_dev", "foo"], + ["media.video_loopback_dev", "bar"] + ] + }); + await testEnumerateDevices(false); // should list nothing + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true] + ] + }); + await testEnumerateDevices(true); // should list a microphone and a camera + await testGetUserMedia(false); // should reject with NotAllowedError +} + +createHTML({ + title: "Neutralize the threat of fingerprinting of media devices API when 'privacy.resistFingerprinting' is true", + bug: "1372073" +}); + +runTest(async () => { + // Make sure enumerateDevices and getUserMedia work when + // privacy.resistFingerprinting is true. + await testDevices(); + + // Test that absence of devices can't be detected. + await testNoDevices(); +}); +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html new file mode 100644 index 0000000000..c5a9820aaa --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the pref media.cubeb.force_sample_rate</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +const WEIRD_SAMPLE_RATE = 44101; + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["media.cubeb.force_sample_rate", WEIRD_SAMPLE_RATE] +]}).then(function() { + var ac = new AudioContext(); + is(ac.sampleRate, WEIRD_SAMPLE_RATE, "Forced sample-rate set successfully."); + SimpleTest.finish(); +}); +</script> +</pre> +</body> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html new file mode 100644 index 0000000000..ae16097d07 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "MediaStreams can be garbage collected", + bug: "1407542" + }); + + let SpecialStream = SpecialPowers.wrap(MediaStream); + + async function testGC(stream, numCopies, copy) { + let startStreams = await SpecialStream.countUnderlyingStreams(); + + let copies = new Array(numCopies).fill(0).map(() => copy(stream)); + ok(await SpecialStream.countUnderlyingStreams() > startStreams, + "MediaStreamTrack constructor creates more underlying streams"); + + copies = []; + await new Promise(r => SpecialPowers.exactGC(r)); + is(await SpecialStream.countUnderlyingStreams(), startStreams, + "MediaStreamTracks should have been collected"); + } + + runTest(async () => { + let gUMStream = await getUserMedia({video: true}); + info("Testing GC of track-array constructor with cloned tracks"); + await testGC(gUMStream, 10, s => new MediaStream(s.getTracks().map(t => t.clone()))); + + info("Testing GC of empty constructor plus addTrack with cloned tracks"); + await testGC(gUMStream, 10, s => { + let s2 = new MediaStream(); + s.getTracks().forEach(t => s2.addTrack(t.clone())); + return s2; + }); + + info("Testing GC of cloned stream"); + await testGC(gUMStream, 10, s => s.clone()); + + info("Testing GC of gUM stream"); + gUMStream = null; + await new Promise(r => SpecialPowers.exactGC(r)); + is(await SpecialStream.countUnderlyingStreams(), 0, + "Original gUM stream should be collectable"); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html new file mode 100644 index 0000000000..c1a39cdd4c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<video id="testAutoplay" autoplay></video> +<script type="application/javascript"> +"use strict"; + +const video = document.getElementById("testAutoplay"); +var stream; +var otherVideoTrack; +var otherAudioTrack; + +createHTML({ + title: "MediaStream can be autoplayed in media element after going inactive and then active", + bug: "1208316" +}); + +runTest(() => getUserMedia({audio: true, video: true}).then(s => { + stream = s; + otherVideoTrack = stream.getVideoTracks()[0].clone(); + otherAudioTrack = stream.getAudioTracks()[0].clone(); + + video.srcObject = stream; + return haveEvent(video, "playing", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(!video.ended, "Video element should be playing after adding a gUM stream"); + stream.getTracks().forEach(t => t.stop()); + return haveEvent(video, "ended", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(video.ended, "Video element should be ended"); + stream.addTrack(otherVideoTrack); + return haveEvent(video, "playing", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(!video.ended, "Video element should be playing after adding a video track"); + stream.getTracks().forEach(t => t.stop()); + return haveEvent(video, "ended", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(video.ended, "Video element should be ended"); + stream.addTrack(otherAudioTrack); + return haveEvent(video, "playing", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(!video.ended, "Video element should be playing after adding a audio track"); + stream.getTracks().forEach(t => t.stop()); + return haveEvent(video, "ended", wait(5000, new Error("Timeout"))); +}) +.then(() => { + ok(video.ended, "Video element should be ended"); +})); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html new file mode 100644 index 0000000000..27dad2519f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html @@ -0,0 +1,169 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "MediaStream's addTrack() and removeTrack() with getUserMedia streams Test", + bug: "1103188" + }); + + runTest(() => Promise.resolve() + .then(() => getUserMedia({audio: true})).then(stream => + getUserMedia({video: true}).then(otherStream => { + info("Test addTrack()ing a video track to an audio-only gUM stream"); + var track = stream.getTracks()[0]; + var otherTrack = otherStream.getTracks()[0]; + + stream.addTrack(track); + checkMediaStreamContains(stream, [track], "Re-added audio"); + + stream.addTrack(otherTrack); + checkMediaStreamContains(stream, [track, otherTrack], "Added video"); + + var testElem = createMediaElement('video', 'testAddTrackAudioVideo'); + var playback = new MediaStreamPlayback(testElem, stream); + return playback.playMedia(false); + })) + .then(() => getUserMedia({video: true})).then(stream => + getUserMedia({video: true}).then(otherStream => { + info("Test addTrack()ing a video track to a video-only gUM stream"); + var track = stream.getTracks()[0]; + var otherTrack = otherStream.getTracks()[0]; + + stream.addTrack(track); + checkMediaStreamContains(stream, [track], "Re-added video"); + + stream.addTrack(otherTrack); + checkMediaStreamContains(stream, [track, otherTrack], "Added video"); + + var test = createMediaElement('video', 'testAddTrackDoubleVideo'); + var playback = new MediaStreamPlayback(test, stream); + return playback.playMedia(false); + })) + .then(() => getUserMedia({video: true})).then(stream => + getUserMedia({video: true}).then(otherStream => { + info("Test removeTrack() existing and added video tracks from a video-only gUM stream"); + var track = stream.getTracks()[0]; + var otherTrack = otherStream.getTracks()[0]; + + stream.removeTrack(otherTrack); + checkMediaStreamContains(stream, [track], "Removed non-existing video"); + + stream.addTrack(otherTrack); + checkMediaStreamContains(stream, [track, otherTrack], "Added video"); + + stream.removeTrack(otherTrack); + checkMediaStreamContains(stream, [track], "Removed added video"); + + stream.removeTrack(otherTrack); + checkMediaStreamContains(stream, [track], "Re-removed added video"); + + stream.removeTrack(track); + checkMediaStreamContains(stream, [], "Removed original video"); + + var elem = createMediaElement('video', 'testRemoveAllVideo'); + var loadeddata = false; + elem.onloadeddata = () => { loadeddata = true; elem.onloadeddata = null; }; + elem.srcObject = stream; + elem.play(); + return wait(500).then(() => { + ok(!loadeddata, "Stream without tracks shall not raise 'loadeddata' on media element"); + elem.pause(); + elem.srcObject = null; + }) + .then(() => { + stream.addTrack(track); + checkMediaStreamContains(stream, [track], "Re-added added-then-removed track"); + var playback = new MediaStreamPlayback(elem, stream); + return playback.playMedia(false); + }) + .then(() => otherTrack.stop()); + })) + .then(() => getUserMedia({ audio: true })).then(audioStream => + getUserMedia({ video: true }).then(videoStream => { + info("Test adding track and removing the original"); + var audioTrack = audioStream.getTracks()[0]; + var videoTrack = videoStream.getTracks()[0]; + videoStream.removeTrack(videoTrack); + audioStream.addTrack(videoTrack); + + checkMediaStreamContains(videoStream, [], "1, Removed original track"); + checkMediaStreamContains(audioStream, [audioTrack, videoTrack], + "2, Added external track"); + + var elem = createMediaElement('video', 'testAddRemoveOriginalTrackVideo'); + var playback = new MediaStreamPlayback(elem, audioStream); + return playback.playMedia(false); + })) + .then(() => getUserMedia({ audio: true, video: true })).then(stream => { + info("Test removing stopped tracks"); + stream.getTracks().forEach(t => { + t.stop(); + stream.removeTrack(t); + }); + checkMediaStreamContains(stream, [], "Removed stopped tracks"); + }) + .then(() => { + var ac = new AudioContext(); + + var osc1k = createOscillatorStream(ac, 1000); + var audioTrack1k = osc1k.getTracks()[0]; + + var osc5k = createOscillatorStream(ac, 5000); + var audioTrack5k = osc5k.getTracks()[0]; + + var osc10k = createOscillatorStream(ac, 10000); + var audioTrack10k = osc10k.getTracks()[0]; + + var stream = osc1k; + return Promise.resolve().then(() => { + info("Analysing audio output with original 1k track"); + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] < 50); + }).then(() => { + info("Analysing audio output with removed original 1k track and added 5k track"); + stream.removeTrack(audioTrack1k); + stream.addTrack(audioTrack5k); + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50); + }).then(() => { + info("Analysing audio output with removed 5k track and added 10k track"); + stream.removeTrack(audioTrack5k); + stream.addTrack(audioTrack10k); + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] > 200); + }).then(() => { + info("Analysing audio output with re-added 1k, 5k and added 10k tracks"); + stream.addTrack(audioTrack1k); + stream.addTrack(audioTrack5k); + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(7500)] < 50 && + array[analyser.binIndexForFrequency(10000)] > 200 && + array[analyser.binIndexForFrequency(11000)] < 50); + }); + })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html new file mode 100644 index 0000000000..8f4eb2a9db --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "MediaStream's 'addtrack' and 'removetrack' events shouldn't fire on manual operations", + bug: "1208328" +}); + +var spinEventLoop = () => new Promise(r => setTimeout(r, 0)); + +var stream; +var clone; +var newStream; +var tracks = []; + +var addTrack = track => { + info("Adding track " + track.id); + stream.addTrack(track); +}; +var removeTrack = track => { + info("Removing track " + track.id); + stream.removeTrack(track); +}; +var stopTrack = track => { + if (track.readyState == "live") { + info("Stopping track " + track.id); + } + track.stop(); +}; + +runTest(() => getUserMedia({audio: true, video: true}) + .then(s => { + stream = s; + clone = s.clone(); + stream.addEventListener("addtrack", function onAddtrack(event) { + ok(false, "addtrack fired unexpectedly for track " + event.track.id); + }); + stream.addEventListener("removetrack", function onRemovetrack(event) { + ok(false, "removetrack fired unexpectedly for track " + event.track.id); + }); + + return getUserMedia({audio: true, video: true}); + }) + .then(s => { + newStream = s; + info("Stopping an original track"); + stopTrack(stream.getTracks()[0]); + + return spinEventLoop(); + }) + .then(() => { + info("Removing original tracks"); + stream.getTracks().forEach(t => { + stream.removeTrack(t); + return tracks.push(t); + }); + + return spinEventLoop(); + }) + .then(() => { + info("Adding other gUM tracks"); + newStream.getTracks().forEach(t => addTrack(t)) + + return spinEventLoop(); + }) + .then(() => { + info("Adding cloned tracks"); + let clone = stream.clone(); + clone.getTracks().forEach(t => addTrack(t)); + + return spinEventLoop(); + }) + .then(() => { + info("Removing a clone"); + removeTrack(clone.getTracks()[0]); + + return spinEventLoop(); + }) + .then(() => { + info("Stopping clones"); + clone.getTracks().forEach(t => stopTrack(t)); + + return spinEventLoop(); + }) + .then(() => { + info("Stopping originals"); + stream.getTracks().forEach(t => stopTrack(t)); + tracks.forEach(t => stopTrack(t)); + + return spinEventLoop(); + }) + .then(() => { + info("Removing remaining tracks"); + stream.getTracks().forEach(t => removeTrack(t)); + + return spinEventLoop(); + }) + .then(() => { + // Test MediaStreamTrackEvent required args here. + mustThrowWith("MediaStreamTrackEvent without required args", + "TypeError", () => new MediaStreamTrackEvent("addtrack", {})); + })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html new file mode 100644 index 0000000000..95e766a9e3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioCapture </title> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script> + +(async () => { + await createHTML({ + bug: "1156472", + title: "Test AudioCapture with regular HTMLMediaElement, AudioContext, " + + "and HTMLMediaElement playing a MediaStream", + visible: true + }); + + await runTestWhenReady(async () => { + // Reduce noise for when --use-test-media-devices is not in use. + await SpecialPowers.pushPrefEnv({ set: [[ "media.volume_scale", "0.0" ]] }); + /** + * Get two HTMLMediaElements: + * - One playing a sine tone from a blob (of an opus file created on the fly) + * - One being the output for an AudioContext's OscillatorNode, connected to + * a MediaSourceDestinationNode. + * + * Also, use the AudioContext playing through its AudioDestinationNode another + * tone, using another OscillatorNode. + * + * Capture the output of the document, feed that back into the AudioContext, + * with an AnalyserNode, and check the frequency content to make sure we + * have recorded the three sources. + * + * The three sine tones have frequencies far apart from each other, so that we + * can check that the spectrum of the capture stream contains three + * components with a high magnitude. + */ + const decodedTone = createMediaElement("audio", "decodedTone"); + decodedTone.muted = false; + const decodedToneMuted = createMediaElement("audio", "decodedToneMuted"); + const acTone = createMediaElement("audio", "audioContextTone"); + acTone.muted = false; + const acToneMuted = createMediaElement("audio", "audioContextToneMuted"); + const micTone = createMediaElement("audio", "audioContextTone"); + micTone.muted = false; + const ac = new AudioContext(); + const constraints = {audio: {mediaSource: "audioCapture"}}; + const stream = await getUserMedia(constraints); + const analyser = new AudioStreamAnalyser(ac, stream); + + const osc200 = ac.createOscillator(); + osc200.frequency.value = analyser.frequencyForBinIndex(200); + const osc250 = ac.createOscillator(); + osc250.frequency.value = analyser.frequencyForBinIndex(250); + const oscThroughAudioDestinationNode = ac.createOscillator(); + oscThroughAudioDestinationNode.frequency.value = + analyser.frequencyForBinIndex(100); + const msDest200 = ac.createMediaStreamDestination(); + const msDest250 = ac.createMediaStreamDestination(); + + osc200.connect(msDest200); + osc250.connect(msDest250); + oscThroughAudioDestinationNode.connect(ac.destination); + + acTone.srcObject = msDest200.stream; + acToneMuted.srcObject = msDest250.stream; + + // The fake microphone uses signed 16-bit audio data: + await SpecialPowers.pushPrefEnv({ set: [ + [ + "media.navigator.audio.fake_frequency", + analyser.frequencyForBinIndex(300) + ] + ]}); + const micStream = await navigator.mediaDevices.getUserMedia({ + 'audio': true, 'fake': true + }); + micTone.srcObject = micStream; + + decodedTone.src = "/tests/dom/media/test/sin-441-1s-44100.flac"; + decodedToneMuted.src = "/tests/dom/media/test/sin-441-1s-44100.flac"; + decodedTone.preservesPitch = false; + decodedToneMuted.preservesPitch = false; + decodedTone.playbackRate = analyser.frequencyForBinIndex(20) / 441; + ok(decodedTone.playbackRate >= 1/16, "within firefox min playback rate"); + decodedToneMuted.playbackRate = analyser.frequencyForBinIndex(40) / 441; + ok(decodedToneMuted.playbackRate <= 16, "within firefox max playback rate"); + osc200.start(); + osc250.start(); + oscThroughAudioDestinationNode.start(); + decodedTone.loop = true; + decodedToneMuted.loop = true; + decodedTone.play(); + decodedToneMuted.play(); + acTone.play(); + acToneMuted.play(); + + let frequencyBytes; + try { + analyser.enableDebugCanvas(); + await analyser.waitForAnalysisSuccess(array => { + // We want to find three frequency components here. + // Frequencies are logarithmic. Also make sure we have low + // energy in between, not just a flat white noise. + if (array[10] == 0 && + array[20] == 255 && + array[40] == 0 && + array[70] == 0 && + array[100] == 255 && + array[150] == 0 && + array[200] == 255 && + array[300] > 240) { + frequencyBytes = new Uint8Array(array); + return true; + } + return false; + }); + } finally { + await ac.close(); + for (let t of [...stream.getTracks(), ...micStream.getTracks()]) { + t.stop(); + } + } + todo(frequencyBytes[250] < 50, "muted MediaStream srcObject - bug 1864067"); + }); +})(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html new file mode 100644 index 0000000000..162e83063a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "Test that microphone getSettings report correct settings after applyConstraints", + bug: "1447982", +}); + +function testTrackAgainstAudioConstraints(track, audioConstraints) { + let constraints = track.getConstraints(); + is(constraints.autoGainControl, audioConstraints.autoGainControl, + "Should report correct autoGainControl constraint"); + is(constraints.echoCancellation, audioConstraints.echoCancellation, + "Should report correct echoCancellation constraint"); + is(constraints.noiseSuppression, audioConstraints.noiseSuppression, + "Should report correct noiseSuppression constraint"); + + let settings = track.getSettings(); + is(settings.autoGainControl, audioConstraints.autoGainControl, + "Should report correct autoGainControl setting"); + is(settings.echoCancellation, audioConstraints.echoCancellation, + "Should report correct echoCancellation setting"); + is(settings.noiseSuppression, audioConstraints.noiseSuppression, + "Should report correct noiseSuppression setting"); +} + +async function testAudioConstraints(track, audioConstraints) { + // We applyConstraints() first and do a fresh gUM later, to avoid + // testing multiple concurrent captures at different settings. + + info(`Testing applying constraints ${JSON.stringify(audioConstraints)} ` + + `to track with settings ${JSON.stringify(track.getSettings())}`); + await track.applyConstraints(audioConstraints); + testTrackAgainstAudioConstraints(track, audioConstraints); + + info("Testing fresh gUM request with audio constraints " + + JSON.stringify(audioConstraints)); + let stream = await getUserMedia({audio: audioConstraints}); + testTrackAgainstAudioConstraints(stream.getTracks()[0], audioConstraints); + stream.getTracks().forEach(t => t.stop()); +} + +runTest(async () => { + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!audioDevice) { + ok(false, "No device set by framework. Try --use-test-media-devices"); + return; + } + + let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + is(supportedConstraints.autoGainControl, true, + "autoGainControl constraint should be supported"); + is(supportedConstraints.echoCancellation, true, + "echoCancellation constraint should be supported"); + is(supportedConstraints.noiseSuppression, true, + "noiseSuppression constraint should be supported"); + + let egn = (e, g, n) => ({ + echoCancellation: e, + autoGainControl: g, + noiseSuppression: n + }); + + let stream = await getUserMedia({ + audio: egn(true, true, true), + }); + let track = stream.getTracks()[0]; + let audioConstraintsToTest = [ + egn(false, true, true), + egn(true, false, true), + egn(true, true, false), + egn(false, false, true), + egn(false, true, false), + egn(true, false, false), + egn(false, false, false), + egn(true, true, true), + ]; + for (let audioConstraints of audioConstraintsToTest) { + await testAudioConstraints(track, audioConstraints); + } + track.stop(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html new file mode 100644 index 0000000000..ae83aa746b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "getUserMedia in multiple iframes with different constraints", + bug: "1404977" +}); +/** + * Verify that we can successfully call getUserMedia for the same device in + * multiple iframes concurrently. This is checked by creating a number of + * iframes and performing a separate getUserMedia call in each. We verify the + * stream returned by that call has the same constraints as requested both + * immediately after the call and after all gUM calls have been made. The test + * then verifies the streams can be played. + */ +runTest(async function() { + // Compare constraints and return a string with the differences in + // echoCancellation, autoGainControl, and noiseSuppression. The string + // will be empty if there are no differences. + function getConstraintDifferenceString(constraints, otherConstraints) { + let diffString = ""; + if (constraints.echoCancellation != otherConstraints.echoCancellation) { + diffString += "echoCancellation different: " + + `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `; + } + if (constraints.autoGainControl != otherConstraints.autoGainControl) { + diffString += "autoGainControl different: " + + `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `; + } + if (constraints.noiseSuppression != otherConstraints.noiseSuppression) { + diffString += "noiseSuppression different: " + + `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `; + } + // Replace trailing comma and space if any + return diffString.replace(/, $/, ""); + } + + // We need a real device to get a MediaEngine supporting constraints + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!audioDevice) { + todo(false, "No device set by framework. Try --use-test-media-devices"); + return; + } + + let egn = (e, g, n) => ({ + echoCancellation: e, + autoGainControl: g, + noiseSuppression: n + }); + + let allConstraintCombinations = [ + egn(false, false, false), + egn(true, false, false), + egn(false, true, false), + egn(false, false, true), + egn(true, true, false), + egn(true, false, true), + egn(false, true, true), + egn(true, true, true), + ]; + + // TODO: We would like to be able to perform an arbitrary number of gUM calls + // at once, but issues with pulse and audio IPC mean on some systems we're + // limited to as few as 2 concurrent calls. To avoid issues we chunk test runs + // to only two calls at a time. The while, splice and GC lines can be removed, + // the extra scope removed and allConstraintCombinations can be renamed to + // constraintCombinations once this issue is resolved. See bug 1480489. + while (allConstraintCombinations.length) { + { + let constraintCombinations = allConstraintCombinations.splice(0, 2); + // Array to store objects that associate information used in our test such as + // constraints, iframes, gum streams, and various promises. + let testCases = []; + + for (let constraints of constraintCombinations) { + let testCase = {requestedConstraints: constraints}; + // Provide an id for logging, labeling related elements. + testCase.id = `testCase.` + + `e=${constraints.echoCancellation}.` + + `g=${constraints.noiseSuppression}.` + + `n=${constraints.noiseSuppression}`; + testCases.push(testCase); + testCase.iframe = document.createElement("iframe"); + testCase.iframeLoadedPromise = new Promise((resolve, reject) => { + testCase.iframe.onload = () => { resolve(); }; + }); + document.body.appendChild(testCase.iframe); + } + is(testCases.length, + constraintCombinations.length, + "Should have created a testcase for each constraint"); + + // Wait for all iframes to be loaded + await Promise.all(testCases.map(tc => tc.iframeLoadedPromise)); + + // One by one see if we can grab a gUM stream per iframe + for (let testCase of testCases) { + // Use normal gUM rather than our test helper as the test harness was + // not made to be used inside iframes. + testCase.gumStream = + await testCase.iframe.contentWindow.navigator.mediaDevices.getUserMedia({audio: testCase.requestedConstraints}) + .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`)); + let differenceString = getConstraintDifferenceString( + testCase.requestedConstraints, + testCase.gumStream.getAudioTracks()[0].getSettings()); + ok(!differenceString, + `gUM stream for ${testCase.id} should have the same constraints as were ` + + `requested from gUM. Differences: ${differenceString}`); + } + + // Once all streams are collected, make sure the constraints haven't been + // mutated by another gUM call. + for (let testCase of testCases) { + let differenceString = getConstraintDifferenceString( + testCase.requestedConstraints, + testCase.gumStream.getAudioTracks()[0].getSettings()); + ok(!differenceString, + `gUM stream for ${testCase.id} should not have had constraints altered after ` + + `all gUM calls are done. Differences: ${differenceString}`); + } + + // We do not currently have tests to verify the behaviour of the different + // constraints. Once we do we should do further verification here. See + // bug 1406372, bug 1406376, and bug 1406377. + + for (let testCase of testCases) { + let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`); + let playback = new MediaStreamPlayback(testAudio, testCase.gumStream); + await playback.playMediaWithoutStoppingTracks(false); + } + + // Stop the tracks for each stream, we left them running above via + // playMediaWithoutStoppingTracks to make sure they can play concurrently. + for (let testCase of testCases) { + testCase.gumStream.getTracks().map(t => t.stop()); + document.body.removeChild(testCase.iframe); + } + } + await new Promise(r => SpecialPowers.exactGC(r)); + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html new file mode 100644 index 0000000000..f5b5e784ea --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "getUserMedia multiple times, concurrently, and with different constraints", + bug: "1404977" +}); +/** + * Verify that we can successfully call getUserMedia multiple times for the + * same device, concurrently. This is checked by calling getUserMedia a number + * of times with different constraints. We verify that the stream returned by + * that call has the same constraints as requested both immediately after the + * call and after all gUM calls have been made. The test then verifies the + * streams can be played. + */ +runTest(async function() { + // Compare constraints and return a string with the differences in + // echoCancellation, autoGainControl, and noiseSuppression. The string + // will be empty if there are no differences. + function getConstraintDifferenceString(constraints, otherConstraints) { + let diffString = ""; + if (constraints.echoCancellation != otherConstraints.echoCancellation) { + diffString += "echoCancellation different: " + + `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `; + } + if (constraints.autoGainControl != otherConstraints.autoGainControl) { + diffString += "autoGainControl different: " + + `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `; + } + if (constraints.noiseSuppression != otherConstraints.noiseSuppression) { + diffString += "noiseSuppression different: " + + `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `; + } + // Replace trailing comma and space if any + return diffString.replace(/, $/, ""); + } + + // We need a real device to get a MediaEngine supporting constraints + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!audioDevice) { + todo(false, "No device set by framework. Try --use-test-media-devices"); + return; + } + + let egn = (e, g, n) => ({ + echoCancellation: e, + autoGainControl: g, + noiseSuppression: n + }); + + let constraintCombinations = [ + egn(false, false, false), + egn(true, false, false), + egn(false, true, false), + egn(false, false, true), + egn(true, true, false), + egn(true, false, true), + egn(false, true, true), + egn(true, true, true), + ]; + + // Array to store objects that associate information used in our test such as + // constraints, gum streams, and various promises. + let testCases = []; + + for (let constraints of constraintCombinations) { + let testCase = {requestedConstraints: constraints}; + // Provide an id for logging, labeling related elements. + testCase.id = `testCase.` + + `e=${constraints.echoCancellation}.` + + `g=${constraints.noiseSuppression}.` + + `n=${constraints.noiseSuppression}`; + testCases.push(testCase); + testCase.gumStream = + await getUserMedia({audio: testCase.requestedConstraints}) + .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`)); + let differenceString = getConstraintDifferenceString( + testCase.requestedConstraints, + testCase.gumStream.getAudioTracks()[0].getSettings()); + ok(!differenceString, + `gUM stream for ${testCase.id} should have the same constraints as were ` + + `requested from gUM. Differences: ${differenceString}`); + } + is(testCases.length, + constraintCombinations.length, + "Should have a stream for each constraint"); + + // Once all streams are collected, make sure the constraints haven't been + // mutated by another gUM call. + for (let testCase of testCases) { + let differenceString = getConstraintDifferenceString( + testCase.requestedConstraints, + testCase.gumStream.getAudioTracks()[0].getSettings()); + ok(!differenceString, + `gUM stream for ${testCase.id} should not have had constraints altered after ` + + `all gUM calls are done. Differences: ${differenceString}`); + } + + // We do not currently have tests to verify the behaviour of the different + // constraints. Once we do we should do further verificaiton here. See + // bug 1406372, bug 1406376, and bug 1406377. + + for (let testCase of testCases) { + let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`); + let playback = new MediaStreamPlayback(testAudio, testCase.gumStream); + await playback.playMediaWithoutStoppingTracks(false); + } + + // Stop the tracks for each stream, we left them running above via + // playMediaWithoutStoppingTracks to make sure they can play concurrently. + for (let testCase of testCases) { + testCase.gumStream.getTracks().map(t => t.stop()); + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html new file mode 100644 index 0000000000..b4775b4244 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ title: "getUserMedia Basic Audio Test", bug: "781534" }); + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for an audio MediaStream on an audio HTMLMediaElement. + */ + runTest(function () { + var testAudio = createMediaElement('audio', 'testAudio'); + var constraints = {audio: true}; + + return getUserMedia(constraints).then(stream => { + var playback = new MediaStreamPlayback(testAudio, stream); + return playback.playMedia(false); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html new file mode 100644 index 0000000000..dedd5e42c3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html @@ -0,0 +1,102 @@ +<!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. + */ + runTest(async () => { + if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) { + todo(false, "No loopback device set by framework. Try --use-test-media-devices"); + return; + } + + // Start a tone so that the gUM call will record something. + const audioContext = new AudioContext(); + let tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + // At this point a tone has been instantiated on frequency + // TEST_AUDIO_FREQ (1 kHz). Verify that a tone is detected on that + // frequency. + info("Capturing at default frequency"); + const stream = await getUserMedia({audio: true}); + + try { + const 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 tone. + // Verify that a tone is detected on the new frequency (800 Hz). + info("Change loopback tone frequency"); + tone.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"); + tone.changeFrequency(TEST_AUDIO_FREQ); + const 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"); + tone.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; + }); + } finally { + for (let t of stream.getTracks()) { + t.stop(); + } + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html new file mode 100644 index 0000000000..59d20508e5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html @@ -0,0 +1,261 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Screenshare Test", + bug: "1211656", + visible: true, + }); + + const {AppConstants} = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + // Since the MacOS backend captures in the wrong rgb color profile we need + // large thresholds there, and they vary greatly by color. We define + // thresholds per platform and color here to still allow the test to run. + // Since the colors used (red, green, blue, white) consist only of "pure" + // components (0 or 255 for each component), the high thresholds on Mac will + // still be able to catch an error where the image is corrupt, or if frames + // don't flow. + const thresholds = { + // macos: captures in the display rgb color profile, which we treat as + // sRGB, which is most likely wrong. These thresholds are needed currently + // in CI. See bug 1827606. + "macosx": { "red": 120, "green": 135, "blue": 35, "white": 10 }, + // windows: rounding errors in 1) conversion to I420 (the capture), + // 2) downscaling, 3) conversion to RGB (for rendering). + "win": { "red": 5, "green": 5, "blue": 10, "white": 5 }, + // linux: rounding errors in 1) conversion to I420 (the capture), + // 2) downscaling, 3) conversion to RGB (for rendering). + "linux": { "red": 5, "green": 5, "blue": 10, "white": 5 }, + // android: we don't have a screen capture backend. + "android": { "red": 0, "green": 0, "blue": 0, "white": 0 }, + // other: here just because it's supported by AppConstants.platform. + "other": { "red": 0, "green": 0, "blue": 0, "white": 0 }, + }; + + const verifyScreenshare = + async (video, helper, upleft, upright, downleft, downright) => { + if (video.readyState < video.HAVE_CURRENT_DATA) { + info("Waiting for data"); + await new Promise(r => video.onloadeddata = r); + } + + // We assume video size will not change. Offsets help to account for a + // square fullscreen-canvas, while the screen is rectangular. + const offsetX = Math.max(0, video.videoWidth - video.videoHeight) / 2; + const offsetY = Math.max(0, video.videoHeight - video.videoWidth) / 2; + + const verifyAround = async (internalX, internalY, color) => { + // Pick a couple of samples around a coordinate to check for a color. + // We check multiple rows and columns, to avoid most artifact issues. + let areaSamples = [ + {dx: 0, dy: 0}, + {dx: 1, dy: 3}, + {dx: 8, dy: 5}, + ]; + const threshold = thresholds[AppConstants.platform][color.name]; + for (let {dx, dy} of areaSamples) { + const x = offsetX + dx + internalX; + const y = offsetY + dy + internalY; + info(`Checking pixel (${[x,y]}) of total resolution ` + + `${video.videoWidth}x${video.videoHeight} against ${color.name}.`); + let lastPixel = [-1, -1, -1, -1]; + await helper.waitForPixel(video, px => { + lastPixel = Array.from(px); + return helper.isPixel(px, color, threshold); + }, { + offsetX: x, + offsetY: y, + cancel: wait(30000).then(_ => + new Error(`Checking ${[x,y]} against ${color.name} timed out. ` + + `Got [${lastPixel}]. Threshold ${threshold}.`)), + }); + ok(true, `Pixel (${[x,y]}) passed. Got [${lastPixel}].`); + } + }; + + const screenSizeSq = Math.min(video.videoWidth, video.videoHeight); + + info("Waiting for upper left quadrant to become " + upleft.name); + await verifyAround(screenSizeSq / 4, screenSizeSq / 4, upleft); + + info("Waiting for upper right quadrant to become " + upright.name); + await verifyAround(screenSizeSq * 3 / 4, screenSizeSq / 4, upright); + + info("Waiting for lower left quadrant to become " + downleft.name); + await verifyAround(screenSizeSq / 4, screenSizeSq * 3 / 4, downleft); + + info("Waiting for lower right quadrant to become " + downright.name); + await verifyAround(screenSizeSq * 3 / 4, screenSizeSq * 3 / 4, downright); + }; + + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for a screenshare MediaStream on a video HTMLMediaElement. + */ + runTest(async function () { + await pushPrefs( + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ["full-screen-api.transition-duration.enter", "0 0"], + ["full-screen-api.transition-duration.leave", "0 0"], + ); + + // Improve real estate for screenshots + const test = document.getElementById("test"); + test.setAttribute("style", "height:0;margin:0;"); + const display = document.getElementById("display"); + display.setAttribute("style", "margin:0;"); + const testVideo = createMediaElement('video', 'testVideo'); + testVideo.removeAttribute("width"); + testVideo.removeAttribute("height"); + testVideo.setAttribute("style", "max-height:240px;"); + + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = 20; + document.getElementById("content").appendChild(canvas); + const draw = ([upleft, upright, downleft, downright]) => { + helper.drawColor(canvas, helper[upleft], {offsetX: 0, offsetY: 0}); + helper.drawColor(canvas, helper[upright], {offsetX: 10, offsetY: 0}); + helper.drawColor(canvas, helper[downleft], {offsetX: 0, offsetY: 10}); + helper.drawColor(canvas, helper[downright], {offsetX: 10, offsetY: 10}); + }; + const helper = new CaptureStreamTestHelper2D(1, 1); + + const doVerify = async (stream, [upleft, upright, downleft, downright]) => { + // Reset from potential earlier verification runs. + testVideo.srcObject = null; + const playback = new MediaStreamPlayback(testVideo, stream); + playback.startMedia(); + await playback.verifyPlaying(); + const settings = stream.getTracks()[0].getSettings(); + is(settings.width, testVideo.videoWidth, + "Width setting should match video width"); + is(settings.height, testVideo.videoHeight, + "Height setting should match video height"); + await SpecialPowers.wrap(canvas).requestFullscreen(); + try { + await verifyScreenshare(testVideo, helper, helper[upleft], helper[upright], + helper[downleft], helper[downright]); + } finally { + await playback.stopTracksForStreamInMediaPlayback(); + await SpecialPowers.wrap(document).exitFullscreen(); + // We wait a bit extra here to make sure we have completely left + // fullscreen when the --screenshot-on-fail screenshot is captured. + await wait(300); + } + }; + + info("Testing screenshare without constraints"); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let stream = await getUserMedia({video: {mediaSource: "screen"}}); + let settings = stream.getTracks()[0].getSettings(); + ok(settings.width <= 8192, + `Width setting ${settings.width} should be set after gUM (or 0 per bug 1453247)`); + ok(settings.height <= 8192, + `Height setting ${settings.height} should be set after gUM (or 0 per bug 1453247)`); + let colors = ["red", "blue", "green", "white"]; + draw(colors); + await doVerify(stream, colors); + const screenWidth = testVideo.videoWidth; + const screenHeight = testVideo.videoHeight; + + info("Testing screenshare with size and framerate constraints"); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + for (const track of stream.getTracks()) { + track.stop(); + } + stream = await getUserMedia({ + video: { + mediaSource: 'screen', + width: { + min: '10', + max: '100' + }, + height: { + min: '10', + max: '100' + }, + frameRate: { + min: '10', + max: '15' + }, + }, + }); + settings = stream.getTracks()[0].getSettings(); + ok(settings.width == 0 || (settings.width >= 10 && settings.width <= 100), + `Width setting ${settings.width} should be correct after gUM (or 0 per bug 1453247)`); + ok(settings.height == 0 || (settings.height >= 10 && settings.height <= 100), + `Height setting ${settings.height} should be correct after gUM (or 0 per bug 1453247)`); + colors = ["green", "red", "white", "blue"]; + draw(colors); + const streamClone = stream.clone(); + await doVerify(streamClone, colors); + settings = stream.getTracks()[0].getSettings(); + ok(settings.width >= 10 && settings.width <= 100, + `Width setting ${settings.width} should be within constraints`); + ok(settings.height >= 10 && settings.height <= 100, + `Height setting ${settings.height} should be within constraints`); + is(settings.width, testVideo.videoWidth, + "Width setting should match video width"); + is(settings.height, testVideo.videoHeight, + "Height setting should match video height"); + let expectedHeight = (screenHeight * settings.width) / screenWidth; + ok(Math.abs(expectedHeight - settings.height) <= 1, + "Aspect ratio after constrained gUM should be close enough"); + + info("Testing modifying screenshare with applyConstraints"); + testVideo.srcObject = stream; + testVideo.play(); + await new Promise(r => testVideo.onloadeddata = r); + const resize = haveEvent( + testVideo, "resize", wait(5000, new Error("Timeout waiting for resize"))); + await stream.getVideoTracks()[0].applyConstraints({ + mediaSource: 'screen', + width: 200, + height: 200, + frameRate: { + min: '5', + max: '10' + } + }); + // getSettings() should report correct size as soon as applyConstraints() + // resolves - bug 1453259. Until fixed, check that we at least report + // something sane. + const newSettings = stream.getTracks()[0].getSettings(); + ok(newSettings.width > settings.width && newSettings.width < screenWidth, + `Width setting ${newSettings.width} should have increased after applyConstraints`); + ok(newSettings.height > settings.height && newSettings.height < screenHeight, + `Height setting ${newSettings.height} should have increased after applyConstraints`); + await resize; + settings = stream.getTracks()[0].getSettings(); + ok(settings.width > 100 && settings.width < screenWidth, + `Width setting ${settings.width} should have increased after first frame after applyConstraints`); + ok(settings.height > 100 && settings.height < screenHeight, + `Height setting ${settings.height} should have increased after first frame after applyConstraints`); + is(settings.width, testVideo.videoWidth, + "Width setting should match video width"); + is(settings.height, testVideo.videoHeight, + "Height setting should match video height"); + expectedHeight = (screenHeight * settings.width) / screenWidth; + ok(Math.abs(expectedHeight - settings.height) <= 1, + "Aspect ratio after applying constraints should be close enough"); + colors = ["white", "green", "blue", "red"]; + draw(colors); + await doVerify(stream, colors); + for (const track of [...stream.getTracks(), ...streamClone.getTracks()]) { + track.stop(); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html new file mode 100644 index 0000000000..635cf387d4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Tabshare Test", + bug: "1193075" + }); + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for a tabshare MediaStream on a video HTMLMediaElement. + * + * Additionally, exercise applyConstraints code for tabshare viewport offset. + */ + runTest(function () { + var testVideo = createMediaElement('video', 'testVideo'); + + return Promise.resolve() + .then(() => pushPrefs(["media.getusermedia.browser.enabled", true])) + .then(() => { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + return getUserMedia({ + video: { mediaSource: "browser", + scrollWithPage: true }, + }); + }) + .then(stream => { + var playback = new MediaStreamPlayback(testVideo, stream); + return playback.playMedia(false); + }) + .then(() => getUserMedia({ + video: { + mediaSource: "browser", + viewportOffsetX: 0, + viewportOffsetY: 0, + viewportWidth: 100, + viewportHeight: 100 + }, + })) + .then(stream => { + var playback = new MediaStreamPlayback(testVideo, stream); + playback.startMedia(false); + return playback.verifyPlaying() + .then(() => Promise.all([ + () => testVideo.srcObject.getVideoTracks()[0].applyConstraints({ + mediaSource: "browser", + viewportOffsetX: 10, + viewportOffsetY: 50, + viewportWidth: 90, + viewportHeight: 50 + }), + () => listenUntil(testVideo, "resize", () => true) + ])) + .then(() => playback.verifyPlaying()) // still playing + .then(() => playback.stopTracksForStreamInMediaPlayback()) + .then(() => playback.detachFromMediaElement()); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html new file mode 100644 index 0000000000..786d9f2e4b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Video Test", + bug: "781534" + }); + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for an video MediaStream on a video HTMLMediaElement. + */ + runTest(function () { + var testVideo = createMediaElement('video', 'testVideo'); + var constraints = {video: true}; + + return getUserMedia(constraints).then(stream => { + var playback = new MediaStreamPlayback(testVideo, stream); + return playback.playMedia(false); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html new file mode 100644 index 0000000000..5218bf7301 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Video & Audio Test", + bug: "781534" + }); + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for a video and audio MediaStream on a video HTMLMediaElement. + */ + runTest(function () { + var testVideoAudio = createMediaElement('video', 'testVideoAudio'); + var constraints = {video: true, audio: true}; + + return getUserMedia(constraints).then(stream => { + var playback = new MediaStreamPlayback(testVideoAudio, stream); + return playback.playMedia(false); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html new file mode 100644 index 0000000000..fbab1b4357 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Video shall receive 'loadedmetadata' without play()ing", + bug: "1149494" + }); + /** + * Run a test to verify that we will always get 'loadedmetadata' from a video + * HTMLMediaElement playing a gUM MediaStream. + */ + runTest(() => { + var testVideo = createMediaElement('video', 'testVideo'); + var constraints = {video: true}; + + return getUserMedia(constraints).then(stream => { + var playback = new MediaStreamPlayback(testVideo, stream); + var video = playback.mediaElement; + + video.srcObject = stream; + return new Promise(resolve => { + ok(playback.mediaElement.paused, + "Media element should be paused before play()ing"); + video.addEventListener('loadedmetadata', function() { + ok(video.videoWidth > 0, "Expected nonzero video width"); + ok(video.videoHeight > 0, "Expected nonzero video width"); + resolve(); + }); + }) + .then(() => stream.getTracks().forEach(t => t.stop())); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html new file mode 100644 index 0000000000..7b27944bdc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia Basic Windowshare Test", + bug: "1038926" + }); + /** + * Run a test to verify that we can complete a start and stop media playback + * cycle for an screenshare MediaStream on a video HTMLMediaElement. + */ + runTest(async function () { + const testVideo = createMediaElement('video', 'testVideo'); + const constraints = { + video: { mediaSource: "window" }, + }; + + try { + await getUserMedia(constraints); + ok(false, "Should require user gesture"); + } catch (e) { + is(e.name, "InvalidStateError"); + } + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + const stream = await getUserMedia(constraints); + const playback = new MediaStreamPlayback(testVideo, stream); + return playback.playMedia(false); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html new file mode 100644 index 0000000000..6af7b69d70 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "Testing that removeTrack+addTrack of video tracks still render the correct track in a media element", + bug: "1223696", + visible: true + }); + + runTest(async function() { + const stream = await getUserMedia({audio:true, video: true}); + info("Test addTrack()ing a video track to an audio-only gUM stream"); + + const video = createMediaElement("video", "test_video_track"); + video.srcObject = stream; + video.play(); + + await haveEvent(video, "loadeddata", wait(5000, new Error("Timeout"))); + info("loadeddata"); + + const removedTrack = stream.getVideoTracks()[0]; + stream.removeTrack(removedTrack); + + const h = new CaptureStreamTestHelper2D(); + const emitter = new VideoFrameEmitter(h.grey, h.grey); + emitter.start(); + + stream.addTrack(emitter.stream().getVideoTracks()[0]); + + checkMediaStreamContains(stream, [stream.getAudioTracks()[0], + emitter.stream().getVideoTracks()[0]]); + + await h.pixelMustBecome(video, h.grey, { + threshold: 5, + infoString: "The canvas track should be rendered by the media element", + }); + + emitter.stop(); + for (const t of [removedTrack, ...stream.getAudioTracks()]) { + t.stop(); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html new file mode 100644 index 0000000000..14c6cc7e7f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "navigator.mozGetUserMedia Callback Test", + bug: "1119593" + }); + /** + * Check that the old fashioned callback-based function works. + */ + runTest(function () { + var testAudio = createMediaElement('audio', 'testAudio'); + var constraints = {audio: true}; + + SimpleTest.waitForExplicitFinish(); + return new Promise(resolve => + navigator.mozGetUserMedia(constraints, stream => { + checkMediaStreamTracks(constraints, stream); + + var playback = new MediaStreamPlayback(testAudio, stream); + return playback.playMedia(false) + .then(() => resolve(), generateErrorCallback()); + }, generateErrorCallback()) + ); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html new file mode 100644 index 0000000000..d6439ce9d6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html @@ -0,0 +1,166 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> + <script src="constraints.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Test getUserMedia constraints", bug: "882145" }); +/** + Tests covering gUM constraints API for audio, video and fake video. Exercise + successful parsing code and ensure that unknown required constraints and + overconstraining cases produce appropriate errors. +*/ +var tests = [ + // Each test here tests a different constraint or codepath. + { message: "unknown required constraint on video ignored", + constraints: { video: { somethingUnknown: { exact: 0 } } }, + error: null }, + { message: "unknown required constraint on audio ignored", + constraints: { audio: { somethingUnknown: { exact: 0 } } }, + error: null }, + { message: "audio overconstrained by facingMode ignored", + constraints: { audio: { facingMode: { exact: 'left' } } }, + error: null }, + { message: "full screensharing requires permission", + constraints: { video: { mediaSource: 'screen' } }, + error: "NotAllowedError" }, + { message: "application screensharing no longer exists", + constraints: { video: { mediaSource: 'application' } }, + error: "OverconstrainedError" }, + { message: "window screensharing requires permission", + constraints: { video: { mediaSource: 'window' } }, + error: "NotAllowedError" }, + { message: "browser screensharing requires permission", + constraints: { video: { mediaSource: 'browser' } }, + error: "NotAllowedError" }, + { message: "unknown mediaSource in video fails", + constraints: { video: { mediaSource: 'uncle' } }, + error: "OverconstrainedError", + constraint: "mediaSource" }, + { message: "unknown mediaSource in audio fails", + constraints: { audio: { mediaSource: 'uncle' } }, + error: "OverconstrainedError", + constraint: "mediaSource" }, + { message: "emtpy constraint fails", + constraints: { }, + error: "TypeError" }, + { message: "Triggering mock failure in default video device fails", + constraints: { video: { deviceId: 'bad device' } }, + error: "NotReadableError" }, + { message: "Triggering mock failure in default audio device fails", + constraints: { audio: { deviceId: 'bad device' } }, + error: "NotReadableError" }, + { message: "Success-path: optional video facingMode + audio ignoring facingMode", + constraints: { audio: { mediaSource: 'microphone', + facingMode: 'left', + foo: 0, + advanced: [{ facingMode: 'environment' }, + { facingMode: 'user' }, + { bar: 0 }] }, + video: { mediaSource: 'camera', + foo: 0, + advanced: [{ facingMode: 'environment' }, + { facingMode: ['user'] }, + { facingMode: ['left', 'right', 'user'] }, + { bar: 0 }] } }, + error: null }, + { message: "legacy facingMode ignored", + constraints: { video: { mandatory: { facingMode: 'left' } } }, + error: null }, +]; + +var mustSupport = [ + 'width', 'height', 'frameRate', 'facingMode', 'deviceId', 'groupId', + 'echoCancellation', 'noiseSuppression', 'autoGainControl', 'channelCount', + + // Yet to add: + // 'aspectRatio', 'volume', 'sampleRate', 'sampleSize', 'latency' + + // http://fluffy.github.io/w3c-screen-share/#screen-based-video-constraints + // OBE by http://w3c.github.io/mediacapture-screen-share + 'mediaSource', + + // Experimental https://bugzilla.mozilla.org/show_bug.cgi?id=1131568#c3 + 'browserWindow', 'scrollWithPage', + 'viewportOffsetX', 'viewportOffsetY', 'viewportWidth', 'viewportHeight', +]; + +var mustFailWith = (msg, reason, constraint, f) => + f().then(() => ok(false, msg + " must fail"), e => { + is(e.name, reason, msg + " must fail: " + e.message); + if (constraint !== undefined) { + is(e.constraint, constraint, msg + " must fail w/correct constraint."); + } + }); + +/** + * Starts the test run by running through each constraint + * test by verifying that the right resolution and rejection is fired. + */ + +runTest(() => pushPrefs( + // This test expects fake devices, particularly for the 'triggering mock + // failure *' steps. So explicitly disable loopback and setup fakes + ['media.audio_loopback_dev', ''], + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true] + ) + .then(() => { + // Check supported constraints first. + var dict = navigator.mediaDevices.getSupportedConstraints(); + var supported = Object.keys(dict); + + mustSupport.forEach(key => ok(supported.includes(key) && dict[key], + "Supports " + key)); + + var unexpected = supported.filter(key => !mustSupport.includes(key)); + is(unexpected.length, 0, + "Unanticipated support (please update test): " + unexpected); + }) + .then(() => pushPrefs(["media.getusermedia.browser.enabled", false], + ["media.getusermedia.screensharing.enabled", false])) + .then(() => tests.reduce((p, test) => p.then( + () => { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + return getUserMedia(test.constraints); + }) + .then(stream => { + is(null, test.error, test.message); + stream.getTracks().forEach(t => t.stop()); + }, e => { + is(e.name, test.error, test.message + ": " + e.message); + if (test.constraint) { + is(e.constraint, test.constraint, + test.message + " w/correct constraint."); + } + }), Promise.resolve())) + .then(() => getUserMedia({video: true, audio: true})) + .then(stream => stream.getVideoTracks()[0].applyConstraints({ width: 320 }) + .then(() => stream.getAudioTracks()[0].applyConstraints({ })) + .then(() => { + stream.getTracks().forEach(track => track.stop()); + ok(true, "applyConstraints code exercised"); + })) + // TODO: Test outcome once fake devices support constraints (Bug 1088621) + .then(() => mustFailWith("applyConstraints fails on non-Gum tracks", + "OverconstrainedError", "", + () => (new AudioContext()) + .createMediaStreamDestination().stream + .getAudioTracks()[0].applyConstraints())) + .then(() => mustFailWith( + "getUserMedia with unsatisfied required constraint", + "OverconstrainedError", "deviceId", + () => getUserMedia({ audio: true, + video: { deviceId: { exact: "unheardof" } } }))) + .then(() => mustFailWith( + "getUserMedia with unsatisfied required constraint array", + "OverconstrainedError", "deviceId", + () => getUserMedia({ audio: true, + video: { deviceId: { exact: ["a", "b"] } } })))); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html new file mode 100644 index 0000000000..54142aeb77 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia with Cubeb Disabled Test", + bug: "1443525" + }); + /** + * Run a test to verify we fail gracefully if we cannot fetch a cubeb context + * during a gUM call. + */ + runTest(async function () { + info("Get user media with cubeb disabled starting"); + // Push prefs to ensure no cubeb context and no fake streams. + await pushPrefs(["media.cubeb.force_null_context", true], + ["media.navigator.permission.device", false], + ["media.navigator.streams.fake", false]); + + // Request audio only, to avoid cams + let constraints = {audio: true, video: false}; + let stream; + try { + stream = await getUserMedia(constraints); + } catch (e) { + // We've got no audio backend, so we expect gUM to fail. + ok(e.name == "NotFoundError", "Expected NotFoundError due to no audio tracks!"); + return; + } + // If we're not on android we should not have gotten a stream without a cubeb context! + ok(false, "getUserMedia not expected to succeed when cubeb is disabled, but it did!"); + }); + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html new file mode 100644 index 0000000000..65d91ec577 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia fake stream with Cubeb Disabled Test", + bug: "1443525" + }); + /** + * Run a test to verify we can still return a fake stream even if we cannot + * get a cubeb context. See also Bug 1434477 + */ + runTest(async function () { + info("Get user media with cubeb disabled and fake tracks starting"); + // Push prefs to ensure no cubeb context and fake streams + await pushPrefs(["media.cubeb.force_null_context", true], + ["media.navigator.streams.fake", true], + ['media.audio_loopback_dev', '']); + let testAudio = createMediaElement('audio', 'testAudio'); + // Request audio only, to avoid cams + let constraints = {audio: true, video: false}; + let stream; + try { + stream = await getUserMedia(constraints); + } catch (e) { + // We've got no audio backend, so we expect gUM to fail + ok(false, `Did not expect to fail, but got ${e}`); + return; + } + ok(stream, "getUserMedia should get a stream!"); + let playback = new MediaStreamPlayback(testAudio, stream); + await playback.playMedia(false); + }); + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html new file mode 100644 index 0000000000..161bf631e3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "Basic getTrackById test of gUM stream", + bug: "1208390", + }); + + runTest(() => { + var constraints = {audio: true, video: true}; + return getUserMedia(constraints).then(stream => { + is(stream.getTrackById(""), null, + "getTrackById of non-matching string should return null"); + + let audioTrack = stream.getAudioTracks()[0]; + is(stream.getTrackById(audioTrack.id), audioTrack, + "getTrackById with matching id should return the track"); + + let videoTrack = stream.getVideoTracks()[0]; + is(stream.getTrackById(videoTrack.id), videoTrack, + "getTrackById with matching id should return the track"); + + stream.removeTrack(audioTrack); + is(stream.getTrackById(audioTrack.id), null, + "getTrackById with id of removed track should return null"); + + let newStream = new MediaStream(); + is(newStream.getTrackById(videoTrack.id), null, + "getTrackById with id of track in other stream should return null"); + + newStream.addTrack(audioTrack); + is(newStream.getTrackById(audioTrack.id), audioTrack, + "getTrackByid with matching id should return the track"); + + newStream.addTrack(videoTrack); + is(newStream.getTrackById(videoTrack.id), videoTrack, + "getTrackByid with matching id should return the track"); + [audioTrack, videoTrack].forEach(t => t.stop()); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html new file mode 100644 index 0000000000..86a7aa5606 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({title: "getUserMedia within getUserMedia", bug: "822109" }); + /** + * Run a test that we can complete a playback cycle for a video, + * then upon completion, do a playback cycle with audio, such that + * the audio gum call happens within the video gum call. + */ + runTest(function () { + return getUserMedia({video: true}) + .then(videoStream => { + var testVideo = createMediaElement('video', 'testVideo'); + var videoPlayback = new MediaStreamPlayback(testVideo, + videoStream); + + return videoPlayback.playMediaWithoutStoppingTracks(false) + .then(() => getUserMedia({audio: true})) + .then(audioStream => { + var testAudio = createMediaElement('audio', 'testAudio'); + var audioPlayback = new MediaStreamPlayback(testAudio, + audioStream); + + return audioPlayback.playMedia(false); + }) + .then(() => videoStream.getTracks().forEach(t => t.stop())); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html new file mode 100644 index 0000000000..d6efac4650 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia in media element should have video dimensions on loadedmetadata", + bug: "1240478" + }); + /** + * Tests that assigning a stream to a media element results in the + * "loadedmetadata" event without having to play() the media element. + * + * Also makes sure that the video size has been set on "loadedmetadata". + */ + runTest(function () { + var v = document.createElement("video"); + document.body.appendChild(v); + v.preload = "metadata"; + + var constraints = {video: true, audio: true}; + return getUserMedia(constraints).then(stream => new Promise(resolve => { + v.srcObject = stream; + v.onloadedmetadata = () => { + isnot(v.videoWidth, 0, "videoWidth shall be set on 'loadedmetadata'"); + isnot(v.videoHeight, 0, "videoHeight shall be set on 'loadedmetadata'"); + resolve(); + }; + }) + .then(() => stream.getTracks().forEach(t => t.stop()))); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html new file mode 100644 index 0000000000..e2f1208490 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script> + +createHTML({ + bug: "1259788", + title: "Test CaptureStream audio content on HTMLMediaElement playing a gUM MediaStream", + visible: true +}); + +var audioContext; +var gUMAudioElement; +var analyser; +let tone; +runTest(() => getUserMedia({audio: { echoCancellation: false }}) + .then(stream => { + gUMAudioElement = createMediaElement("audio", "gUMAudio"); + gUMAudioElement.srcObject = stream; + + audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. + tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + + info("Capturing"); + + analyser = new AudioStreamAnalyser(audioContext, + gUMAudioElement.mozCaptureStream()); + analyser.enableDebugCanvas(); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50); + }) + .then(() => { + info("Audio flowing. Pausing."); + gUMAudioElement.pause(); + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 && + array[analyser.binIndexForFrequency(2500)] < 50); + }) + .then(() => { + info("Audio stopped flowing. Playing."); + gUMAudioElement.play(); + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50); + }) + .then(() => { + info("Audio flowing. Removing source."); + var stream = gUMAudioElement.srcObject; + gUMAudioElement.srcObject = null; + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 && + array[analyser.binIndexForFrequency(2500)] < 50) + .then(() => stream); + }) + .then(stream => { + info("Audio stopped flowing. Setting source."); + gUMAudioElement.srcObject = stream; + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50); + }) + .then(() => { + info("Audio flowing from new source. Adding a track."); + let oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.value = 2000; + oscillator.start(); + + let oscOut = audioContext.createMediaStreamDestination(); + oscillator.connect(oscOut); + + gUMAudioElement.srcObject.addTrack(oscOut.stream.getTracks()[0]); + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 && + array[analyser.binIndexForFrequency(1500)] < 50 && + array[analyser.binIndexForFrequency(2000)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50); + }) + .then(() => { + info("Audio flowing from new track. Removing a track."); + + const gUMTrack = gUMAudioElement.srcObject.getTracks()[0]; + gUMAudioElement.srcObject.removeTrack(gUMTrack); + + is(gUMAudioElement.srcObject.getTracks().length, 1, + "A track should have been removed"); + + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 && + array[analyser.binIndexForFrequency(1500)] < 50 && + array[analyser.binIndexForFrequency(2000)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50) + .then(() => [gUMTrack, ...gUMAudioElement.srcObject.getTracks()] + .forEach(t => t.stop())); + }) + .then(() => ok(true, "Test passed.")) + .catch(e => ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : ""))) + .finally(() => tone.stop())); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html new file mode 100644 index 0000000000..a747e75de9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html @@ -0,0 +1,179 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script> + +createHTML({ + bug: "1259788", + title: "Test CaptureStream track output on HTMLMediaElement playing a gUM MediaStream", + visible: true +}); + +let audioElement; +let audioCaptureStream; +let videoElement; +let videoCaptureStream; +let untilEndedElement; +let streamUntilEnded; +const tracks = []; +runTest(async () => { + try { + let stream = await getUserMedia({audio: true, video: true}); + // We need to test with multiple tracks. We add an extra of each kind. + for (const track of stream.getTracks()) { + stream.addTrack(track.clone()); + } + + audioElement = createMediaElement("audio", "gUMAudio"); + audioElement.srcObject = stream; + + await haveEvent(audioElement, "loadedmetadata", wait(50000, new Error("Timeout"))); + + info("Capturing audio element (loadedmetadata -> captureStream)"); + audioCaptureStream = audioElement.mozCaptureStream(); + + is(audioCaptureStream.getAudioTracks().length, 2, + "audio element should capture two audio tracks"); + is(audioCaptureStream.getVideoTracks().length, 0, + "audio element should not capture any video tracks"); + + await haveNoEvent(audioCaptureStream, "addtrack"); + + videoElement = createMediaElement("video", "gUMVideo"); + + info("Capturing video element (captureStream -> loadedmetadata)"); + videoCaptureStream = videoElement.mozCaptureStream(); + videoElement.srcObject = audioElement.srcObject.clone(); + + is(videoCaptureStream.getTracks().length, 0, + "video element should have no tracks before metadata known"); + + await haveEventsButNoMore( + videoCaptureStream, "addtrack", 3, wait(50000, new Error("No event"))); + + is(videoCaptureStream.getAudioTracks().length, 2, + "video element should capture two audio tracks"); + is(videoCaptureStream.getVideoTracks().length, 1, + "video element should capture one video track at most"); + + info("Testing dynamically adding audio track to audio element"); + audioElement.srcObject.addTrack( + audioElement.srcObject.getAudioTracks()[0].clone()); + await haveEventsButNoMore( + audioCaptureStream, "addtrack", 1, wait(50000, new Error("No event"))); + + is(audioCaptureStream.getAudioTracks().length, 3, + "Audio element should have three audio tracks captured."); + + info("Testing dynamically adding video track to audio element"); + audioElement.srcObject.addTrack( + audioElement.srcObject.getVideoTracks()[0].clone()); + await haveNoEvent(audioCaptureStream, "addtrack"); + + is(audioCaptureStream.getVideoTracks().length, 0, + "Audio element should have no video tracks captured."); + + info("Testing dynamically adding audio track to video element"); + videoElement.srcObject.addTrack( + videoElement.srcObject.getAudioTracks()[0].clone()); + await haveEventsButNoMore( + videoCaptureStream, "addtrack", 1, wait(50000, new Error("Timeout"))); + + is(videoCaptureStream.getAudioTracks().length, 3, + "Captured video stream should have three audio tracks captured."); + + info("Testing dynamically adding video track to video element"); + videoElement.srcObject.addTrack( + videoElement.srcObject.getVideoTracks()[0].clone()); + await haveNoEvent(videoCaptureStream, "addtrack"); + + is(videoCaptureStream.getVideoTracks().length, 1, + "Captured video stream should have at most one video tracks captured."); + + info("Testing track removal."); + tracks.push(...videoElement.srcObject.getTracks()); + for (const track of videoElement.srcObject.getVideoTracks().reverse()) { + videoElement.srcObject.removeTrack(track); + } + is(videoCaptureStream.getVideoTracks().length, 1, + "Captured video should have still have one video track."); + + await haveEvent(videoCaptureStream.getVideoTracks()[0], "ended", + wait(50000, new Error("Timeout"))); + await haveEvent(videoCaptureStream, "removetrack", + wait(50000, new Error("Timeout"))); + + is(videoCaptureStream.getVideoTracks().length, 0, + "Captured video stream should have no video tracks after removal."); + + + info("Testing source reset."); + stream = await getUserMedia({audio: true, video: true}); + videoElement.srcObject = stream; + for (const track of videoCaptureStream.getTracks()) { + await Promise.race(videoCaptureStream.getTracks().map( + t => haveEvent(t, "ended", wait(50000, new Error("Timeout")))) + ); + await haveEvent(videoCaptureStream, "removetrack", wait(50000, new Error("Timeout"))); + } + await haveEventsButNoMore( + videoCaptureStream, "addtrack", 2, wait(50000, new Error("Timeout"))); + is(videoCaptureStream.getAudioTracks().length, 1, + "Captured video stream should have one audio track"); + + is(videoCaptureStream.getVideoTracks().length, 1, + "Captured video stream should have one video track"); + + info("Testing CaptureStreamUntilEnded"); + untilEndedElement = + createMediaElement("video", "gUMVideoUntilEnded"); + untilEndedElement.srcObject = audioElement.srcObject; + + await haveEvent(untilEndedElement, "loadedmetadata", + wait(50000, new Error("Timeout"))); + + streamUntilEnded = untilEndedElement.mozCaptureStreamUntilEnded(); + + is(streamUntilEnded.getAudioTracks().length, 3, + "video element should capture all 3 audio tracks until ended"); + is(streamUntilEnded.getVideoTracks().length, 1, + "video element should capture only 1 video track until ended"); + + for (const track of untilEndedElement.srcObject.getTracks()) { + track.stop(); + } + + await haveEvent(untilEndedElement, "ended", wait(50000, new Error("Timeout"))); + for (const track of streamUntilEnded.getTracks()) { + await Promise.race(streamUntilEnded.getTracks().map( + t => haveEvent(t, "ended", wait(50000, new Error("Timeout")))) + ); + await haveEvent(streamUntilEnded, "removetrack", wait(50000, new Error("Timeout"))); + } + + info("Element and tracks ended. Ensuring that new tracks aren't created."); + untilEndedElement.srcObject = videoElement.srcObject; + await haveEventsButNoMore( + untilEndedElement, "loadedmetadata", 1, wait(50000, new Error("Timeout"))); + + is(streamUntilEnded.getTracks().length, 0, "Should have no tracks"); + } catch(e) { + ok(false, "Test failed: " + e + (e && e.stack ? "\n" + e.stack : "")); + } finally { + if (videoElement) { + tracks.push(...videoElement.srcObject.getTracks()); + } + for(const track of tracks) { + track.stop(); + } + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html new file mode 100644 index 0000000000..d177e93bfb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script> + +createHTML({ + bug: "1259788", + title: "Test CaptureStream video content on HTMLMediaElement playing a gUM MediaStream", + visible: true +}); + +var gUMVideoElement; +var captureStreamElement; + +const pausedTimeout = 1000; +let h; + +runTest(async () => { + try { + await pushPrefs( + // This test expects fake video devices, as it expects captured frames to + // shift over time, which is not currently provided by loopback devices + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + + let stream = await getUserMedia({video: true}); + h = new VideoStreamHelper(); + gUMVideoElement = + createMediaElement("video", "gUMVideo"); + gUMVideoElement.srcObject = stream; + gUMVideoElement.play(); + + info("Capturing"); + captureStreamElement = + createMediaElement("video", "captureStream"); + captureStreamElement.srcObject = gUMVideoElement.mozCaptureStream(); + captureStreamElement.play(); + + await h.checkVideoPlaying(captureStreamElement); + + // Adding a dummy audio track to the stream will keep a consuming media + // element from ending. + // We could also solve it by repeatedly play()ing or autoplay, but then we + // wouldn't be sure the media element stopped rendering video because it + // went to the ended state or because there were no frames for the track. + let osc = createOscillatorStream(new AudioContext(), 1000); + captureStreamElement.srcObject.addTrack(osc.getTracks()[0]); + + info("Video flowing. Pausing."); + gUMVideoElement.pause(); + await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout }); + + info("Video stopped flowing. Playing."); + gUMVideoElement.play(); + await h.checkVideoPlaying(captureStreamElement); + + info("Video flowing. Removing source."); + stream = gUMVideoElement.srcObject; + gUMVideoElement.srcObject = null; + await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout }); + + info("Video stopped flowing. Setting source."); + gUMVideoElement.srcObject = stream; + await h.checkVideoPlaying(captureStreamElement); + + info("Video flowing. Changing source by track manipulation. Remove first."); + let track = gUMVideoElement.srcObject.getTracks()[0]; + gUMVideoElement.srcObject.removeTrack(track); + await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout }); + + info("Video paused. Changing source by track manipulation. Add first."); + gUMVideoElement.srcObject.addTrack(track); + gUMVideoElement.play(); + await h.checkVideoPlaying(captureStreamElement); + + gUMVideoElement.srcObject.getTracks().forEach(t => t.stop()); + ok(true, "Test passed."); + } catch (e) { + ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : "")); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html new file mode 100644 index 0000000000..f0ae8df64c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html @@ -0,0 +1,260 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "MediaStream.clone()", + bug: "1208371" +}); + +runTest(async () => { + await pushPrefs( + ["media.getusermedia.camera.stop_on_disable.enabled", true], + ["media.getusermedia.camera.stop_on_disable.delay_ms", 0], + ["media.getusermedia.microphone.stop_on_disable.enabled", true], + ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0]); + + let gUMStream = await getUserMedia({audio: true, video: true}); + { + info("Test clone()ing an audio/video gUM stream"); + let clone = gUMStream.clone(); + + checkMediaStreamCloneAgainstOriginal(clone, gUMStream); + checkMediaStreamTrackCloneAgainstOriginal(clone.getAudioTracks()[0], + gUMStream.getAudioTracks()[0]); + checkMediaStreamTrackCloneAgainstOriginal(clone.getVideoTracks()[0], + gUMStream.getVideoTracks()[0]); + + isnot(clone.id.length, 0, "Stream clone should have an id string"); + isnot(clone.getAudioTracks()[0].id.length, 0, + "Audio track clone should have an id string"); + isnot(clone.getVideoTracks()[0].id.length, 0, + "Audio track clone should have an id string"); + + info("Playing from track clones"); + let test = createMediaElement('video', 'testClonePlayback'); + let playback = new MediaStreamPlayback(test, clone); + await playback.playMedia(false); + } + + { + info("Test addTrack()ing a video track to a stream without affecting its clone"); + let stream = new MediaStream(gUMStream.getVideoTracks()); + let otherStream = await getUserMedia({video: true}); + let track = stream.getTracks()[0]; + let otherTrack = otherStream.getTracks()[0]; + + let streamClone = stream.clone(); + let trackClone = streamClone.getTracks()[0]; + checkMediaStreamContains(streamClone, [trackClone], "Initial clone"); + + stream.addTrack(otherTrack); + checkMediaStreamContains(stream, [track, otherTrack], + "Added video to original"); + checkMediaStreamContains(streamClone, [trackClone], + "Clone not affected"); + + stream.removeTrack(track); + streamClone.addTrack(track); + checkMediaStreamContains(streamClone, [trackClone, track], + "Added video to clone"); + checkMediaStreamContains(stream, [otherTrack], + "Original not affected"); + + // Not part of streamClone. Does not get stopped by the playback test. + otherTrack.stop(); + + let test = createMediaElement('video', 'testClonePlayback'); + let playback = new MediaStreamPlayback(test, streamClone); + await playback.playMedia(false); + } + + { + info("Test cloning a stream into inception"); + let stream = gUMStream.clone() + let clone = stream; + let clones = Array(10).fill().map(() => clone = clone.clone()); + let inceptionClone = clones.pop(); + checkMediaStreamCloneAgainstOriginal(inceptionClone, stream); + stream.getTracks().forEach(t => { + stream.removeTrack(t); + return inceptionClone.addTrack(t); + }); + is(inceptionClone.getAudioTracks().length, 2, + "The inception clone should contain the original audio track and a track clone"); + is(inceptionClone.getVideoTracks().length, 2, + "The inception clone should contain the original video track and a track clone"); + + let test = createMediaElement('video', 'testClonePlayback'); + let playback = new MediaStreamPlayback(test, inceptionClone); + await playback.playMedia(false); + clones.forEach(c => c.getTracks().forEach(t => t.stop())); + stream.getTracks().forEach(t => t.stop()); + } + + { + info("Test adding tracks from many stream clones to the original stream"); + let stream = gUMStream.clone(); + + const LOOPS = 3; + for (let i = 0; i < LOOPS; i++) { + stream.clone().getTracks().forEach(t => stream.addTrack(t)); + } + is(stream.getAudioTracks().length, Math.pow(2, LOOPS), + "The original track should contain the original audio track and all the audio clones"); + is(stream.getVideoTracks().length, Math.pow(2, LOOPS), + "The original track should contain the original video track and all the video clones"); + stream.getTracks().forEach(t1 => is(stream.getTracks() + .filter(t2 => t1.id == t2.id) + .length, + 1, "Each track should be unique")); + + let test = createMediaElement('video', 'testClonePlayback'); + let playback = new MediaStreamPlayback(test, stream); + await playback.playMedia(false); + } + + { + info("Testing audio content routing with MediaStream.clone()"); + let ac = new AudioContext(); + + let osc1kOriginal = createOscillatorStream(ac, 1000); + let audioTrack1kOriginal = osc1kOriginal.getTracks()[0]; + let audioTrack1kClone = osc1kOriginal.clone().getTracks()[0]; + + let osc5kOriginal = createOscillatorStream(ac, 5000); + let audioTrack5kOriginal = osc5kOriginal.getTracks()[0]; + let audioTrack5kClone = osc5kOriginal.clone().getTracks()[0]; + + info("Analysing audio output of original stream (1k + 5k)"); + let stream = new MediaStream(); + stream.addTrack(audioTrack1kOriginal); + stream.addTrack(audioTrack5kOriginal); + + let analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50); + + info("Waiting for original tracks to stop"); + stream.getTracks().forEach(t => t.stop()); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + // WebAudioDestination streams do not handle stop() + // XXX Should they? Plan to resolve that in bug 1208384. + // array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(3000)] < 50 && + // array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] < 50); + analyser.disconnect(); + + info("Analysing audio output of stream clone (1k + 5k)"); + stream = new MediaStream(); + stream.addTrack(audioTrack1kClone); + stream.addTrack(audioTrack5kClone); + + analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50); + analyser.disconnect(); + + info("Analysing audio output of clone of clone (1k + 5k)"); + stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]).clone(); + + analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50); + analyser.disconnect(); + + info("Analysing audio output of clone() + addTrack()ed tracks (1k + 5k)"); + stream = new MediaStream(new MediaStream([ audioTrack1kClone + , audioTrack5kClone + ]).clone().getTracks()); + + analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50); + analyser.disconnect(); + + info("Analysing audio output of clone()d tracks in original stream (1k) " + + "and clone()d tracks in stream clone (5k)"); + stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]); + let streamClone = stream.clone(); + + stream.getTracks().forEach(t => stream.removeTrack(t)); + stream.addTrack(streamClone.getTracks()[0]); + streamClone.removeTrack(streamClone.getTracks()[0]); + + analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50); + analyser.disconnect(); + + let cloneAnalyser = new AudioStreamAnalyser(ac, streamClone); + await cloneAnalyser.waitForAnalysisSuccess(array => + array[cloneAnalyser.binIndexForFrequency(1000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(3000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(5000)] > 200 && + array[cloneAnalyser.binIndexForFrequency(10000)] < 50); + cloneAnalyser.disconnect(); + + info("Analysing audio output enabled and disabled tracks that don't affect each other"); + stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]); + let clone = stream.clone(); + + stream.getTracks()[0].enabled = true; + stream.getTracks()[1].enabled = false; + + clone.getTracks()[0].enabled = false; + clone.getTracks()[1].enabled = true; + + analyser = new AudioStreamAnalyser(ac, stream); + await analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50); + analyser.disconnect(); + + cloneAnalyser = new AudioStreamAnalyser(ac, clone); + await cloneAnalyser.waitForAnalysisSuccess(array => + array[cloneAnalyser.binIndexForFrequency(1000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(3000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(5000)] > 200 && + array[cloneAnalyser.binIndexForFrequency(10000)] < 50); + cloneAnalyser.disconnect(); + + // Restore original tracks + stream.getTracks().forEach(t => t.enabled = true); + } + + gUMStream.getTracks().forEach(t => t.stop()); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html new file mode 100644 index 0000000000..4ea6e3f444 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html @@ -0,0 +1,171 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "MediaStream constructors with getUserMedia streams Test", + bug: "1070216" + }); + + var audioContext = new AudioContext(); + var videoElement; + + runTest(() => Promise.resolve() + .then(() => videoElement = createMediaElement('video', 'constructorsTest')) + .then(() => getUserMedia({video: true})).then(gUMStream => { + info("Test default constructor with video"); + ok(gUMStream.active, "gUMStream with one track should be active"); + var track = gUMStream.getTracks()[0]; + + var stream = new MediaStream(); + ok(!stream.active, "New MediaStream should be inactive"); + checkMediaStreamContains(stream, [], "Default constructed stream"); + + stream.addTrack(track); + ok(stream.active, "MediaStream should be active after adding a track"); + checkMediaStreamContains(stream, [track], "Added video track"); + + var playback = new MediaStreamPlayback(videoElement, stream); + return playback.playMedia(false).then(() => { + ok(!gUMStream.active, "gUMStream should be inactive after stopping"); + ok(!stream.active, "stream with stopped tracks should be inactive"); + }); + }) + .then(() => getUserMedia({video: true})).then(gUMStream => { + info("Test copy constructor with gUM stream"); + ok(gUMStream.active, "gUMStream with one track should be active"); + var track = gUMStream.getTracks()[0]; + + var stream = new MediaStream(gUMStream); + ok(stream.active, "List constructed MediaStream should be active"); + checkMediaStreamContains(stream, [track], "Copy constructed video track"); + + var playback = new MediaStreamPlayback(videoElement, stream); + return playback.playMedia(false).then(() => { + ok(!gUMStream.active, "gUMStream should be inactive after stopping"); + ok(!stream.active, "stream with stopped tracks should be inactive"); + }); + }) + .then(() => getUserMedia({video: true})).then(gUMStream => { + info("Test list constructor with empty list"); + ok(gUMStream.active, "gUMStream with one track should be active"); + var track = gUMStream.getTracks()[0]; + + var stream = new MediaStream([]); + ok(!stream.active, "Empty-list constructed MediaStream should be inactive"); + checkMediaStreamContains(stream, [], "Empty-list constructed stream"); + + stream.addTrack(track); + ok(stream.active, "MediaStream should be active after adding a track"); + checkMediaStreamContains(stream, [track], "Added video track"); + + var playback = new MediaStreamPlayback(videoElement, stream); + return playback.playMedia(false).then(() => { + ok(!gUMStream.active, "gUMStream should be inactive after stopping"); + ok(!stream.active, "stream with stopped tracks should be inactive"); + }); + }) + .then(() => getUserMedia({audio: true, video: true})).then(gUMStream => { + info("Test list constructor with a gUM audio/video stream"); + ok(gUMStream.active, "gUMStream with two tracks should be active"); + var audioTrack = gUMStream.getAudioTracks()[0]; + var videoTrack = gUMStream.getVideoTracks()[0]; + + var stream = new MediaStream([audioTrack, videoTrack]); + ok(stream.active, "List constructed MediaStream should be active"); + checkMediaStreamContains(stream, [audioTrack, videoTrack], + "List constructed audio and video tracks"); + + var playback = new MediaStreamPlayback(videoElement, stream); + return playback.playMedia(false).then(() => { + ok(!gUMStream.active, "gUMStream should be inactive after stopping"); + ok(!stream.active, "stream with stopped tracks should be inactive"); + }); + }) + .then(() => getUserMedia({video: true})).then(gUMStream => { + info("Test list constructor with gUM-video and WebAudio tracks"); + ok(gUMStream.active, "gUMStream with one track should be active"); + var audioStream = createOscillatorStream(audioContext, 2000); + ok(audioStream.active, "WebAudio stream should be active"); + + var audioTrack = audioStream.getTracks()[0]; + var videoTrack = gUMStream.getTracks()[0]; + + var stream = new MediaStream([audioTrack, videoTrack]); + ok(stream.active, "List constructed MediaStream should be active"); + checkMediaStreamContains(stream, [audioTrack, videoTrack], + "List constructed WebAudio and gUM-video tracks"); + + var playback = new MediaStreamPlayback(videoElement, stream); + return playback.playMedia(false).then(() => { + gUMStream.getTracks().forEach(t => t.stop()); + ok(!gUMStream.active, "gUMStream should be inactive after stopping"); + ok(!stream.active, "stream with stopped tracks should be inactive"); + }); + }) + .then(() => { + var osc1k = createOscillatorStream(audioContext, 1000); + var audioTrack1k = osc1k.getTracks()[0]; + + var osc5k = createOscillatorStream(audioContext, 5000); + var audioTrack5k = osc5k.getTracks()[0]; + + var osc10k = createOscillatorStream(audioContext, 10000); + var audioTrack10k = osc10k.getTracks()[0]; + + return Promise.resolve().then(() => { + info("Analysing audio output with empty default constructed stream"); + var stream = new MediaStream(); + var analyser = new AudioStreamAnalyser(audioContext, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] < 50) + .then(() => analyser.disconnect()); + }).then(() => { + info("Analysing audio output with copy constructed 5k stream"); + var stream = new MediaStream(osc5k); + is(stream.active, osc5k.active, + "Copy constructed MediaStream should preserve active state"); + var analyser = new AudioStreamAnalyser(audioContext, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50) + .then(() => analyser.disconnect()); + }).then(() => { + info("Analysing audio output with empty-list constructed stream"); + var stream = new MediaStream([]); + var analyser = new AudioStreamAnalyser(audioContext, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] < 50) + .then(() => analyser.disconnect()); + }).then(() => { + info("Analysing audio output with list constructed 1k, 5k and 10k tracks"); + var stream = new MediaStream([audioTrack1k, audioTrack5k, audioTrack10k]); + ok(stream.active, + "List constructed MediaStream from WebAudio should be active"); + var analyser = new AudioStreamAnalyser(audioContext, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(2500)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(7500)] < 50 && + array[analyser.binIndexForFrequency(10000)] > 200 && + array[analyser.binIndexForFrequency(11000)] < 50) + .then(() => analyser.disconnect()); + }); + })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html new file mode 100644 index 0000000000..e5e0764427 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "MediaStreamTrack.clone()", + bug: "1208371" + }); + + var testSingleTrackClonePlayback = constraints => + getUserMedia(constraints).then(stream => { + info("Test clone()ing an " + constraints + " gUM track"); + var track = stream.getTracks()[0]; + var clone = track.clone(); + + checkMediaStreamTrackCloneAgainstOriginal(clone, track); + + info("Stopping original track"); + track.stop(); + + info("Creating new stream for clone"); + var cloneStream = new MediaStream([clone]); + checkMediaStreamContains(cloneStream, [clone]); + + info("Testing playback of track clone"); + var test = createMediaElement('video', 'testClonePlayback'); + var playback = new MediaStreamPlayback(test, cloneStream); + return playback.playMedia(false); + }); + + runTest(() => Promise.resolve() + .then(() => testSingleTrackClonePlayback({audio: true})) + .then(() => testSingleTrackClonePlayback({video: true})) + .then(() => getUserMedia({video: true})).then(stream => { + info("Test cloning a track into inception"); + var track = stream.getTracks()[0]; + var clone = track; + var clones = Array(10).fill().map(() => clone = clone.clone()); + var inceptionClone = clones.pop(); + checkMediaStreamTrackCloneAgainstOriginal(inceptionClone, track); + + var cloneStream = new MediaStream(); + cloneStream.addTrack(inceptionClone); + + // cloneStream is now essentially the same as stream.clone(); + checkMediaStreamCloneAgainstOriginal(cloneStream, stream); + + var test = createMediaElement('video', 'testClonePlayback'); + var playback = new MediaStreamPlayback(test, cloneStream); + return playback.playMedia(false).then(() => { + info("Testing that clones of ended tracks are ended"); + cloneStream.clone().getTracks().forEach(t => + is(t.readyState, "ended", "Track " + t.id + " should be ended")); + }) + .then(() => { + clones.forEach(t => t.stop()); + track.stop(); + }); + }) + .then(() => getUserMedia({audio: true, video: true})).then(stream => { + info("Test adding many track clones to the original stream"); + + const LOOPS = 3; + for (var i = 0; i < LOOPS; i++) { + stream.getTracks().forEach(t => stream.addTrack(t.clone())); + } + is(stream.getVideoTracks().length, Math.pow(2, LOOPS), + "The original track should contain the original video track and all the video clones"); + stream.getTracks().forEach(t1 => is(stream.getTracks() + .filter(t2 => t1.id == t2.id) + .length, + 1, "Each track should be unique")); + + var test = createMediaElement('video', 'testClonePlayback'); + var playback = new MediaStreamPlayback(test, stream); + return playback.playMedia(false); + }) + .then(() => { + info("Testing audio content routing with MediaStreamTrack.clone()"); + var ac = new AudioContext(); + + var osc1kOriginal = createOscillatorStream(ac, 1000); + var audioTrack1kOriginal = osc1kOriginal.getTracks()[0]; + var audioTrack1kClone = audioTrack1kOriginal.clone(); + + var osc5kOriginal = createOscillatorStream(ac, 5000); + var audioTrack5kOriginal = osc5kOriginal.getTracks()[0]; + var audioTrack5kClone = audioTrack5kOriginal.clone(); + + return Promise.resolve().then(() => { + info("Analysing audio output enabled and disabled tracks that don't affect each other"); + audioTrack1kOriginal.enabled = true; + audioTrack5kOriginal.enabled = false; + + audioTrack1kClone.enabled = false; + audioTrack5kClone.enabled = true; + + var analyser = + new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kOriginal, + audioTrack5kOriginal])); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50) + .then(() => analyser.disconnect()) + .then(() => { + var cloneAnalyser = + new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kClone, + audioTrack5kClone])); + return cloneAnalyser.waitForAnalysisSuccess(array => + array[cloneAnalyser.binIndexForFrequency(1000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(3000)] < 50 && + array[cloneAnalyser.binIndexForFrequency(5000)] > 200 && + array[cloneAnalyser.binIndexForFrequency(10000)] < 50) + .then(() => cloneAnalyser.disconnect()); + }) + // Restore original tracks + .then(() => [audioTrack1kOriginal, + audioTrack5kOriginal, + audioTrack1kClone, + audioTrack5kClone].forEach(t => t.enabled = true)); + }).then(() => { + info("Analysing audio output of 1k original and 5k clone."); + var stream = new MediaStream(); + stream.addTrack(audioTrack1kOriginal); + stream.addTrack(audioTrack5kClone); + + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50) + .then(() => { + info("Waiting for tracks to stop"); + stream.getTracks().forEach(t => t.stop()); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] < 50 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] < 50 && + array[analyser.binIndexForFrequency(10000)] < 50); + }).then(() => analyser.disconnect()); + }).then(() => { + info("Analysing audio output of clones of clones (1kx2 + 5kx4)"); + var stream = new MediaStream([audioTrack1kClone.clone(), + audioTrack5kOriginal.clone().clone().clone().clone()]); + + var analyser = new AudioStreamAnalyser(ac, stream); + return analyser.waitForAnalysisSuccess(array => + array[analyser.binIndexForFrequency(50)] < 50 && + array[analyser.binIndexForFrequency(1000)] > 200 && + array[analyser.binIndexForFrequency(3000)] < 50 && + array[analyser.binIndexForFrequency(5000)] > 200 && + array[analyser.binIndexForFrequency(10000)] < 50) + .then(() => analyser.disconnect()); + }); + })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html new file mode 100644 index 0000000000..376381ddf1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "getUserMedia feed to a graph with non default rate", + bug: "1387454", + }); + + /** + * Run a test to verify that when we use the streem from a gUM to an AudioContext + * with non default rate the connection fails. (gUM is always on default rate). + */ + runTest(async () => { + const stream = await getUserMedia({audio: true}); + const nonDefaultRate = 32000; + const ac = new AudioContext({sampleRate: nonDefaultRate}); + mustThrowWith( + "Connect stream with graph of different sample rate", + "NotSupportedError", () => { + ac.createMediaStreamSource(stream); + } + ); + for (let t of stream.getTracks()) { + t.stop(); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html new file mode 100644 index 0000000000..2c964e2f89 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> + <script type="application/javascript" src="blacksilence.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "Test getUserMedia peerIdentity Constraint", + bug: "942367" +}); +async function theTest() { + async function testPeerIdentityConstraint(withConstraint) { + const config = { audio: true, video: true }; + if (withConstraint) { + config.peerIdentity = 'user@example.com'; + } + info('getting media with constraints: ' + JSON.stringify(config)); + const stream = await getUserMedia(config); + for (const track of stream.getTracks()) { + const recorder = new MediaRecorder(new MediaStream([track])); + try { + recorder.start(); + ok(!withConstraint, + `gUM ${track.kind} track without peerIdentity must not throw`); + recorder.stop(); + } catch(e) { + ok(withConstraint, + `gUM ${track.kind} track with peerIdentity must throw`); + } + } + await Promise.all([ + audioIsSilence(withConstraint, stream), + videoIsBlack(withConstraint, stream), + ]); + stream.getTracks().forEach(t => t.stop()); + }; + + // Start a tone so that the gUM call will record something even + // with --use-test-media-devices. + const audioContext = new AudioContext(); + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + + try { + // both without and with the constraint + await testPeerIdentityConstraint(false); + await testPeerIdentityConstraint(true); + } finally { + tone.stop(); + } +} + +runTest(theTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html new file mode 100644 index 0000000000..cd02c7326c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Test getUserMedia in iframes", bug: "1371741" }); +/** + Tests covering enumerateDevices API and deviceId constraint. Exercise code. +*/ + +// Call gUM in iframe. +async function iframeGum(dict, iframe = document.createElement("iframe")) { + Object.assign(iframe, dict); + if (dict.src) { + info(`<iframe src="${dict.src}" sandbox="${dict.sandbox}">`); + } else { + info(`<iframe srcdoc sandbox="${dict.sandbox}">`); + } + document.documentElement.appendChild(iframe); + + const once = (t, msg) => new Promise(r => t.addEventListener(msg, r, { once: true })); + const haveMessage = once(window, "message"); + await new Promise(resolve => iframe.onload = resolve); + return (await haveMessage).data; +}; + +runTest(async () => { + const path = "/tests/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html"; + + async function sourceFn() { + try { + const gUM = c => navigator.mediaDevices.getUserMedia(c); + let message; + let stream; + try { + stream = await gUM({ video: true }); + message = 'success'; + } catch(e) { + message = e.name; + } + parent.postMessage(message, 'https://example.com:443'); + + if (message == "success") { + stream.getTracks().forEach(track => track.stop()); + } + } catch (e) { + setTimeout(() => { throw e; }); + } + } + + const source = `<html\><script\>(${sourceFn.toString()})()</script\></html\>`; + + // Test gUM in sandboxed vs. regular iframe. + + for (const origin of [window.location.origin, "https://test1.example.com"]) { + const src = origin + path; + is(await iframeGum({ src, sandbox: "allow-scripts" }), + "NotAllowedError", "gUM fails in sandboxed iframe " + origin); + } + is(await iframeGum({ + src: path, + sandbox: "allow-scripts allow-same-origin", + }), + "success", "gUM works in regular same-origin iframe"); + is(await iframeGum({ + src: `https://test1.example.com${path}`, + sandbox: "allow-scripts allow-same-origin", + }), + "NotAllowedError", "gUM fails in regular cross-origin iframe"); + + // Test gUM in sandboxed vs regular srcdoc iframe + + const iframeSrcdoc = document.createElement("iframe"); + iframeSrcdoc.srcdoc = source; + is(await iframeGum({ sandbox: "allow-scripts" }, iframeSrcdoc), + "NotAllowedError", "gUM fails in sandboxed srcdoc iframe"); + is(await iframeGum({ sandbox: "allow-scripts allow-same-origin" }, iframeSrcdoc), + "success", "gUM works in regular srcdoc iframe"); + + // Test gUM in sandboxed vs regular blob iframe + + const blob = new Blob([source], {type : "text/html"}); + let src = URL.createObjectURL(blob); + is(await iframeGum({ src, sandbox: "allow-scripts" }), + "NotAllowedError", "gUM fails in sandboxed blob iframe"); + is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}), + "success", "gUM works in regular blob iframe"); + URL.revokeObjectURL(src); + + // data iframes always have null-principals + + src = `data:text/html;base64,${btoa(source)}`; + is(await iframeGum({ src, sandbox: "allow-scripts" }), + "NotAllowedError", "gUM fails in sandboxed data iframe"); + is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}), + "NotAllowedError", "gUM fails in regular data iframe"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html new file mode 100644 index 0000000000..732c2cf98c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<body> +<pre id="test"> +<script type="application/javascript"> +/** + Runs inside iframe in test_getUserMedia_permission.html. +*/ + +const gUM = c => navigator.mediaDevices.getUserMedia(c); + +(async () => { + let message; + let stream; + try { + stream = await gUM({ video: true }); + message = "success"; + } catch(e) { + message = e.name; + } + parent.postMessage(message, "https://example.com:443"); + + if (message == "success") { + stream.getTracks().forEach(track => track.stop()); + } +})().catch(e => setTimeout(() => { throw e; })); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html new file mode 100644 index 0000000000..30d168bf38 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({title: "getUserMedia Play Audio Twice", bug: "822109" }); + /** + * Run a test that we can complete an audio playback cycle twice in a row. + */ + runTest(function () { + return getUserMedia({audio: true}).then(audioStream => { + var testAudio = createMediaElement('audio', 'testAudio'); + var playback = new MediaStreamPlayback(testAudio, audioStream); + + return playback.playMediaWithoutStoppingTracks(false) + .then(() => playback.playMedia(true)); + }); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html new file mode 100644 index 0000000000..7b5e6effd1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({title: "getUserMedia Play Video and Audio Twice", bug: "822109" }); + /** + * Run a test that we can complete a video playback cycle twice in a row. + */ + runTest(function () { + return getUserMedia({video: true, audio: true}).then(stream => { + var testVideo = createMediaElement('video', 'testVideo'); + var playback = new MediaStreamPlayback(testVideo, stream); + + return playback.playMediaWithoutStoppingTracks(false) + .then(() => playback.playMedia(true)); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html new file mode 100644 index 0000000000..2890f45eab --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ title: "getUserMedia Play Video Twice", bug: "822109" }); + /** + * Run a test that we can complete a video playback cycle twice in a row. + */ + runTest(function () { + return getUserMedia({video: true}).then(stream => { + var testVideo = createMediaElement('video', 'testVideo'); + var streamPlayback = new MediaStreamPlayback(testVideo, stream); + + return streamPlayback.playMediaWithoutStoppingTracks(false) + .then(() => streamPlayback.playMedia(true)); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html new file mode 100644 index 0000000000..782110823e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + +createHTML({title: "Detect screensharing sources that are firefox", bug: "1311048"}); + +const Services = SpecialPowers.Services; + +let observe = topic => new Promise(r => Services.obs.addObserver(function o(...args) { + Services.obs.removeObserver(o, topic); + r(args.map(x => SpecialPowers.wrap(x))); +}, topic)); + +let getDevices = async constraints => { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let [{ windowID, innerWindowID, callID, devices }] = await Promise.race([ + getUserMedia(constraints), + observe("getUserMedia:request") + ]); + let window = Services.wm.getOuterWindowWithId(windowID); + return devices.map(SpecialPowers.wrapCallback(d => d.QueryInterface(Ci.nsIMediaDevice))); +}; + +runTest(async () => { + await pushPrefs(["media.navigator.permission.disabled", true], + ["media.navigator.permission.force", true]); + let devices = await getDevices({video: { mediaSource: "window" }}); + ok(devices.length, "Found one or more windows."); + devices = Array.prototype.filter.call(devices, d => d.scary); + ok(devices.length, "Found one or more scary windows (our own counts)."); + devices = devices.filter(d => d.rawName.includes("MochiTest")); + ok(devices.length, + "Our own window is among the scary: " + + devices.map(d => `"${d.rawName}"`)); + + devices = await getDevices({video: { mediaSource: "screen" }}); + let numScreens = devices.length; + ok(numScreens, "Found one or more screens."); + devices = Array.prototype.filter.call(devices, d => d.scary); + is(devices.length, numScreens, "All screens are scary."); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html new file mode 100644 index 0000000000..ae691785f5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ title: "getUserMedia Basic Audio Test", bug: "1208656" }); + /** + * Run a test to verify that we can spin the event loop from within a mozGUM callback. + */ + runTest(() => { + var testAudio = createMediaElement('audio', 'testAudio'); + return new Promise((resolve, reject) => { + navigator.mozGetUserMedia({ audio: true }, stream => { + SpecialPowers.spinEventLoop(window); + ok(true, "Didn't crash"); + stream.getTracks().forEach(t => t.stop()); + resolve(); + }, () => {}); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html new file mode 100644 index 0000000000..60077ec73b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + "use strict"; + + createHTML({ + title: "Stopping a MediaStreamTrack and its clones should deallocate the device", + bug: "1294605" + }); + + runTest(async () => { + await pushPrefs(["media.navigator.permission.fake", true]); + const stream = await getUserMedia({audio: true, video: true, fake: true}); + const clone = stream.clone(); + stream.getTracks().forEach(t => t.stop()); + stream.clone().getTracks().forEach(t => stream.addTrack(t)); + is(stream.getTracks().filter(t => t.readyState == "live").length, 0, + "Cloning ended tracks should make them ended"); + [...stream.getTracks(), ...clone.getTracks()].forEach(t => t.stop()); + + // Bug 1295352: better to be explicit about noGum here wrt future refactoring. + return noGum(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html new file mode 100644 index 0000000000..b275f4555f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<iframe id="iframe" onload="iframeLoaded()" srcdoc=" + <script type='application/javascript'> + document.gUM = (constraints, success, failure) => + navigator.mediaDevices.getUserMedia(constraints).then(success, failure); + </script>"> +</iframe> +<script type="application/javascript"> + "use strict"; + let iframeLoadedPromise = {}; + iframeLoadedPromise.promise = new Promise(r => { + iframeLoadedPromise.resolve = r; + });; + function iframeLoaded() { + iframeLoadedPromise.resolve(); + } + + createHTML({ + title: "getUserMedia MediaStreamTrack 'ended' event on navigating", + bug: "1208373", + }); + + runTest(async () => { + await iframeLoadedPromise.promise; + let iframe = document.getElementById("iframe"); + let stream; + // We're passing callbacks into a method in the iframe here, because + // a Promise created in the iframe is unusable after the iframe has + // navigated away (see bug 1269400 for details). + return new Promise((resolve, reject) => + iframe.contentDocument.gUM({audio: true, video: true}, resolve, reject)) + .then(s => { + // We're cloning a stream containing identical tracks (an original + // and its clone) to test that ended works both for originals + // clones when they're both owned by the same MediaStream. + // (Bug 1274221) + stream = new MediaStream([].concat(s.getTracks(), s.getTracks()) + .map(t => t.clone())).clone(); + var allTracksEnded = Promise.all(stream.getTracks().map(t => { + info("Set up ended handler for track " + t.id); + return haveEvent(t, "ended", wait(50000)) + .then(event => { + info("ended handler invoked for track " + t.id); + is(event.target, t, "Target should be correct"); + }, e => e ? Promise.reject(e) + : ok(false, "ended event never raised for track " + t.id)); + })); + stream.getTracks().forEach(t => + is(t.readyState, "live", + "Non-ended track should have readyState 'live'")); + iframe.srcdoc = ""; + info("iframe has been reset. Waiting for tracks to end."); + return allTracksEnded; + }) + .then(() => stream.getTracks().forEach(t => + is(t.readyState, "ended", + "Ended track should have readyState 'ended'"))); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_groupId.html b/dom/media/webrtc/tests/mochitests/test_groupId.html new file mode 100644 index 0000000000..f2aefe5e80 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_groupId.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ title: "Test group id of MediaDeviceInfo", bug: "1213453" }); + +async function getDefaultDevices() { + const devices = await navigator.mediaDevices.enumerateDevices(); + is(devices.length, 2, "Two fake devices found."); + + devices.forEach(d => isnot(d.groupId, "", "GroupId is included in every device")); + + const videos = devices.filter(d => d.kind == "videoinput"); + is(videos.length, 1, "One video device found."); + const audios = devices.filter(d => d.kind == "audioinput"); + is(audios.length, 1, "One microphone device found."); + + return {audio: audios[0], video: videos[0]}; +} + +runTest(async () => { + // Force fake devices in order to be able to change camera name by pref. + await pushPrefs(["media.navigator.streams.fake", true], + ["media.audio_loopback_dev", ""], + ["media.video_loopback_dev", ""]); + + const afterGum = await navigator.mediaDevices.getUserMedia({ + video: true, audio: true + }); + afterGum.getTracks().forEach(track => track.stop()); + + let {audio, video} = await getDefaultDevices(); + + /* The low level method to correlate groupIds is by device names. + * Use a similar comparison here to verify that it works. + * Multiple devices of the same device name are not expected in + * automation. */ + isnot(audio.label, video.label, "Audio label differs from video"); + isnot(audio.groupId, video.groupId, "Not the same groupIds"); + // Change video name to match. + await pushPrefs(["media.getusermedia.fake-camera-name", audio.label]); + ({audio, video} = await getDefaultDevices()); + is(audio.label, video.label, "Audio label matches video"); + is(audio.groupId, video.groupId, "GroupIds should be the same"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_multi_mics.html b/dom/media/webrtc/tests/mochitests/test_multi_mics.html new file mode 100644 index 0000000000..f7b03f0fd5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_multi_mics.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "Test the ability of opening multiple microphones via gUM", + bug: "1238038", +}); + +runTest(async () => { + // Ensure we use the real microphones by disabling loopback devices and fake devices. + await pushPrefs(["media.audio_loopback_dev", ""], ["media.navigator.streams.fake", false]); + + try { + // Allow exposing microphone by calling gUM first + const defaultStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + for (const track of defaultStream.getTracks()) { + track.stop(); + } + + const devices = await navigator.mediaDevices.enumerateDevices(); + // Create constraints + let constraints = []; + devices.forEach((device) => { + if (device.kind === "audioinput") { + constraints.push({ + audio: { deviceId: { exact: device.deviceId } }, + }); + } + }); + if (constraints.length >= 2) { + // Open microphones by the constraints + let mediaStreams = []; + for (const c of constraints) { + let stream = await navigator.mediaDevices.getUserMedia(c); + mediaStreams.push(stream); + } + // Close microphones + for (const stream of mediaStreams) { + for (const track of stream.getTracks()) { + track.stop(); + } + } + mediaStreams = []; + } else { + dump("Skip test since we need at least two microphones\n"); + } + } catch (e) { + ok(false, e.name + ": " + e.message); + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_ondevicechange.html b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html new file mode 100644 index 0000000000..4358d9d748 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html @@ -0,0 +1,180 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "ondevicechange tests", + bug: "1152383" +}); + +async function resolveOnEvent(target, name) { + return new Promise(r => target.addEventListener(name, r, {once: true})); +} +let eventCount = 0; +async function triggerVideoDevicechange() { + ++eventCount; + // "media.getusermedia.fake-camera-name" specifies the name of the single + // fake video camera. + // Changing the pref imitates replacing one device with another. + return pushPrefs(["media.getusermedia.fake-camera-name", + `devicechange ${eventCount}`]) +} +function addIframe() { + const iframe = document.createElement("iframe"); + // Workaround for bug 1743933 + iframe.loadPromise = resolveOnEvent(iframe, "load"); + document.documentElement.appendChild(iframe); + return iframe; +} + +runTest(async () => { + // A toplevel Window and an iframe Windows are compared for devicechange + // events. + const iframe1 = addIframe(); + const iframe2 = addIframe(); + await Promise.all([ + iframe1.loadPromise, + iframe2.loadPromise, + pushPrefs( + // Use the fake video backend to trigger devicechange events. + ["media.navigator.streams.fake", true], + // Loopback would override fake. + ["media.video_loopback_dev", ""], + // Make fake devices count as real, permission-wise, or devicechange + // events won't be exposed + ["media.navigator.permission.fake", true], + // For gUM. + ["media.navigator.permission.disabled", true] + ), + ]); + const topDevices = navigator.mediaDevices; + const frame1Devices = iframe1.contentWindow.navigator.mediaDevices; + const frame2Devices = iframe2.contentWindow.navigator.mediaDevices; + // Initialization of MediaDevices::mLastPhysicalDevices is triggered when + // ondevicechange is set but tests "media.getusermedia.fake-camera-name" + // asynchronously. Wait for getUserMedia() completion to ensure that the + // pref has been read before doDevicechanges() changes it. + frame1Devices.ondevicechange = () => {}; + const topEventPromise = resolveOnEvent(topDevices, "devicechange"); + const frame2EventPromise = resolveOnEvent(frame2Devices, "devicechange"); + (await frame1Devices.getUserMedia({video: true})).getTracks()[0].stop(); + + await Promise.all([ + resolveOnEvent(frame1Devices, "devicechange"), + triggerVideoDevicechange(), + ]); + ok(true, + "devicechange event is fired when gUM has been in use"); + // The number of devices has not changed. Race a settled Promise to check + // that no devicechange event has been received in frame2. + const racer = {}; + is(await Promise.race([frame2EventPromise, racer]), racer, + "devicechange event is NOT fired in iframe2 for replaced device when " + + "gUM has NOT been in use"); + // getUserMedia() is invoked on frame2Devices after a first device list + // change but before returning to the previous state, in order to test that + // the device set is compared with the set after previous device list + // changes regardless of whether a "devicechange" event was previously + // dispatched. + (await frame2Devices.getUserMedia({video: true})).getTracks()[0].stop(); + // Revert device list change. + await Promise.all([ + resolveOnEvent(frame1Devices, "devicechange"), + resolveOnEvent(frame2Devices, "devicechange"), + SpecialPowers.popPrefEnv(), + ]); + ok(true, + "devicechange event is fired on return to previous list " + + "after gUM has been is use"); + + const frame1EventPromise1 = resolveOnEvent(frame1Devices, "devicechange"); + while (true) { + const racePromise = Promise.race([ + frame1EventPromise1, + // 100ms is half the coalescing time in MediaManager::DeviceListChanged(). + wait(100, {type: "wait done"}), + ]); + await triggerVideoDevicechange(); + if ((await racePromise).type == "devicechange") { + ok(true, + "devicechange event is fired even when hardware changes continue"); + break; + } + } + + is(await Promise.race([topEventPromise, racer]), racer, + "devicechange event is NOT fired for device replacements when " + + "gUM has NOT been in use"); + + if (navigator.userAgent.includes("Android")) { + todo(false, "test assumes Firefox-for-Desktop specific API and behavior"); + return; + } + // Open a new tab, which is expected to receive focus and hide the first tab. + const tab = window.open(); + SimpleTest.registerCleanupFunction(() => tab.close()); + await Promise.all([ + resolveOnEvent(document, 'visibilitychange'), + resolveOnEvent(tab, 'focus'), + ]); + ok(tab.document.hasFocus(), "tab.document.hasFocus()"); + await Promise.all([ + resolveOnEvent(tab, 'blur'), + SpecialPowers.spawnChrome([], function focusUrlBar() { + this.browsingContext.topChromeWindow.gURLBar.focus(); + }), + ]); + ok(!tab.document.hasFocus(), "!tab.document.hasFocus()"); + is(document.visibilityState, 'hidden', 'visibilityState') + const frame1EventPromise2 = resolveOnEvent(frame1Devices, "devicechange"); + const tabDevices = tab.navigator.mediaDevices; + tabDevices.ondevicechange = () => {}; + const tabStream = await tabDevices.getUserMedia({video: true}); + // Trigger and await two devicechanges on tabDevices to wait long enough to + // provide that a devicechange on another MediaDevices would be received. + for (let i = 0; i < 2; ++i) { + await Promise.all([ + resolveOnEvent(tabDevices, "devicechange"), + triggerVideoDevicechange(), + ]); + }; + is(await Promise.race([frame1EventPromise2, racer]), racer, + "devicechange event is NOT fired while tab is in background"); + tab.close(); + await resolveOnEvent(document, 'visibilitychange'); + is(document.visibilityState, 'visible', 'visibilityState') + await frame1EventPromise2; + ok(true, "devicechange event IS fired when tab returns to foreground"); + + const audioLoopbackDev = + SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!navigator.userAgent.includes("Linux")) { + todo_isnot(audioLoopbackDev, "", "audio_loopback_dev"); + return; + } + isnot(audioLoopbackDev, "", "audio_loopback_dev"); + await Promise.all([ + resolveOnEvent(topDevices, "devicechange"), + pushPrefs(["media.audio_loopback_dev", "none"]), + ]); + ok(true, + "devicechange event IS fired when last audio device is removed and " + + "gUM has NOT been in use"); + await Promise.all([ + resolveOnEvent(topDevices, "devicechange"), + pushPrefs(["media.audio_loopback_dev", audioLoopbackDev]), + ]); + ok(true, + "devicechange event IS fired when first audio device is added and " + + "gUM has NOT been in use"); +}); + +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html new file mode 100644 index 0000000000..b0a60ee46b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1246310", + title: "Renegotiation: add audio track to existing video-only stream", + }); + + runNetworkTest(function (options) { + SimpleTest.requestCompleteLog(); + + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. TEST_AUDIO_FREQ matches the frequency of the + // tone in fake microphone devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + + const test = new PeerConnectionTest(options); + test.chain.replace("PC_LOCAL_GUM", + [ + function PC_LOCAL_GUM_ATTACH_VIDEO_ONLY(test) { + var localConstraints = {audio: true, video: true}; + test.setMediaConstraints([{video: true}], []); + return getUserMedia(localConstraints) + .then(s => test.originalGumStream = s) + .then(() => is(test.originalGumStream.getAudioTracks().length, 1, + "Should have 1 audio track")) + .then(() => is(test.originalGumStream.getVideoTracks().length, 1, + "Should have 1 video track")) + .then(() => test.pcLocal.attachLocalTrack( + test.originalGumStream.getVideoTracks()[0], + test.originalGumStream)); + }, + ] + ); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ATTACH_SECOND_TRACK_AUDIO(test) { + test.setMediaConstraints([{audio: true, video: true}], []); + return test.pcLocal.attachLocalTrack( + test.originalGumStream.getAudioTracks()[0], + test.originalGumStream); + }, + ], + [ + function PC_CHECK_REMOTE_AUDIO_FLOW(test) { + return test.pcRemote.checkReceivingToneFrom(audioContext, test.pcLocal); + } + ] + ); + + return test.run() + .finally(() => tone.stop()); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html new file mode 100644 index 0000000000..c7536214e5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add DataChannel" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + commandsCreateDataChannel, + commandsCheckDataChannel); + + // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW + test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW', + commandsWaitForDataChannel, + false, + 1); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html new file mode 100644 index 0000000000..6ad754336c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add DataChannel" + }); + + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + commandsCreateDataChannel.concat( + [ + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + }, + ] + ), + commandsCheckDataChannel); + + // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW + test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW', + commandsWaitForDataChannel, + false, + 1); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html new file mode 100644 index 0000000000..d9618ea07a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add second audio stream" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}]); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + ], + [ + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + // We test both tracks to avoid an ordering problem + is(test.pcRemote._pc.getReceivers().length, 2, + "pcRemote should have two receivers"); + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even + // with --use-test-media-devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + return Promise.all(test.pcRemote._pc.getReceivers().map(r => { + const analyser = new AudioStreamAnalyser( + audioContext, new MediaStream([r.track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200); + })) + .finally(() => tone.stop()); + }, + ] + ); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html new file mode 100644 index 0000000000..aed13d7f3a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add second audio stream, no bundle" + }); + + runNetworkTest(function (options = {}) { + options.bundle = false; + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}]); + // Since this is a NoBundle variant, adding a track will cause us to + // go back to checking. + test.pcLocal.expectIceChecking(); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + }, + ], + [ + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + // We test both tracks to avoid an ordering problem + is(test.pcRemote._pc.getReceivers().length, 2, + "pcRemote should have two receivers"); + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even + // with --use-test-media-devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + return Promise.all(test.pcRemote._pc.getReceivers().map(r => { + const analyser = new AudioStreamAnalyser( + new AudioContext(), new MediaStream([r.track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200); + })) + .finally(() => tone.stop()); + }, + ] + ); + + // TODO(bug 1093835): figure out how to verify if media flows through the new stream + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html new file mode 100644 index 0000000000..1565958d01 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add second video stream" + }); + + runNetworkTest(async function (options) { + // Use fake video here since the native fake device on linux doesn't + // change color as needed by checkVideoPlaying() below. + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}]); + return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]); + }, + ], + [ + function PC_REMOTE_CHECK_VIDEO_FLOW(test) { + const h = new VideoStreamHelper(); + is(test.pcRemote.remoteMediaElements.length, 2, + "Should have two remote media elements after renegotiation"); + return Promise.all(test.pcRemote.remoteMediaElements.map(video => + h.checkVideoPlaying(video))); + }, + ] + ); + + test.setMediaConstraints([{video: true, fake: true}], [{video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html new file mode 100644 index 0000000000..2857100998 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: add second video stream, no bundle" + }); + + runNetworkTest(async function (options = {}) { + // Use fake video here since the native fake device on linux doesn't + // change color as needed by checkVideoPlaying() below. + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + options.bundle = false; + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}]); + // Since this is a NoBundle variant, adding a track will cause us to + // go back to checking. + test.pcLocal.expectIceChecking(); + return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + }, + ], + [ + function PC_REMOTE_CHECK_VIDEO_FLOW(test) { + const h = new VideoStreamHelper(); + is(test.pcRemote.remoteMediaElements.length, 2, + "Should have two remote media elements after renegotiation"); + return Promise.all(test.pcRemote.remoteMediaElements.map(video => + h.checkVideoPlaying(video))); + }, + ] + ); + + test.setMediaConstraints([{video: true}], [{video: true}]); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html new file mode 100644 index 0000000000..ff9ca9a772 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +createHTML({ + title: "MediaStream's 'addtrack' and 'removetrack' events with gUM", + bug: "1208328" +}); + +runNetworkTest(function (options) { + let test = new PeerConnectionTest(options); + let eventsPromise; + addRenegotiation(test.chain, + [ + function PC_LOCAL_SWAP_VIDEO_TRACKS(test) { + return getUserMedia({video: true}).then(stream => { + var videoTransceiver = test.pcLocal._pc.getTransceivers()[1]; + is(videoTransceiver.currentDirection, "sendonly", + "Video transceiver's current direction is sendonly"); + is(videoTransceiver.direction, "sendrecv", + "Video transceiver's desired direction is sendrecv"); + + const localStream = test.pcLocal._pc.getLocalStreams()[0]; + ok(localStream, "Should have local stream"); + + const remoteStream = test.pcRemote._pc.getRemoteStreams()[0]; + ok(remoteStream, "Should have remote stream"); + + const newTrack = stream.getTracks()[0]; + + const videoSenderIndex = + test.pcLocal._pc.getSenders().findIndex(s => s.track.kind == "video"); + isnot(videoSenderIndex, -1, "Should have video sender"); + + test.pcLocal.removeSender(videoSenderIndex); + is(videoTransceiver.direction, "recvonly", + "Video transceiver should be recvonly after removeTrack"); + test.pcLocal.attachLocalTrack(stream.getTracks()[0], localStream); + is(videoTransceiver.direction, "recvonly", + "Video transceiver should be recvonly after addTrack"); + + eventsPromise = haveEvent(remoteStream, "addtrack", + wait(50000, new Error("No addtrack event for " + newTrack.id))) + .then(trackEvent => { + ok(trackEvent instanceof MediaStreamTrackEvent, + "Expected event to be instance of MediaStreamTrackEvent"); + is(trackEvent.type, "addtrack", + "Expected addtrack event type"); + is(trackEvent.track.readyState, "live", + "added track should be live"); + }) + .then(() => haveNoEvent(remoteStream, "addtrack")); + }); + }, + ], + [ + function PC_REMOTE_CHECK_EVENTS(test) { + return eventsPromise; + }, + ] + ); + + test.setMediaConstraints([{audio: true, video: true}], []); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html new file mode 100644 index 0000000000..d9b01bf722 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: answerer adds second audio stream" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + addRenegotiationAnswerer(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}]); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + ] + ); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html new file mode 100644 index 0000000000..f6e77f8271 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + +createHTML({ + bug: "1765005", + title: "Verify audio channel limits for each negotiated audio codec", +}); + +const matchesChannels = (sdp, codec, channels) => + !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\/${channels}\r\n`, "g")) || + (channels <= 1 && + !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\r\n`, "g"))); + +async function testAudioChannels(track, codec, channels, accepted, expectedChannels) { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + try { + pc1.addTrack(track); + pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); + pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + let {type, sdp} = await pc2.createAnswer(); + sdp = sdp.replace(new RegExp(`a=rtpmap:(\\d*) ${codec}\/(\\d*)\/?\\d*\r\n`, "g"), + `a=rtpmap:$1 ${codec}/$2/${channels}\r\n`); + const payloadType = Number(sdputils.findCodecId(sdp, codec)); + sdp = sdputils.removeAllButPayloadType(sdp, payloadType); + ok(matchesChannels(sdp, codec, channels), "control"); + await pc2.setLocalDescription({type, sdp}); + is(matchesChannels(pc2.localDescription.sdp, codec, channels), + accepted, + `test pc2.localDescription`); + try { + await pc1.setRemoteDescription(pc2.localDescription); + ok(expectedChannels, "SRD should succeed iff we're expecting channels"); + const [receiver] = pc2.getReceivers(); + await new Promise(r => receiver.track.onunmute = r); + let stats = await receiver.getStats(); + let inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp"); + if (!inboundStat) { + info("work around bug 1765215"); // TODO bug 1765215 + await new Promise(r => setTimeout(r, 200)); + stats = await receiver.getStats(); + inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp"); + } + ok(inboundStat, "has inbound-rtp stats after track unmute (w/workaround)"); + const codecStat = stats.get(inboundStat.codecId); + ok(codecStat.mimeType.includes(codec), "correct codec"); + is(codecStat.payloadType, payloadType, "correct payloadType"); + is(codecStat.channels, expectedChannels, "expected channels"); + } catch (e) { + ok(!expectedChannels, "SRD should fail iff we're not expecting channels"); + } + } finally { + pc1.close(); + pc2.close(); + } +} + +runNetworkTest(async () => { + const [track] = (await navigator.mediaDevices.getUserMedia({audio: true})) + .getAudioTracks(); + try { + for (let [codec, channels, accepted, expectedChannels] of [ + ["opus", 2, true, 2], + ["opus", 1, true, 0], + ["opus", 1000, true, 0], + ["G722", 1, true, 1], + ["G722", 2, true, 0], + ["G722", 1000, true, 0], + ["PCMU", 1, true, 1], + ["PCMU", 2, false, 1], + ["PCMU", 1000, false, 1], + ["PCMA", 1, true, 1], + ["PCMA", 2, false, 1], + ["PCMA", 1000, false, 1] + ]) { + const testName = `${codec} with ${channels} channel(s) is ` + + `${accepted? "accepted" : "ignored"} and produces ` + + `${expectedChannels || "no"} channels`; + try { + info(`Testing that ${testName}`); + await testAudioChannels(track, codec, channels, accepted, expectedChannels); + } catch (e) { + ok(false, `Error testing that ${testName}: ${e}\n${e.stack}`); + } + } + } finally { + track.stop(); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html new file mode 100644 index 0000000000..41dc5f8860 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1395853", + title: "Verify audio content over WebRTC for every audio codec", + }); + + // We match the format member against the sdp to figure out the payload type, + // So all other present codecs can be removed. + const codecs = [ "opus", "G722", "PCMU", "PCMA" ]; + const audioContext = new AudioContext(); + + async function testAudioCodec(options = {}, codec) { + // sdputils checks for opus as part of its sdp sanity test + options.opus = codec == "opus"; + + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], []); + + test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [ + function PC_LOCAL_FILTER_OUT_CODECS() { + let otherCodec = codecs.find(c => c != codec); + let otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec); + + let id = sdputils.findCodecId(test.originalOffer.sdp, codec); + test.originalOffer.sdp = + sdputils.removeAllButPayloadType(test.originalOffer.sdp, id); + + ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")), + `Other codec ${otherId} should be removed after filtering`); + ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")), + `Tested codec ${id} should remain after filtering`); + + for (let c of codecs.filter(c => c != codec)) { + // Remove rtpmaps for the other codecs so sdp sanity tests pass. + let id = sdputils.findCodecId(test.originalOffer.sdp, c); + test.originalOffer.sdp = + sdputils.removeRtpMapForPayloadType(test.originalOffer.sdp, id); + } + + ok(!test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi")), + `Rtpmap of other codec ${otherId} should be removed after filtering`); + ok(test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${id}.*\\r\\n`, "gi")), + `Rtpmap of tested codec should remain after filtering`); + }, + ]); + + test.chain.append([ + async function CHECK_AUDIO_FLOW() { + try { + await test.pcRemote.checkReceivingToneFrom(audioContext, test.pcLocal); + ok(true, "input and output audio data matches"); + } catch(e) { + ok(false, `No audio flow: ${e}`); + } + }, + ]); + + await test.run(); + } + + runNetworkTest(async (options) => { + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. TEST_AUDIO_FREQ matches the frequency of the + // tone in fake microphone devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + + for (let codec of codecs) { + info(`Testing audio for codec ${codec}`); + try { + await testAudioCodec(options, codec); + } catch(e) { + ok(false, `Error in test for codec ${codec}: ${e}\n${e.stack}`); + } + info(`Tested audio for codec ${codec}`); + } + + tone.stop(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html new file mode 100644 index 0000000000..333b40a888 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1363667", + title: "Test audio receiver getContributingSources" + }); + + // test_peerConnection_audioSynchronizationSources.html tests + // much of the functionality of getContributingSources as the implementation + // is shared. + var testGetContributingSources = async (test) => { + const remoteReceiver = test.pcRemote.getReceivers()[0]; + const localReceiver = test.pcLocal.getReceivers()[0]; + + // Check that getContributingSources is empty as there is no MCU + is(remoteReceiver.getContributingSources().length, 0, + "remote contributing sources is empty"); + is(localReceiver.getContributingSources().length, 0, + "local contributing sources is empty"); + // Wait for the next JS event loop iteration, to clear the cache + await Promise.resolve().then(); + // Insert new entries as if there were an MCU + const csrc0 = 124756; + const timestamp0 = performance.now() + performance.timeOrigin; + const rtpTimestamp0 = 11111; + const hasAudioLevel0 = true; + // Audio level as expected to be received in RTP + const audioLevel0 = 34; + // Audio level as expected to be returned + const expectedAudioLevel0 = 10 ** (-audioLevel0 / 20); + + SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource( + csrc0, + timestamp0, + rtpTimestamp0, + hasAudioLevel0, + audioLevel0); + + const csrc1 = 5786; + const timestamp1 = timestamp0 - 200; + const rtpTimestamp1 = 22222; + const hasAudioLevel1 = false; + const audioLevel1 = 0; + + SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource( + csrc1, + timestamp1, + rtpTimestamp1, + hasAudioLevel1, + audioLevel1); + + const csrc2 = 93487; + const timestamp2 = timestamp0 - 200; + const rtpTimestamp2 = 333333; + const hasAudioLevel2 = true; + const audioLevel2 = 127; + + SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource( + csrc2, + timestamp2, + rtpTimestamp2, + hasAudioLevel2, + audioLevel2); + + const contributingSources = remoteReceiver.getContributingSources(); + is(contributingSources.length, 3, + "Expected number of contributing sources"); + + // Check that both inserted were returned + const source0 = contributingSources.find(c => c.source == csrc0); + ok(source0, "first csrc was found"); + + const source1 = contributingSources.find(c => c.source == csrc1); + ok(source1, "second csrsc was found"); + + // Add a small margin of error in the timestamps + const compareTimestamps = (ts1, ts2) => Math.abs(ts1 - ts2) < 100; + + // Check the CSRC with audioLevel + const isWithinErr = Math.abs(source0.audioLevel - expectedAudioLevel0) + < expectedAudioLevel0 / 50; + ok(isWithinErr, + `Contributing source has correct audio level. (${source0.audioLevel})`); + ok(compareTimestamps(source0.timestamp, timestamp0), + `Contributing source has correct timestamp (got ${source0.timestamp}), expected ${timestamp0}`); + is(source0.rtpTimestamp, rtpTimestamp0, + `Contributing source has correct RTP timestamp (${source0.rtpTimestamp}`); + // Check the CSRC without audioLevel + is(source1.audioLevel, undefined, + `Contributing source has no audio level. (${source1.audioLevel})`); + ok(compareTimestamps(source1.timestamp, timestamp1), + `Contributing source has correct timestamp (got ${source1.timestamp}, expected ${timestamp1})`); + is(source1.rtpTimestamp, rtpTimestamp1, + `Contributing source has correct RTP timestamp (${source1.rtpTimestamp}`); + // Check that a received RTP audio level 127 is exactly 0 + const source2 = contributingSources.find(c => c.source == csrc2); + ok(source2, "third csrc was found"); + is(source2.audioLevel, 0, + `Contributing source has audio level of 0 when RTP audio level is 127`); + // Check caching + is(JSON.stringify(contributingSources), + JSON.stringify(remoteReceiver.getContributingSources()), + "getContributingSources is cached"); + // Check that sources are sorted in descending order by time stamp + const timestamp3 = performance.now() + performance.timeOrigin; + const rtpTimestamp3 = 44444; + // Larger offsets are further back in time + const testOffsets = [3, 7, 5, 6, 1, 4]; + for (const offset of testOffsets) { + SpecialPowers.wrap(localReceiver).mozInsertAudioLevelForContributingSource( + offset, // Using offset for SSRC for convenience + timestamp3 - offset, + rtpTimestamp3, + true, + offset); + } + const sources = localReceiver.getContributingSources(); + const sourceOffsets = sources.map(s => s.source); + is(JSON.stringify(sourceOffsets), + JSON.stringify([...testOffsets].sort((a, b) => a - b)), + `Contributing sources are sorted in descending order by timestamp:` + + ` ${JSON.stringify(sources)}`); + }; + + var test; + runNetworkTest(async function(options) { + test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [testGetContributingSources]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.pcLocal.audioElementsOnly = true; + await pushPrefs(["privacy.reduceTimerPrecision", false]); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html new file mode 100644 index 0000000000..ebf4c7fabe --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1213773", + title: "Renegotiation: answerer uses a=inactive for audio" + }); + + runNetworkTest(function (options) { + const helper = new AudioStreamFlowingHelper(); + + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], []); + let haveFirstUnmuteEvent; + + test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONUNMUTE_1() { + haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute"); + } + ]); + + test.chain.append([ + function PC_REMOTE_CHECK_AUDIO_UNMUTED() { + return haveFirstUnmuteEvent; + }, + function PC_REMOTE_CHECK_AUDIO_FLOWING() { + return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]); + } + ]); + + addRenegotiation(test.chain, []); + + test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [ + function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) { + test._remote_answer.sdp = + sdputils.setAllMsectionsInactive(test._remote_answer.sdp); + } + ], false, 1); + + test.chain.append([ + function PC_REMOTE_CHECK_AUDIO_NOT_FLOWING() { + return helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]); + } + ]); + + test.chain.remove("PC_REMOTE_CHECK_STATS", 1); + test.chain.remove("PC_LOCAL_CHECK_STATS", 1); + test.chain.remove("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", 1); + + addRenegotiation(test.chain, []); + + test.chain.append([ + function PC_REMOTE_CHECK_AUDIO_FLOWING_2() { + return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]); + } + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html new file mode 100644 index 0000000000..32603b2e40 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1363667", + title: "Test audio receiver getSynchronizationSources" + }); + + var waitForSyncSources = async (test) => { + let receivers = [...test.pcRemote.getReceivers(), + ...test.pcLocal.getReceivers()]; + is(receivers.length, 2, "Expected number of receivers"); + // Wait for sync sources + while (true) { + if (receivers[0].getSynchronizationSources().length && + receivers[1].getSynchronizationSources().length) { + break; + } + await wait(250); + } + }; + + var testGetSynchronizationSources = async (test) => { + await waitForSyncSources(test); + let receivers = [...test.pcRemote.getReceivers(), + ...test.pcLocal.getReceivers()]; + is(receivers.length, 2, + `Expected number of receivers is 2. (${receivers.length})`); + for (let recv of receivers) { + let syncSources = recv.getSynchronizationSources(); + ok(syncSources, + "Receiver has Synchronization sources " + JSON.stringify(syncSources)); + is(syncSources.length, 1, "Each receiver has only a single sync source"); + let source = recv.getSynchronizationSources()[0]; + ok(source.audioLevel !== null, + `Synchronization source has audio level. (${source.audioLevel})`); + ok(source.audioLevel >= 0.0, + `Synchronization source audio level >= 0.0 (${source.audioLevel})`); + ok(source.audioLevel <= 1.0, + `Synchronization source audio level <= 1.0 (${source.audioLevel})`); + ok(source.timestamp, + `Synchronization source has timestamp (${source.timestamp})`); + const ageSeconds = + (window.performance.now() + window.performance.timeOrigin - + source.timestamp) / 1000; + ok(ageSeconds >= 0, + `Synchronization source timestamp is in the past`); + ok(ageSeconds < 2.5, + `Synchronization source timestamp is close to now`); + is(source.voiceActivityFlag, undefined, + "Synchronization source unsupported voiceActivity is undefined"); + } + }; + + var testSynchronizationSourceCached = async (test) => { + await waitForSyncSources(test); + let receivers = [...test.pcRemote.getReceivers(), + ...test.pcLocal.getReceivers()]; + is(receivers.length, 2, + `Expected number of receivers is 2. (${receivers.length})`); + let sourceSets = [[],[]]; + for (let sourceSet of sourceSets) { + for (let recv of receivers) { + let sources = recv.getSynchronizationSources(); + is(sources.length, 1, + `Expected number of sources is 1. (${sources.length})`); + sourceSet.push(sources); + } + // Busy wait 1s before trying again + let endTime = performance.now() + 1000; + while (performance.now() < endTime) {}; + } + is(JSON.stringify(sourceSets[0]), JSON.stringify(sourceSets[1]), + "Subsequent getSynchronizationSources calls are cached."); + }; + + var test; + runNetworkTest(function(options) { + test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [testGetSynchronizationSources, + testSynchronizationSourceCached]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.pcLocal.audioElementsOnly = true; + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html new file mode 100644 index 0000000000..6d66614e91 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1439001", + title: "Test audio unidirectional getSynchronizationSources" + }); + + var waitForSyncSources = async (test) => { + let receiver = test.pcRemote.getReceivers()[0]; + ok(receiver, "Remote has a receiver"); + // Wait for remote sync source + while (!receiver.getSynchronizationSources().length) { + await wait(250); + } + is(receiver.getSynchronizationSources().length, 1, + "Remote receiver has a synchronization source"); + // Make sure local has no sync source + is(test.pcLocal.getReceivers()[0].getSynchronizationSources().length, 0, + "Local receiver has no synchronization source"); + }; + /* + * Test to make sure that in unidirectional calls, the receiving end has + * synchronization sources with audio levels, and the sending end has none. + */ + var testGetSynchronizationSourcesUnidirectional = async (test) => { + await waitForSyncSources(test); + let receiver = test.pcRemote.getReceivers()[0]; + let syncSources = receiver.getSynchronizationSources(); + ok(syncSources.length, + "Receiver has Synchronization sources " + JSON.stringify(syncSources)); + is(syncSources.length, 1, "Receiver has only a single sync source"); + let syncSource = syncSources[0]; + ok(syncSource.audioLevel !== undefined, "SynchronizationSource has audioLevel"); + }; + + var test; + runNetworkTest(function(options) { + test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [testGetSynchronizationSourcesUnidirectional]); + test.setMediaConstraints([{audio: true}], []); + test.pcLocal.audioElementsOnly = true; + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html new file mode 100644 index 0000000000..5fd10a67f9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796892", + title: "Basic audio-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + // pc.js uses video elements by default, we want to test audio elements here + test.pcLocal.audioElementsOnly = true; + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html new file mode 100644 index 0000000000..a076bf80f1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1246011", + title: "Offer with dynamic PT but missing rtpmap" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + // we want Opus to get selected and 101 to be ignored + options.opus = true; + test = new PeerConnectionTest(options); + test.chain.insertBefore("PC_REMOTE_GET_OFFER", [ + function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) { + test.originalOffer.sdp = + sdputils.reduceAudioMLineToDynamicPtAndOpus(test.originalOffer.sdp); + test.originalOffer.sdp = + sdputils.removeAllRtpMaps(test.originalOffer.sdp); + test.originalOffer.sdp = test.originalOffer.sdp + "a=rtpmap:109 opus/48000/2\r\n"; + info("SDP with dyn PT and no Rtpmap: " + JSON.stringify(test.originalOffer)); + } + ]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html new file mode 100644 index 0000000000..180abc075a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1231975", + title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.block_tcp', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "srflx"; + options.expectedRemoteCandidateType = "relay"; + // If both have TURN, it is a toss-up which one will end up using a + // relay. + options.turn_disabled_local = true; + const test = new PeerConnectionTest(options); + // Make sure we don't end up choosing the wrong thing due to delays in + // trickle. Once we are willing to accept trickle after ICE success, we + // can maybe wait a bit to allow things to stabilize. + // TODO(bug 1238249) + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html new file mode 100644 index 0000000000..7bb51764bd --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1231975", + title: "Basic audio-only peer connection with port dependent NAT that blocks UDP" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.block_udp', true], + ['media.peerconnection.nat_simulator.block_tcp', false], + ['media.peerconnection.nat_simulator.block_tls', true], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "relay-tcp"; + options.expectedRemoteCandidateType = "relay-tcp"; + // No reason to wait for gathering to complete like the other NAT tests, + // since relayed-tcp is the only thing that can work. + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html new file mode 100644 index 0000000000..43ea6aaea7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "857668", + title: "Basic audio-only peer connection with UDP-blocking NAT, for verifying TCP relay with STUN 300 responses" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.block_udp', true], + ['media.peerconnection.nat_simulator.block_tcp', false], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "relay-tcp"; + options.expectedRemoteCandidateType = "relay-tcp"; + const turnServer = iceServersArray.find(server => "username" in server); + const turnRedirectPort = turnServer.turn_redirect_port; + const turnHostname = getTurnHostname(turnServer.urls[0]); + turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}?transport=tcp`]; + // Override turn servers so we can test simulated redirects + options.config_local = {iceServers: [turnServer]}; + options.config_remote = {iceServers: [turnServer]}; + const test = new PeerConnectionTest(options); + // Make sure we don't end up choosing the wrong thing due to delays in + // trickle. Once we are willing to accept trickle after ICE success, we + // can maybe wait a bit to allow things to stabilize. + // TODO(bug 1238249) + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + await SpecialPowers.popPrefEnv(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html new file mode 100644 index 0000000000..7446401f87 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1231975", + title: "Basic audio-only peer connection with port dependent NAT that blocks STUN" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.block_udp', true], + ['media.peerconnection.nat_simulator.block_tcp', true], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "relay-tls"; + options.expectedRemoteCandidateType = "relay-tls"; + // No reason to wait for gathering to complete like the other NAT tests, + // since relayed-tcp is the only thing that can work. + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html new file mode 100644 index 0000000000..286e67bc2f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "857668", + title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay with STUN 300 responses" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.block_tcp', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "srflx"; + options.expectedRemoteCandidateType = "relay"; + const turnServer = iceServersArray.find(server => "username" in server); + const turnRedirectPort = turnServer.turn_redirect_port; + const turnHostname = getTurnHostname(turnServer.urls[0]); + turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}`]; + // Override turn servers so we can test redirects + options.config_remote = {iceServers: [turnServer]}; + // If both have TURN, it is a toss-up which one will end up using a + // relay, so we disable TURN for one side. + options.turn_disabled_local = true; + const test = new PeerConnectionTest(options); + // Make sure we don't end up choosing the wrong thing due to delays in + // trickle. Once we are willing to accept trickle after ICE success, we + // can maybe wait a bit to allow things to stabilize. + // TODO(bug 1238249) + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html new file mode 100644 index 0000000000..78fa8bcb2c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1231975", + title: "Basic audio-only peer connection with endpoint independent NAT, for verifying UDP srflx" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.block_tcp', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "srflx"; + options.expectedRemoteCandidateType = "srflx"; + const test = new PeerConnectionTest(options); + // Make sure we don't end up choosing the wrong thing due to delays in + // trickle. Once we are willing to accept trickle after ICE success, we + // can maybe wait a bit to allow things to stabilize. + // TODO(bug 1238249) + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html new file mode 100644 index 0000000000..297121cd94 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1231975", + title: "Basic audio-only peer connection where UDP sockets return errors on send" +}); + +// This test uses the NAT simulator, which doesn't work in https, so we turn +// on getUserMedia in http, which requires a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.block_udp', true], + ['media.peerconnection.nat_simulator.error_code_for_drop', 3 /*R_INTERNAL*/], + ['media.peerconnection.nat_simulator.block_tls', true], + ['media.getusermedia.insecure.enabled', true]); + options.expectedLocalCandidateType = "relay-tcp"; + options.expectedRemoteCandidateType = "relay-tcp"; + // No reason to wait for gathering to complete like the other NAT tests, + // since relayed-tcp is the only thing that can work. + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + await test.run(); + }, { useIceServer: true }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html new file mode 100644 index 0000000000..f0fe721b8e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1221837", + title: "Only offer PCMA and PMCU in mline (no rtpmaps)" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.opus = false; + test = new PeerConnectionTest(options); + test.chain.insertBefore("PC_REMOTE_GET_OFFER", [ + function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) { + test.originalOffer.sdp = + sdputils.reduceAudioMLineToPcmuPcma(test.originalOffer.sdp); + test.originalOffer.sdp = + sdputils.removeAllRtpMaps(test.originalOffer.sdp); + info("SDP without Rtpmaps: " + JSON.stringify(test.originalOffer)); + } + ]); + test.chain.insertAfter("PC_REMOTE_SANE_LOCAL_SDP", [ + function PC_REMOTE_VERIFY_PCMU(test) { + ok(test._remote_answer.sdp.includes("a=rtpmap:0 PCMU/8000"), "PCMU codec is present in SDP"); + } + ]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html new file mode 100644 index 0000000000..ced57ff8a3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1663746", + title: "Basic tests for relay ice policy" +}); + +runNetworkTest(async () => { + await pushPrefs( + // Enable mDNS, since there are some checks we want to run with that + ['media.peerconnection.ice.obfuscate_host_addresses', true]); + + const offerer = new RTCPeerConnection({iceServers: iceServersArray, iceTransportPolicy: 'relay'}); + const answerer = new RTCPeerConnection({iceServers: iceServersArray}); + + offerer.onicecandidate = e => { + if (e.candidate) { + ok(!e.candidate.candidate.includes(' host '), 'IceTransportPolicy \"relay\" should prevent the advertisement of host candidates'); + ok(!e.candidate.candidate.includes(' srflx '), 'IceTransportPolicy \"relay\" should prevent the advertisement of srflx candidates'); + } + answerer.addIceCandidate(e.candidate); + }; + + answerer.onicecandidate = e => { + if (e.candidate && e.candidate.candidate.includes(' host ')) { + ok(e.candidate.candidate.includes('.local'), 'When obfuscate_host_addresses is true, we expect host candidates to use mDNS'); + } + offerer.addIceCandidate(e.candidate); + }; + + const offererConnected = new Promise(r => { + offerer.oniceconnectionstatechange = () => { + if (offerer.iceConnectionState == 'connected') { + r(); + } + }; + }); + + const answererConnected = new Promise(r => { + answerer.oniceconnectionstatechange = () => { + if (answerer.iceConnectionState == 'connected') { + r(); + } + }; + }); + + const offer = await offerer.createOffer({offerToReceiveAudio: true}); + await Promise.all([offerer.setLocalDescription(offer), answerer.setRemoteDescription(offer)]); + const answer = await answerer.createAnswer(); + await Promise.all([answerer.setLocalDescription(answer), offerer.setRemoteDescription(answer)]); + + info('Waiting for ICE to connect'); + await Promise.all([offererConnected, answererConnected]); + + const offererStats = await offerer.getStats(); + const localCandidates = [...offererStats.values()].filter(stat => stat.type == 'local-candidate'); + const remoteCandidates = [...offererStats.values()].filter(stat => stat.type == 'remote-candidate'); + isnot(localCandidates, []); + isnot(remoteCandidates, []); + + const localNonRelayCandidates = + localCandidates.filter(cand => cand.candidateType != 'relay'); + is(localNonRelayCandidates.length, 0, `There should only be local relay candidates, because we are using the "relay" IceTransportPolicy, but we got ${JSON.stringify(localNonRelayCandidates)}`); + + const remoteHostCandidates = + remoteCandidates.filter(cand => cand.candidateType == 'host'); + is(remoteHostCandidates.length, 0, `There should be no remote host candidates in the stats, because mDNS resolution should have been disabled by the "relay" IceTransportPolicy, but we got ${JSON.stringify(remoteHostCandidates)}`); + + offerer.close(); + answerer.close(); + + await SpecialPowers.popPrefEnv(); +}, { useIceServer: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html new file mode 100644 index 0000000000..afad4550d4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1167443", + title: "Basic audio-only peer connection which waits for end-of-candidates" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleSdp.then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label)); + } + ]); + test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcRemote.endOfTrickleSdp.then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label)); + } + ]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html new file mode 100644 index 0000000000..f28a990bd2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1416932", + title: "Basic audio-only peer connection and verify rtp header extensions" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + // pc.js uses video elements by default, we want to test audio elements here + test.pcLocal.audioElementsOnly = true; + + let getRtpPacket = (pc) => { + // we only examine received packets + let sending = false; + pc.mozEnablePacketDump(0, "rtp", sending); + return new Promise((res, rej) => + pc.mozSetPacketCallback((...args) => { + res([...args]); + pc.mozSetPacketCallback(() => {}); + pc.mozDisablePacketDump(0, "rtp", sending); + }) + ); + } + + const pc = SpecialPowers.wrap(test.pcRemote._pc); + const haveFirstPacket = getRtpPacket(pc); + + test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [ + async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() { + + const sdpExtmapIds = sdputils.findExtmapIds(test.originalAnswer.sdp); + + const [level, type, sending, data] = await haveFirstPacket; + const extensions = ParseRtpPacket(data).header.extensions; + + // make sure we got the same number of rtp header extensions in + // the received packet as were negotiated in the sdp. Then + // check to make sure each of the received extension ids were in + // the sdp. + is(sdpExtmapIds.length, extensions.length, "number of received ids match sdp ids"); + // note, we are comparing a number (from the parsed rtp packet) + // and a string (from the answer sdp) + ok(extensions.every((ext) => sdpExtmapIds.includes(""+ext.id)), "extension id arrays equivalent"); + } + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html new file mode 100644 index 0000000000..c2c2d43f09 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796890", + title: "Basic audio/video (separate) peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html new file mode 100644 index 0000000000..02a561f9b8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796890", + title: "Basic audio/video (combined) peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true, video: true}], + [{audio: true, video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html new file mode 100644 index 0000000000..cae7f6617f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1016476", + title: "Basic audio/video peer connection with no Bundle" + }); + + runNetworkTest(options => { + options = options || { }; + options.bundle = false; + var test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html new file mode 100644 index 0000000000..49b0136752 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1167443", + title: "Basic audio & video call with disabled bundle and disabled RTCP-Mux" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + options.rtcpmux = false; + test = new PeerConnectionTest(options); + test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleSdp .then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label)); + } + ]); + test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcRemote.endOfTrickleSdp .then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label)); + } + ]); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html new file mode 100644 index 0000000000..48524604ba --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1167443", + title: "Basic audio & video call with disabled RTCP-Mux" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.rtcpmux = false; + test = new PeerConnectionTest(options); + test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleSdp .then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label)); + } + ]); + test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [ + function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) { + return test.pcRemote.endOfTrickleSdp .then(sdp => + sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label)); + } + ]); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html new file mode 100644 index 0000000000..181d089d26 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1290948", + title: "Basic audio/video with addTransceiver" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + test.chain.replace("PC_LOCAL_GUM", + [ + function PC_LOCAL_GUM_TRANSCEIVERS(test) { + return test.pcLocal.getAllUserMediaAndAddTransceivers(test.pcLocal.constraints); + } + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html new file mode 100644 index 0000000000..e3da00bfa5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1406529", + title: "Verify SDP extmap attribute for sendrecv connection" + }); + + var test; + runNetworkTest(async function (options) { + await pushPrefs(["media.navigator.video.use_transport_cc", true]); + + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + + test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [ + async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() { + sdputils.verify_unique_extmap_ids(test.originalOffer.sdp); + + const audio = sdputils.findExtmapIdsUrnsDirections( + sdputils.getAudioMSections(test.originalOffer.sdp)); + const expected_audio = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""], + ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"], + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""], + ]; + // *Ugh* ... + ok(JSON.stringify(audio) === + JSON.stringify(expected_audio), + "List of offer audio URNs meets expected values"); + + const video = sdputils.findExtmapIdsUrnsDirections( + sdputils.getVideoMSections(test.originalOffer.sdp)); + const expected_video = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""], + ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""], + ["5", "urn:ietf:params:rtp-hdrext:toffset", ""], + ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"], + ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""], + ]; + // *Ugh* ... + ok(JSON.stringify(video) === + JSON.stringify(expected_video), + "List of offer video URNs meets expected values"); + } + ]); + + test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION'); + test.chain.append([ + async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() { + sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp); + + const audio = sdputils.findExtmapIdsUrnsDirections( + sdputils.getAudioMSections(test.originalAnswer.sdp)); + const expected_audio = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""], + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""], + ]; + // *Ugh* ... + ok(JSON.stringify(audio) === + JSON.stringify(expected_audio), + "List of answer audio URNs meets expected values"); + + const video = sdputils.findExtmapIdsUrnsDirections( + sdputils.getVideoMSections(test.originalAnswer.sdp)); + const expected_video = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""], + ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""], + ["5", "urn:ietf:params:rtp-hdrext:toffset",""], + ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""], + ]; + ok(JSON.stringify(video) === + JSON.stringify(expected_video), + "List of answer video URNs meets expected values"); + } + ]); + + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html new file mode 100644 index 0000000000..6cbc9e4c00 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1406529", + title: "Verify SDP extmap attribute for sendonly connection" + }); + + var test; + runNetworkTest(async function (options) { + await pushPrefs(["media.navigator.video.use_transport_cc", true]); + + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + []); + + test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [ + async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() { + sdputils.verify_unique_extmap_ids(test.originalOffer.sdp); + + const audio = sdputils.findExtmapIdsUrnsDirections( + sdputils.getAudioMSections(test.originalOffer.sdp)); + const expected_audio = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""], + ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"], + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""], + ]; + // *Ugh* ... + ok(JSON.stringify(audio) === + JSON.stringify(expected_audio), + "List of offer audio URNs meets expected values"); + + const video = sdputils.findExtmapIdsUrnsDirections( + sdputils.getVideoMSections(test.originalOffer.sdp)); + const expected_video = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""], + ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""], + ["5", "urn:ietf:params:rtp-hdrext:toffset", ""], + ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"], + ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""], + ]; + // *Ugh* ... + ok(JSON.stringify(video) === + JSON.stringify(expected_video), + "List of offer video URNs meets expected values"); + } + ]); + + test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION'); + test.chain.append([ + async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() { + sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp); + + const audio = sdputils.findExtmapIdsUrnsDirections( + sdputils.getAudioMSections(test.originalAnswer.sdp)); + const expected_audio = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""], + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""], + ]; + // *Ugh* ... + ok(JSON.stringify(audio) === + JSON.stringify(expected_audio), + "List of answer audio URNs meets expected values"); + + const video = sdputils.findExtmapIdsUrnsDirections( + sdputils.getVideoMSections(test.originalAnswer.sdp)); + const expected_video = [ + /* Please modify this list when you add or remove RTP header + extensions. */ + ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""], + ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""], + ["5", "urn:ietf:params:rtp-hdrext:toffset",""], + ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""], + ]; + ok(JSON.stringify(video) === + JSON.stringify(expected_video), + "List of answer video URNs meets expected values"); + } + ]); + + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html new file mode 100644 index 0000000000..70d27b48c6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1427009", + title: "Test mid longer than 16 characters fails" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + + test.chain.replaceAfter("PC_LOCAL_CREATE_OFFER", + [ + function PC_LOCAL_MUNGE_OFFER_SDP(test) { + test.originalOffer.sdp = + test.originalOffer.sdp.replace(/a=mid:.*\r\n/g, + "a=mid:really_long_mid_over_16_chars\r\n"); + }, + function PC_LOCAL_EXPECT_SET_LOCAL_DESCRIPTION_FAIL(test) { + return test.setLocalDescription(test.pcLocal, + test.originalOffer, + HAVE_LOCAL_OFFER) + .then(() => ok(false, "setLocalDescription must fail"), + // This needs to be RTCError once we support it, and once we + // stop allowing any modification, InvalidModificationError + e => is(e.name, "OperationError", + "setLocalDescription must fail and did")); + } + ], 0 // first occurance + ); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html new file mode 100644 index 0000000000..95bfb06514 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1437366", + title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (49000Hz)" +}); + +test_peerconnection_audio_forced_sample_rate(49000); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html new file mode 100644 index 0000000000..aab9778971 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1437366", + title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (24000Hz)" +}); + +test_peerconnection_audio_forced_sample_rate(24000); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html new file mode 100644 index 0000000000..072c35da39 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1040346", + title: "Basic H.264 GMP video-only peer connection" + }); + + var test; + runNetworkTest(async function (options) { + matchPlatformH264CodecPrefs(); + options = options || { }; + options.h264 = true; + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html new file mode 100644 index 0000000000..a90e63ced7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1039666", + title: "Basic screenshare-only peer connection" + }); + + async function supportedVideoPayloadTypes() { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return sdputils.getPayloadTypes(offer.sdp); + } + + async function testScreenshare(payloadType) { + const options = {}; + options.h264 = payloadType == 97 || payloadType == 126; + const test = new PeerConnectionTest(options); + const constraints = { + video: { mediaSource: "screen" }, + }; + test.setMediaConstraints([constraints], []); + test.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [ + function PC_LOCAL_ISOLATE_CODEC() { + info(`Forcing payload type ${payloadType}. Note that other associated ` + + `payload types, like RTX, are removed too.`); + test.originalOffer.sdp = + sdputils.removeAllButPayloadType(test.originalOffer.sdp, payloadType); + }, + ]); + await test.run(); + } + + runNetworkTest(async () => { + await SpecialPowers.pushPrefEnv({ set: [["media.navigator.video.red_ulpfec_enabled", true]] }); + await matchPlatformH264CodecPrefs(); + const pts = await supportedVideoPayloadTypes(); + ok(pts.includes("120"), "VP8 is supported"); + ok(pts.includes("121"), "VP9 is supported"); + if (pts.length > 2) { + is(pts.length, 6, "Expected VP8, VP9, two variants of H264, ULPFEC, and RED"); + ok(pts.includes("97"), "H264 with no packetization-mode is supported"); + ok(pts.includes("126"), "H264 with packetization-mode=1 is supported"); + ok(pts.includes("122"), "RED is supported"); + ok(pts.includes("123"), "ULPFEC is supported"); + } + for (const pt of pts) { + if (pt == "122" || pt == "123") { + // ULPFEC and RED are meant to work combined with other codecs. + // Forcing sdp with only one of them is not supported and will result in failures. + continue; + } + await testScreenshare(pt); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html new file mode 100644 index 0000000000..4a0655d696 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "796888", + title: "Basic video-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html new file mode 100644 index 0000000000..7874e52a10 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1416932", + title: "Basic video-only peer connection and verify rtp header extensions" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + + let getRtpPacketWithExtension = (pc, extension) => { + // we only examine received packets + let sending = false; + pc.mozEnablePacketDump(0, "rtp", sending); + return new Promise((res, rej) => + pc.mozSetPacketCallback((...args) => { + const packet = ParseRtpPacket(args[3]); + info(`midId = ${extension} packet = ${JSON.stringify(packet, null, 2)}`); + if (packet.header.extensions.find(e => e.id == extension) !== undefined) { + res(packet); + pc.mozSetPacketCallback(() => {}); + pc.mozDisablePacketDump(0, "rtp", sending); + } + }) + ); + } + + let havePacketWithMid; + let sdpExtmaps; + + // MID can stop being sent when acked causing failures if packets are checked later. + // Starting packet sniffer before PC_LOCAL_SET_REMOTE_DESCRIPTION to be ready + // to inspect packets ahead of any packets arriving. + test.chain.insertBefore('PC_LOCAL_SET_REMOTE_DESCRIPTION', [ + function PC_REMOTE_FIND_RTP_PACKETS_WITH_MIDID() { + + sdpExtmaps = sdputils.findExtmapIdsUrnsDirections(test.originalAnswer.sdp); + const [midId] = sdpExtmaps.find(([, urn]) => urn == "urn:ietf:params:rtp-hdrext:sdes:mid"); + const pc = SpecialPowers.wrap(test.pcRemote._pc); + havePacketWithMid = getRtpPacketWithExtension(pc, midId); + } + ]); + + test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [ + async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() { + + const sdpExtmapIds = sdpExtmaps.map(e => e[0]); + const packet = await havePacketWithMid; + const extIds = packet.header.extensions.map(e => `${e.id}`); + // make sure we got the same number of rtp header extensions in + // the received packet as were negotiated in the sdp. Then + // check to make sure each of the received extension ids were in + // the sdp. + is(sdpExtmapIds.length, extIds.length, + `number of sdp ids match received ids ` + + `${JSON.stringify(sdpExtmapIds)} == ${JSON.stringify(extIds)}\n` + + `sdp = ${test.originalAnswer.sdp}\n` + + `packet = ${JSON.stringify(packet, null, 2)}`); + // note, we are comparing a number (from the parsed rtp packet) + // and a string (from the answer sdp) + ok(extIds.every(id => sdpExtmapIds.includes(id)) && + sdpExtmapIds.every(id => extIds.includes(id)), + `extension id arrays equivalent`); + } + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html new file mode 100644 index 0000000000..1cfb0797db --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1038926", + title: "Basic windowshare-only peer connection" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + const constraints = { + video: { mediaSource: "window" }, + }; + test.setMediaConstraints([constraints], []); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html new file mode 100644 index 0000000000..a8c7004793 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1013809", + title: "Audio-only peer connection with swapped setLocal and setRemote steps" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION"); + test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html new file mode 100644 index 0000000000..a84dcf9d09 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1040346", + title: "Basic H.264 GMP video-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.h264 = true; + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + test.chain.removeAfter("PC_LOCAL_CREATE_OFFER"); + + test.chain.append([ + function PC_LOCAL_VERIFY_H264_OFFER(test) { + ok(!test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=0x42e0"), + "H264 offer does not contain profile-level-id=0x42e0"); + ok(test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=42e0"), + "H264 offer contains profile-level-id=42e0"); + } + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html new file mode 100644 index 0000000000..41e4aec457 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1227781", + title: "Test with invalid TURN server" + }); + + const turnConfig = { + iceServers: [ + { + username: "mozilla", + credential: "mozilla", + url: "turn:test@10.0.0.1", + }, + ], + }; + runNetworkTest(function (options) { + let exception = false; + try { + new RTCPeerConnection(turnConfig); + } catch (e) { + info(e); + exception = true; + } + is(exception, true, "Exception fired"); + ok("Success"); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html new file mode 100644 index 0000000000..e6451becea --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1512281", + title: "Test that RTCP sender and receiver stats are not swapped" + }); + +const ensure_missing_rtcp = async stats => { + const rtcp_stats = [...stats.values()].filter( + s => s.type.endsWith("bound-rtp") && + s.isRemote == true).map(s => JSON.stringify(s)) + is(rtcp_stats, [], + "There are no RTCP stats when RTCP reception is turned off"); +}; + +const PC_LOCAL_TEST_FOR_MISSING_RTCP = async test => + await ensure_missing_rtcp(await test.pcLocal.getStats()); + +const PC_REMOTE_TEST_FOR_MISSING_RTCP = async test => + await ensure_missing_rtcp(await test.pcRemote.getStats()); + +runNetworkTest(async options => { + await pushPrefs(["media.webrtc.net.force_disable_rtcp_reception", true]); + + const test = new PeerConnectionTest(options); + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_FOR_MISSING_RTCP]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_FOR_MISSING_RTCP]); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + await test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html new file mode 100644 index 0000000000..9e6d79a107 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1773067", + title: "getStats on a closed peer connection should fail, not hang, " + + " until bug 1056433 is fixed" + }); + + // TODO: Bug 1056433 removes the need for this test + runNetworkTest(async function () { + let errorName; + try { + const pc = new RTCPeerConnection(); + pc.close(); + await pc.getStats(); + } catch(e) { + errorName = e.name; + } + is(errorName, + "InvalidStateError", + "getStats on closed peer connection fails instead of hanging"); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html new file mode 100644 index 0000000000..fceb2c2a1d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "822674", + title: "RTCPeerConnection isn't a true javascript object as it should be" + }); + + runNetworkTest(function () { + var pc = new RTCPeerConnection(); + + pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface = 1; + is(pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface, 1, + "Can set expandos on an RTCPeerConnection"); + + pc = null; + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html new file mode 100644 index 0000000000..5cd168af8a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "825703", + title: "RTCConfiguration valid/invalid permutations" + }); + +// ^^^ Don't insert data above this line without adjusting line number below! +var lineNumberAndFunction = { +// <--- 16 is the line this must be. + line: 17, func: () => new RTCPeerConnection().onaddstream = () => {} +}; + +var makePC = (config, expected_error) => { + var exception; + try { + new RTCPeerConnection(config).close(); + } catch (e) { + exception = e; + } + is((exception? exception.name : "success"), expected_error || "success", + "RTCPeerConnection(" + JSON.stringify(config) + ")"); +}; + +// The order of properties in objects is not guaranteed in JavaScript, so this +// transform produces json-comparable dictionaries. The resulting copy is only +// meant to be used in comparisons (e.g. array-ness is not preserved). + +var toComparable = o => + (typeof o != 'object' || !o)? o : Object.keys(o).sort().reduce((co, key) => { + co[key] = toComparable(o[key]); + return co; +}, {}); + +// This is a test of the iceServers parsing code + readable errors +runNetworkTest(() => { + var exception = null; + + try { + new RTCPeerConnection().close(); + } catch (e) { + exception = e; + } + ok(!exception, "RTCPeerConnection() succeeds"); + exception = null; + + // Some overlap still with WPT RTCConfiguration-iceServers.html + + makePC({ iceServers: [ + { urls:"stun:127.0.0.1" }, + { urls:"stun:localhost", foo:"" }, + { urls: ["stun:127.0.0.1", "stun:localhost"] }, + { urls:"stuns:localhost", foo:"" }, + { urls:"turn:[::1]:3478", username:"p", credential:"p" }, + { urls:"turn:[::1]:3478", username:"", credential:"" }, + { urls:"turns:[::1]:3478", username:"", credential:"" }, + { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" }, + { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" }, + { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" }, + { url:"stun:localhost", foo:"" }, + { url:"turn:localhost", username:"p", credential:"p" } + ]}); + + makePC({ iceServers: [{ urls:"http:0.0.0.0" }] }, "SyntaxError"); + + try { + new RTCPeerConnection({ iceServers: [{ url:"http:0.0.0.0" }] }).close(); + } catch (e) { + ok(e.message.indexOf("http") > 0, + "RTCPeerConnection() constructor has readable exceptions"); + } + + // Test getConfiguration + const config = { + bundlePolicy: "max-bundle", + iceTransportPolicy: "relay", + peerIdentity: null, + certificates: [], + iceServers: [ + { urls: ["stun:127.0.0.1", "stun:localhost"], credentialType:"password" }, + { urls: ["turn:[::1]:3478"], username:"p", credential:"p", credentialType:"password" }, + ], + }; + // Make sure sdpSemantics is not exposed in getConfiguration + const configWithExtraProps = Object.assign({}, + config, + {sdpSemantics: "plan-b"}); + ok("sdpSemantics" in configWithExtraProps, "sdpSemantics control"); + + const pc = new RTCPeerConnection(configWithExtraProps); + is(JSON.stringify(toComparable(pc.getConfiguration())), + JSON.stringify(toComparable(config)), "getConfiguration"); + pc.close(); + + var push = prefs => SpecialPowers.pushPrefEnv(prefs); + + return Promise.resolve() + // This set of tests are setting the about:config User preferences for default + // ice servers and checking the outputs when RTCPeerConnection() is + // invoked. See Bug 1167922 for more information. + .then(() => push({ set: [['media.peerconnection.default_iceservers', ""]] }) + .then(() => makePC()) + .then(() => push({ set: [['media.peerconnection.default_iceservers', "k"]] })) + .then(() => makePC()) + .then(() => push({ set: [['media.peerconnection.default_iceservers', "[{\"urls\": [\"stun:stun.services.mozilla.com\"]}]"]] })) + .then(() => makePC())) + // This set of tests check that warnings work. See Bug 1254839 for more. + .then(() => { + let promise = new Promise(resolve => { + SpecialPowers.registerConsoleListener(msg => { + if (msg.message.includes("onaddstream")) { + SpecialPowers.postConsoleSentinel(); + resolve(msg.message); + } + }); + }); + lineNumberAndFunction.func(); + return promise; + }).then(warning => { + is(warning.split('"')[1], + "WebRTC: onaddstream is deprecated! Use peerConnection.ontrack instead.", + "warning logged"); + var remainder = warning.split('"').slice(2).join('"'); + info(remainder); + ok(remainder.includes('file: "' + window.location + '"'), + "warning has this file"); + ok(remainder.includes('line: ' + lineNumberAndFunction.line), + "warning has correct line number"); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html new file mode 100644 index 0000000000..06cfde9e5d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "827843", + title: "Ensure that localDescription and remoteDescription are null after close" + }); + +var steps = [ + function CHECK_SDP_ON_CLOSED_PC(test) { + var description; + var exception = null; + + test.pcLocal.close(); + + try { description = test.pcLocal.localDescription; } catch (e) { exception = e; } + ok(exception, "Attempt to access localDescription of pcLocal after close throws exception"); + exception = null; + + try { description = test.pcLocal.remoteDescription; } catch (e) { exception = e; } + ok(exception, "Attempt to access remoteDescription of pcLocal after close throws exception"); + exception = null; + + test.pcRemote.close(); + + try { description = test.pcRemote.localDescription; } catch (e) { exception = e; } + ok(exception, "Attempt to access localDescription of pcRemote after close throws exception"); + exception = null; + + try { description = test.pcRemote.remoteDescription; } catch (e) { exception = e; } + ok(exception, "Attempt to access remoteDescription of pcRemote after close throws exception"); + } +]; + +var test; +runNetworkTest(() => { + test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.append(steps); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html new file mode 100644 index 0000000000..6d8ca2a7ce --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "834153", + title: "Queue CreateAnswer in PeerConnection.js" + }); + + runNetworkTest(function () { + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + return pc1.createOffer({ offerToReceiveAudio: true }).then(offer => { + // The whole point of this test is not to wait for the + // setRemoteDescription call to succesfully complete, so we + // don't wait for it to succeed. + pc2.setRemoteDescription(offer); + return pc2.createAnswer(); + }) + .then(answer => is(answer.type, "answer", "CreateAnswer created an answer")) + .catch(reason => ok(false, reason.message)) + .then(() => { + pc1.close(); + pc2.close(); + }) + .catch(reason => ok(false, reason.message)); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html new file mode 100644 index 0000000000..4c890e4400 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "PeerConnection using callback functions", + bug: "1119593", + visible: true + }); + +// This still aggressively uses promises, but it is testing that the callback functions +// are properly in place. + +// wrapper that turns a callback-based function call into a promise +function pcall(o, f, beforeArg) { + return new Promise((resolve, reject) => { + var args = [resolve, reject]; + if (typeof beforeArg !== 'undefined') { + args.unshift(beforeArg); + } + info('Calling ' + f.name); + f.apply(o, args); + }); +} + +var pc1 = new RTCPeerConnection(); +var pc2 = new RTCPeerConnection(); + +var pc2_haveRemoteOffer = new Promise(resolve => { + pc2.onsignalingstatechange = + e => (e.target.signalingState == "have-remote-offer") && resolve(); +}); +var pc1_stable = new Promise(resolve => { + pc1.onsignalingstatechange = + e => (e.target.signalingState == "stable") && resolve(); +}); + +pc1.onicecandidate = e => { + pc2_haveRemoteOffer + .then(() => !e.candidate || pcall(pc2, pc2.addIceCandidate, e.candidate)) + .catch(generateErrorCallback()); +}; +pc2.onicecandidate = e => { + pc1_stable + .then(() => !e.candidate || pcall(pc1, pc1.addIceCandidate, e.candidate)) + .catch(generateErrorCallback()); +}; + +var v1, v2; +var delivered = new Promise(resolve => { + pc2.onaddstream = e => { + v2.srcObject = e.stream; + resolve(e.stream); + }; +}); + +runNetworkTest(function() { + v1 = createMediaElement('video', 'v1'); + v2 = createMediaElement('video', 'v2'); + var canPlayThrough = new Promise(resolve => v2.canplaythrough = resolve); + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + + // not testing legacy gUM here + return navigator.mediaDevices.getUserMedia({ video: true, audio: true }) + .then(stream => pc1.addStream(v1.srcObject = stream)) + .then(() => pcall(pc1, pc1.createOffer)) + .then(offer => pcall(pc1, pc1.setLocalDescription, offer)) + .then(() => pcall(pc2, pc2.setRemoteDescription, pc1.localDescription)) + .then(() => pcall(pc2, pc2.createAnswer)) + .then(answer => pcall(pc2, pc2.setLocalDescription, answer)) + .then(() => pcall(pc1, pc1.setRemoteDescription, pc2.localDescription)) + .then(() => delivered) + // .then(() => canPlayThrough) // why doesn't this fire? + .then(() => waitUntil(() => v2.currentTime > 0)) + .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")")) + .then(() => ok(true, "Connected.")) + .then(() => { v1.pause(); v2.pause(); }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html new file mode 100644 index 0000000000..db3a735008 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1032848", + title: "Canvas(2D)::CaptureStream as video-only input to peerconnection", + visible: true +}); + +runNetworkTest(async () => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + var test = new PeerConnectionTest(); + var mediaElement; + var h = new CaptureStreamTestHelper2D(); + var canvas = document.createElement('canvas'); + var stream; + canvas.id = 'source_canvas'; + canvas.width = canvas.height = 16; + document.getElementById('content').appendChild(canvas); + + test.setMediaConstraints([{video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + h.drawColor(canvas, h.green); + stream = canvas.captureStream(0); + test.pcLocal.attachLocalStream(stream); + stream.requestFrame(); + var i = 0; + return setInterval(function() { + try { + info("draw " + i ? "green" : "red"); + h.drawColor(canvas, i ? h.green : h.red); + i = 1 - i; + stream.requestFrame(); + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + ]); + test.chain.append([ + function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() { + mediaElement = test.pcRemote.remoteMediaElements[0]; + ok(!!mediaElement, "Should have remote video element for pcRemote"); + return h.pixelMustBecome(mediaElement, h.green, { + threshold: 128, + infoString: "pcRemote's remote should become green", + }); + }, + function PC_LOCAL_DRAW_LOCAL_RED() { + // After requesting a frame it will be captured at the time of next render. + // Next render will happen at next stable state, at the earliest, + // i.e., this order of `requestFrame(); draw();` should work. + stream.requestFrame(); + h.drawColor(canvas, h.red); + }, + function PC_REMOTE_WAIT_FOR_REMOTE_RED() { + return h.pixelMustBecome(mediaElement, h.red, { + threshold: 128, + infoString: "pcRemote's remote should become red", + }); + } + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html new file mode 100644 index 0000000000..e33a7e8886 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "Canvas(2D)::CaptureStream as video-only input to peerconnection with no a=ssrc", + visible: true +}); + +var test; +runNetworkTest(async (options) => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + options = options || { }; + options.ssrc = false; + test = new PeerConnectionTest(options); + var mediaElement; + var h = new CaptureStreamTestHelper2D(); + var canvas = document.createElement('canvas'); + var stream; + canvas.id = 'source_canvas'; + canvas.width = canvas.height = 16; + document.getElementById('content').appendChild(canvas); + + test.setMediaConstraints([{video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + h.drawColor(canvas, h.green); + stream = canvas.captureStream(0); + test.pcLocal.attachLocalStream(stream); + stream.requestFrame(); + var i = 0; + return setInterval(function() { + try { + info("draw " + i ? "green" : "red"); + h.drawColor(canvas, i ? h.green : h.red); + i = 1 - i; + stream.requestFrame(); + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + ]); + test.chain.append([ + function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() { + mediaElement = test.pcRemote.remoteMediaElements[0]; + ok(!!mediaElement, "Should have remote video element for pcRemote"); + return h.pixelMustBecome(mediaElement, h.green, { + threshold: 128, + infoString: "pcRemote's remote should become green", + }); + }, + function PC_LOCAL_DRAW_LOCAL_RED() { + // After requesting a frame it will be captured at the time of next render. + // Next render will happen at next stable state, at the earliest, + // i.e., this order of `requestFrame(); draw();` should work. + stream.requestFrame(); + h.drawColor(canvas, h.red); + }, + function PC_REMOTE_WAIT_FOR_REMOTE_RED() { + return h.pixelMustBecome(mediaElement, h.red, { + threshold: 128, + infoString: "pcRemote's remote should become red", + }); + } + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html new file mode 100644 index 0000000000..167379fb37 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/webgl-mochitest/webgl-util.js"></script> +</head> +<body> +<pre id="test"> +<script id="v-shader" type="x-shader/x-vertex"> + attribute vec2 aPosition; + void main() { + gl_Position = vec4(aPosition, 0, 1); +} +</script> +<script id="f-shader" type="x-shader/x-fragment"> + precision mediump float; + uniform vec4 uColor; + void main() { gl_FragColor = uColor; } +</script> +<script type="application/javascript"> +createHTML({ + bug: "1032848", + title: "Canvas(WebGL)::CaptureStream as video-only input to peerconnection" +}); + +runNetworkTest(async () => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + var test = new PeerConnectionTest(); + var vremote; + var h = new CaptureStreamTestHelperWebGL(); + var canvas = document.createElement('canvas'); + canvas.id = 'source_canvas'; + canvas.width = canvas.height = 16; + canvas.style.display = 'none'; + document.getElementById('content').appendChild(canvas); + + var gl = canvas.getContext('webgl'); + if (!gl) { + todo(false, "WebGL unavailable."); + networkTestFinished(); + return; + } + + test.setMediaConstraints([{video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function WEBGL_SETUP(test) { + var program = WebGLUtil.createProgramByIds(gl, 'v-shader', 'f-shader'); + + if (!program) { + ok(false, "Program should link"); + return Promise.reject("Program should link"); + } + gl.useProgram(program); + + var uColorLocation = gl.getUniformLocation(program, "uColor"); + h.setFragmentColorLocation(uColorLocation); + + var squareBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer); + + var vertices = [ 0, 0, + -1, 0, + 0, 1, + -1, 1 ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + squareBuffer.itemSize = 2; + squareBuffer.numItems = 4; + + program.aPosition = gl.getAttribLocation(program, "aPosition"); + gl.enableVertexAttribArray(program.aPosition); + gl.vertexAttribPointer(program.aPosition, squareBuffer.itemSize, gl.FLOAT, false, 0, 0); + }, + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + h.drawColor(canvas, h.green); + test.pcLocal.canvasStream = canvas.captureStream(0.0); + is(test.pcLocal.canvasStream.canvas, canvas, "Canvas attribute is correct"); + test.pcLocal.attachLocalStream(test.pcLocal.canvasStream); + var i = 0; + return setInterval(function() { + try { + info("draw " + i ? "green" : "red"); + h.drawColor(canvas, i ? h.green : h.red); + i = 1 - i; + test.pcLocal.canvasStream.requestFrame(); + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + ]); + test.chain.append([ + function FIND_REMOTE_VIDEO() { + vremote = test.pcRemote.remoteMediaElements[0]; + ok(!!vremote, "Should have remote video element for pcRemote"); + }, + function WAIT_FOR_REMOTE_GREEN() { + return h.pixelMustBecome(vremote, h.green, { + threshold: 128, + infoString: "pcRemote's remote should become green", + }); + }, + function REQUEST_FRAME(test) { + // After requesting a frame it will be captured at the time of next render. + // Next render will happen at next stable state, at the earliest, + // i.e., this order of `requestFrame(); draw();` should work. + test.pcLocal.canvasStream.requestFrame(); + }, + function DRAW_LOCAL_RED() { + h.drawColor(canvas, h.red); + }, + function WAIT_FOR_REMOTE_RED() { + return h.pixelMustBecome(vremote, h.red, { + threshold: 128, + infoString: "pcRemote's remote should become red", + }); + } + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html new file mode 100644 index 0000000000..f6f48ba429 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="pc.js"></script> + <script src="../../../test/manifest.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +(async () => { + await createHTML({ + bug: "1081409", + title: "Captured video-only over peer connection", + visible: true + }); + + // Run tests in sequence for log readability. + PARALLEL_TESTS = 1; + const manager = new MediaTestManager; + + async function startTest(media, token) { + manager.started(token); + info(`Starting test for ${media.name}`); + const video = document.createElement('video'); + video.id = "id_" + media.name; + video.width = 160; + video.height = 120; + video.muted = true; + video.controls = true; + video.preload = "metadata"; + video.src = "../../../test/" + media.name; + + document.getElementById("content").appendChild(video); + + const onerror = new Promise(r => video.onerror = r).then(_ => + new Error(`${media.name} failed in playback. code=${video.error.code}`)); + + await Promise.race([ + new Promise(res => video.onloadedmetadata = res), + onerror, + ]); + onerror.catch(e => ok(false, e)); + setupEnvironment(); + await testConfigured; + const stream = video.mozCaptureStream(); + const test = new PeerConnectionTest( + { + config_local: { label_suffix: media.name }, + config_remote: { label_suffix: media.name }, + } + ); + test.setOfferOptions( + { + offerToReceiveVideo: false, + offerToReceiveAudio: false, + } + ); + const hasVideo = !!stream.getVideoTracks().length; + const hasAudio = !!stream.getAudioTracks().length; + test.setMediaConstraints([{ video: hasVideo, audio: hasAudio }], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CAPTUREVIDEO(test) { + test.pcLocal.attachLocalStream(stream); + }, + ]); + test.chain.insertBefore("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [ + function PC_LOCAL_START_MEDIA(test) { + video.play(); + }, + ]); + await test.run(); + removeNodeAndSource(video); + manager.finished(token); + } + + manager.runTests(getPlayableVideos(gLongerTests), startTest); +})(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html new file mode 100644 index 0000000000..17b05c8a25 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html @@ -0,0 +1,186 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1172785", + title: "Certificate management" + }); + + function badCertificate(config, expectedError, message) { + return RTCPeerConnection.generateCertificate(config) + .then(() => ok(false, message), + e => is(e.name, expectedError, message)); + } + + // Checks a handful of obviously bad options to RTCCertificate.create(). Most + // of the checking is done by the WebCrypto code underpinning this, hence the + // baffling error codes, but a sanity check is still in order. + function checkBadParameters() { + return Promise.all([ + badCertificate({ + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + modulusLength: 1023, + publicExponent: new Uint8Array([1, 0, 1]) + }, "NotSupportedError", "1023-bit is too small to succeed"), + + badCertificate({ + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-384", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]) + }, "NotSupportedError", "SHA-384 isn't supported yet"), + + // A SyntaxError happens in the "generate key operation" step, but + // webrtc-pc does not say to reject the promise if this step fails. + // It does say to throw NotSupportedError if we have passed "an + // algorithm that the user agent cannot or will not use to generate a + // certificate". + badCertificate({ + name: "ECDH", + namedCurve: "P-256" + }, "NotSupportedError", "ECDH is rejected because the usage is neither \"deriveKey\" or \"deriveBits\""), + + badCertificate({ + name: "not a valid algorithm" + }, "NotSupportedError", "not a valid algorithm"), + + badCertificate("ECDSA", "NotSupportedError", "a bare name is not enough"), + + badCertificate({ + name: "ECDSA", + namedCurve: "not a curve" + }, "NotSupportedError", "ECDSA with an unknown curve") + ]); + } + + function createDB() { + var openDB = indexedDB.open("genericstore"); + openDB.onupgradeneeded = e => { + var db = e.target.result; + db.createObjectStore("data"); + }; + return new Promise(resolve => { + openDB.onsuccess = e => resolve(e.target.result); + }); + } + + function resultPromise(tx, op) { + return new Promise((resolve, reject) => { + op.onsuccess = e => resolve(e.target.result); + op.onerror = () => reject(op.error); + tx.onabort = () => reject(tx.error); + }); + } + + function store(db, value) { + var tx = db.transaction("data", "readwrite"); + var store = tx.objectStore("data"); + return resultPromise(tx, store.put(value, "value")); + } + + function retrieve(db) { + var tx = db.transaction("data", "readonly"); + var store = tx.objectStore("data"); + return resultPromise(tx, store.get("value")); + } + + // Creates a database, stores a value, retrieves it. + function storeAndRetrieve(value) { + return createDB().then(db => { + return store(db, value) + .then(() => retrieve(db)) + .then(retrieved => { + db.close(); + return retrieved; + }); + }); + } + + var test; + runNetworkTest(function (options) { + var expiredCert; + return Promise.resolve() + .then(() => RTCPeerConnection.generateCertificate({ + name: "ECDSA", + namedCurve: "P-256", + expires: 1 // smallest possible expiration window + })) + .then(cert => { + ok(!isNaN(cert.expires), 'cert has expiration time'); + info('Expires at ' + new Date(cert.expires)); + expiredCert = cert; + }) + + .then(() => checkBadParameters()) + + .then(() => { + var delay = expiredCert.expires - Date.now(); + // Hopefully this delay is never needed. + if (delay > 0) { + return new Promise(r => setTimeout(r, delay)); + } + return Promise.resolve(); + }) + .then(() => { + ok(expiredCert.expires <= Date.now(), 'Cert should be at or past expiration'); + try { + new RTCPeerConnection({ certificates: [expiredCert] }); + ok(false, 'Constructing peer connection with an expired cert is not allowed'); + } catch(e) { + is(e.name, 'InvalidAccessError', + 'Constructing peer connection with an expired certs is not allowed'); + } + }) + + .then(() => Promise.all([ + RTCPeerConnection.generateCertificate({ + name: "ECDSA", + namedCurve: "P-256" + }), + RTCPeerConnection.generateCertificate({ + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]) + }) + ])) + + // A round trip through indexedDB should not do anything. + .then(storeAndRetrieve) + .then(certs => { + try { + new RTCPeerConnection({ certificates: certs }); + ok(false, 'Constructing peer connection with multiple certs is not allowed'); + } catch(e) { + is(e.name, 'NotSupportedError', + 'Constructing peer connection with multiple certs is not allowed'); + } + return certs; + }) + .then(certs => { + test = new PeerConnectionTest({ + config_local: { + certificates: [certs[0]] + }, + config_remote: { + certificates: [certs[1]] + } + }); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }) + .catch(e => { + console.log('test failure', e); + ok(false, 'test failed: ' + e); + }); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html new file mode 100644 index 0000000000..248e102dd2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1377299", + title: "Check that packet dump hooks generate callbacks" + }); + + function waitForPacket(pc, checkFunction) { + return new Promise(resolve => { + function onPacket(level, type, sending, packet) { + if (checkFunction(level, type, sending, packet)) { + SpecialPowers.wrap(pc).mozSetPacketCallback(() => {}); + resolve(); + } + } + + SpecialPowers.wrap(pc).mozSetPacketCallback(onPacket); + } + ); + } + + async function waitForSendPacket(pc, type, level) { + await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, true); + await timeout( + waitForPacket(pc, (obsLevel, obsType, sending) => { + is(obsLevel, level, "Level for packet is " + level); + is(obsType, type, "Type for packet is " + type); + ok(sending, "This is a send packet"); + return true; + }), + 10000, "Timeout waiting for " + type + " send packet on level " + level); + await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, true); + } + + async function waitForRecvPacket(pc, type, level) { + await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, false); + await timeout( + waitForPacket(pc, (obsLevel, obsType, sending) => { + is(obsLevel, level, "Level for packet is " + level); + is(obsType, type, "Type for packet is " + type); + ok(!sending, "This is a recv packet"); + return true; + }), + 10000, "Timeout waiting for " + type + " recv packet on level " + level); + await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, false); + } + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true, video: true}], + [{audio: true, video: true}]); + // pc.js uses video elements by default, we want to test audio elements here + test.pcLocal.audioElementsOnly = true; + + test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',[ + async function PC_LOCAL_CHECK_PACKET_DUMP_HOOKS() { + await waitForRecvPacket(test.pcLocal._pc, "rtp", 0); + await waitForRecvPacket(test.pcLocal._pc, "rtcp", 0); + await waitForRecvPacket(test.pcLocal._pc, "srtp", 0); + await waitForRecvPacket(test.pcLocal._pc, "srtcp", 0); + await waitForSendPacket(test.pcLocal._pc, "rtp", 0); + await waitForSendPacket(test.pcLocal._pc, "rtcp", 0); + await waitForSendPacket(test.pcLocal._pc, "srtp", 0); + await waitForSendPacket(test.pcLocal._pc, "srtcp", 0); + + await waitForRecvPacket(test.pcLocal._pc, "rtp", 1); + await waitForRecvPacket(test.pcLocal._pc, "rtcp", 1); + await waitForRecvPacket(test.pcLocal._pc, "srtp", 1); + await waitForRecvPacket(test.pcLocal._pc, "srtcp", 1); + await waitForSendPacket(test.pcLocal._pc, "rtp", 1); + await waitForSendPacket(test.pcLocal._pc, "rtcp", 1); + await waitForSendPacket(test.pcLocal._pc, "srtp", 1); + await waitForSendPacket(test.pcLocal._pc, "srtcp", 1); + }, + async function PC_REMOTE_CHECK_PACKET_DUMP_HOOKS() { + await waitForRecvPacket(test.pcRemote._pc, "rtp", 0); + await waitForRecvPacket(test.pcRemote._pc, "rtcp", 0); + await waitForRecvPacket(test.pcRemote._pc, "srtp", 0); + await waitForRecvPacket(test.pcRemote._pc, "srtcp", 0); + await waitForSendPacket(test.pcRemote._pc, "rtp", 0); + await waitForSendPacket(test.pcRemote._pc, "rtcp", 0); + await waitForSendPacket(test.pcRemote._pc, "srtp", 0); + await waitForSendPacket(test.pcRemote._pc, "srtcp", 0); + + await waitForRecvPacket(test.pcRemote._pc, "rtp", 1); + await waitForRecvPacket(test.pcRemote._pc, "rtcp", 1); + await waitForRecvPacket(test.pcRemote._pc, "srtp", 1); + await waitForRecvPacket(test.pcRemote._pc, "srtcp", 1); + await waitForSendPacket(test.pcRemote._pc, "rtp", 1); + await waitForSendPacket(test.pcRemote._pc, "rtcp", 1); + await waitForSendPacket(test.pcRemote._pc, "srtp", 1); + await waitForSendPacket(test.pcRemote._pc, "srtcp", 1); + } + ]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html new file mode 100644 index 0000000000..3edf677203 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "991877", + title: "Basic RTCPeerConnection.close() tests" + }); + + runNetworkTest(function () { + var pc = new RTCPeerConnection(); + var sender = pc.addTrack(getSilentTrack(), new MediaStream()); + var exception = null; + var eTimeout = null; + + // everything should be in initial state + is(pc.signalingState, "stable", "Initial signalingState is 'stable'"); + is(pc.iceConnectionState, "new", "Initial iceConnectionState is 'new'"); + is(pc.iceGatheringState, "new", "Initial iceGatheringState is 'new'"); + + var finish; + var finished = new Promise(resolve => finish = resolve); + + var mustNotSettle = (p, ms, msg) => Promise.race([ + p.then(() => ok(false, msg + " must not settle"), + e => ok(false, msg + " must not settle. Got " + e.name)), + wait(ms).then(() => ok(true, msg + " must not settle")) + ]); + + var silence = mustNotSettle(pc.createOffer(), 1000, + "createOffer immediately followed by close"); + try { + pc.close(); + } catch (e) { + exception = e; + } + is(exception, null, "closing the connection raises no exception"); + is(pc.signalingState, "closed", "Final signalingState is 'closed'"); + is(pc.iceConnectionState, "closed", "Final iceConnectionState is 'closed'"); + + // test that pc is really closed (and doesn't crash, bug 1259728) + try { + pc.getLocalStreams(); + } catch (e) { + exception = e; + } + is(exception && exception.name, "InvalidStateError", + "pc.getLocalStreams should throw when closed"); + exception = null; + + try { + pc.close(); + } catch (e) { + exception = e; + } + is(exception, null, "A second close() should not raise an exception"); + is(pc.signalingState, "closed", "Final signalingState stays at 'closed'"); + is(pc.iceConnectionState, "closed", "Final iceConnectionState stays at 'closed'"); + + // Due to a limitation in our WebIDL compiler that prevents overloads with + // both Promise and non-Promise return types, legacy APIs with callbacks + // are unable to continue to throw exceptions. Luckily the spec uses + // exceptions solely for "programming errors" so this should not hinder + // working code from working, which is the point of the legacy API. All + // new code should use the promise API. + // + // The legacy methods that no longer throw on programming errors like + // "invalid-on-close" are: + // - createOffer + // - createAnswer + // - setLocalDescription + // - setRemoteDescription + // - addIceCandidate + // - getStats + // + // These legacy methods fire the error callback instead. This is not + // entirely to spec but is better than ignoring programming errors. + + var offer = new RTCSessionDescription({ sdp: "sdp", type: "offer" }); + var answer = new RTCSessionDescription({ sdp: "sdp", type: "answer" }); + var candidate = new RTCIceCandidate({ candidate: "dummy", + sdpMid: "test", + sdpMLineIndex: 3 }); + + var doesFail = (p, msg) => p.then(generateErrorCallback(msg), + r => is(r.name, "InvalidStateError", msg)); + Promise.all([ + [pc.createOffer(), "createOffer"], + [pc.createOffer({offerToReceiveAudio: true}), "createOffer({offerToReceiveAudio: true})"], + [pc.createOffer({offerToReceiveAudio: false}), "createOffer({offerToReceiveAudio: false})"], + [pc.createOffer({offerToReceiveVideo: true}), "createOffer({offerToReceiveVideo: true})"], + [pc.createOffer({offerToReceiveVideo: false}), "createOffer({offerToReceiveVideo: false})"], + [pc.createAnswer(), "createAnswer"], + [pc.setLocalDescription(offer), "setLocalDescription"], + [pc.setRemoteDescription(answer), "setRemoteDescription"], + [pc.addIceCandidate(candidate), "addIceCandidate"], + [new Promise((y, n) => pc.createOffer(y, n)), "Legacy createOffer"], + [new Promise((y, n) => pc.createAnswer(y, n)), "Legacy createAnswer"], + [new Promise((y, n) => pc.setLocalDescription(offer, y, n)), "Legacy setLocalDescription"], + [new Promise((y, n) => pc.setRemoteDescription(answer, y, n)), "Legacy setRemoteDescription"], + [new Promise((y, n) => pc.addIceCandidate(candidate, y, n)), "Legacy addIceCandidate"], + [sender.replaceTrack(getSilentTrack()), "replaceTrack"], + ].map(([p, name]) => doesFail(p, name + " fails on close"))) + .catch(reason => ok(false, "unexpected failure: " + reason)) + .then(finish); + + // Other methods are unaffected. + + SimpleTest.doesThrow(function() { + pc.updateIce("Invalid RTC Configuration")}, + "updateIce() on closed PC raised expected exception"); + + SimpleTest.doesThrow(function() { + pc.addStream("Invalid Media Stream")}, + "addStream() on closed PC raised expected exception"); + + SimpleTest.doesThrow(function() { + pc.createDataChannel({})}, + "createDataChannel() on closed PC raised expected exception"); + + SimpleTest.doesThrow(function() { + pc.setIdentityProvider("Invalid Provider")}, + "setIdentityProvider() on closed PC raised expected exception"); + + return Promise.all([finished, silence]); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html new file mode 100644 index 0000000000..db3a2922d5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1087629", + title: "Close PCs during ICE connectivity check" + }); + +// Test closeDuringIce to simulate problems during peer connections + + +function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {}); +} +function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) { + test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {}); +} +function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) { + var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0}; + test.pcRemote.storeOrAddIceCandidate(cand); + info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand)); +} +function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) { + var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0}; + test.pcLocal.storeOrAddIceCandidate(cand); + info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand)); +} +function PC_LOCAL_CLOSE_DURING_ICE(test) { + return test.pcLocal.iceChecking.then(() => { + test.pcLocal.onsignalingstatechange = function () {}; + test.pcLocal.close(); + }); +} +function PC_REMOTE_CLOSE_DURING_ICE(test) { + return test.pcRemote.iceChecking.then(() => { + test.pcRemote.onsignalingstatechange = function () {}; + test.pcRemote.close(); + }); +} +function PC_LOCAL_WAIT_FOR_ICE_CHECKING(test) { + var resolveIceChecking; + test.pcLocal.iceChecking = new Promise(r => resolveIceChecking = r); + test.pcLocal.ice_connection_callbacks.checkIceStatus = () => { + if (test.pcLocal._pc.iceConnectionState === "checking") { + resolveIceChecking(); + } + } +} +function PC_REMOTE_WAIT_FOR_ICE_CHECKING(test) { + var resolveIceChecking; + test.pcRemote.iceChecking = new Promise(r => resolveIceChecking = r); + test.pcRemote.ice_connection_callbacks.checkIceStatus = () => { + if (test.pcRemote._pc.iceConnectionState === "checking") { + resolveIceChecking(); + } + } +} + +runNetworkTest(() => { + var test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER); + test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER); + test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_CHECKING); + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_CHECKING", PC_REMOTE_WAIT_FOR_ICE_CHECKING); + test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION"); + test.chain.append([PC_REMOTE_ADD_FAKE_ICE_CANDIDATE, PC_LOCAL_ADD_FAKE_ICE_CANDIDATE, + PC_LOCAL_CLOSE_DURING_ICE, PC_REMOTE_CLOSE_DURING_ICE]); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html new file mode 100644 index 0000000000..819e13fe1b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="iceTestUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1683934", + title: "RTCPeerConnection check codec negotiation failure" + }); + + function makeWeirdCodecs(sdp) { + return sdp + .replaceAll('VP8', 'VEEEEEEEEP8') + .replaceAll('VP9', 'VEEEEEEEEP9') + .replaceAll('H264', 'HERP264'); + } + + const tests = [ + async function offererWeirdCodecs() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + const offer = await pc1.createOffer(); + offer.sdp = makeWeirdCodecs(offer.sdp); + // It is not an error to receive an offer with no codecs we support + await pc2.setRemoteDescription(offer); + await pc2.setLocalDescription(); + await wait(2000); + }, + + async function answererWeirdCodecs() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + answer.sdp = makeWeirdCodecs(answer.sdp); + try { + await pc1.setRemoteDescription(answer); + ok(false, "Should have thrown"); + } catch (e) { + ok(true, "Should have thrown"); + } + }, + + async function reoffererWeirdCodecs() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + const offer = await pc1.createOffer(); + offer.sdp = makeWeirdCodecs(offer.sdp); + // It is not an error to receive an offer with no codecs we support + await pc2.setRemoteDescription(offer); + await pc2.setLocalDescription(); + await wait(2000); + }, + + async function reanswererWeirdCodecs() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + answer.sdp = makeWeirdCodecs(answer.sdp); + try { + await pc1.setRemoteDescription(answer); + ok(false, "Should have thrown"); + } catch (e) { + ok(true, "Should have thrown"); + } + }, + + ]; + + runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html new file mode 100644 index 0000000000..8431b7534e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1271669", + title: "Test that pc.addTrack() accepts any MediaStream", + visible: true +}); + +runNetworkTest(() => { + var test = new PeerConnectionTest(); + var constructedStream; + var dummyStream = new MediaStream(); + var dummyStreamTracks = []; + + test.setMediaConstraints([ {audio: true, video: true} + , {audio: true} + , {video: true} + ], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_GUM_CONSTRUCTED_STREAM(test) { + return getUserMedia(test.pcLocal.constraints[0]).then(stream => { + constructedStream = new MediaStream(stream.getTracks()); + test.pcLocal.attachLocalStream(constructedStream); + }); + }, + function PC_LOCAL_GUM_DUMMY_STREAM(test) { + return getUserMedia(test.pcLocal.constraints[1]) + .then(stream => dummyStreamTracks.push(...stream.getTracks())) + .then(() => getUserMedia(test.pcLocal.constraints[2])) + .then(stream => dummyStreamTracks.push(...stream.getTracks())) + .then(() => dummyStreamTracks.forEach(t => + test.pcLocal.attachLocalTrack(t, dummyStream))); + }, + ]); + + let checkSentTracksReceived = (sentStreamId, sentTracks) => { + let receivedStream = + test.pcRemote._pc.getRemoteStreams().find(s => s.id == sentStreamId); + ok(receivedStream, "We should receive a stream with with the sent stream's id (" + sentStreamId + ")"); + if (!receivedStream) { + return; + } + + is(receivedStream.getTracks().length, sentTracks.length, + "Should receive same number of tracks as were sent"); + }; + + test.chain.append([ + function PC_REMOTE_CHECK_RECEIVED_CONSTRUCTED_STREAM() { + checkSentTracksReceived(constructedStream.id, constructedStream.getTracks()); + }, + function PC_REMOTE_CHECK_RECEIVED_DUMMY_STREAM() { + checkSentTracksReceived(dummyStream.id, dummyStreamTracks); + }, + ]); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html new file mode 100644 index 0000000000..4c06de792e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1570673", + title: "Sending an initially disabled video track should be playable remotely", + visible: true, + }); + + var test; + runNetworkTest(async (options) => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + test.chain.insertAfter("PC_LOCAL_GUM", function PC_LOCAL_DISABLE_VIDEO() { + for (const {track} of test.pcLocal._pc.getSenders()) { + if (track.kind == "video") { + track.enabled = false; + } + } + }); + test.chain.append(async function PC_REMOTE_RECEIVING_BLACK() { + const v = test.pcRemote.remoteMediaElements[0]; + is(v.readyState, v.HAVE_ENOUGH_DATA, "video element should be playing"); + const h = new CaptureStreamTestHelper2D(); + await h.waitForPixel(test.pcRemote.remoteMediaElements[0], + px => h.isPixel(px, h.black, 128)); + }); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html new file mode 100644 index 0000000000..f46d7eb0d2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1401592", + title: "Simulcast negotiation tests", + visible: true +}); + +// simulcast negotiation is mostly tested in wpt, but we test a few +// implementation-specific things here. +const tests = [ + async function checkVideoEncodingLimit() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3", "4"]); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["1", "2", "3"]); + + pc1.close(); + pc2.close(); + stream.getTracks().forEach(track => track.stop()); + }, + + // wpt currently does not assume support for 3 encodings, which limits the + // effectiveness of its powers-of-2 test (since it can test only for 1 and 2) + async function checkScaleResolutionDownByAutoFillPowersOf2() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3"]); + + const {encodings} = sender.getParameters(); + const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy); + isDeeply(scaleValues, [4, 2, 1]); + }, + + async function checkLibwebrtcRidLengthLimit() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await doOfferToRecvSimulcast(pc2, pc1, ["foo", "wibblywobblyjeremybearimy"]); + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo"]); + + pc1.close(); + pc2.close(); + stream.getTracks().forEach(track => track.stop()); + }, +]; + +runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html new file mode 100644 index 0000000000..851a256509 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "834270", + title: "Align PeerConnection error handling with WebRTC specification" + }); + + function validateReason(reason) { + ok(reason.name.length, "Reason name = " + reason.name); + ok(reason.message.length, "Reason message = " + reason.message); + }; + + function testCreateAnswerError() { + var pc = new RTCPeerConnection(); + info ("Testing createAnswer error"); + return pc.createAnswer() + .then(generateErrorCallback("createAnswer before offer should fail"), + validateReason); + }; + + function testSetLocalDescriptionError() { + var pc = new RTCPeerConnection(); + info ("Testing setLocalDescription error"); + return pc.setLocalDescription({ sdp: "Picklechips!", type: "offer" }) + .then(generateErrorCallback("setLocalDescription with nonsense SDP should fail"), + validateReason); + }; + + function testSetRemoteDescriptionError() { + var pc = new RTCPeerConnection(); + info ("Testing setRemoteDescription error"); + return pc.setRemoteDescription({ sdp: "Who?", type: "offer" }) + .then(generateErrorCallback("setRemoteDescription with nonsense SDP should fail"), + validateReason); + }; + + // No test for createOffer errors -- there's nothing we can do at this + // level to evoke an error in createOffer. + + runNetworkTest(function () { + return testCreateAnswerError() + .then(testSetLocalDescriptionError) + .then(testSetRemoteDescriptionError); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html new file mode 100644 index 0000000000..78c6bb986c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html @@ -0,0 +1,325 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="iceTestUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1799932", + title: "RTCPeerConnection check renegotiation of extmap" + }); + + function setExtmap(sdp, uri, id) { + const regex = new RegExp(`a=extmap:[0-9]+(\/[a-z]+)? ${uri}`, 'g'); + if (id) { + return sdp.replaceAll(regex, `a=extmap:${id}$1 ${uri}`); + } else { + return sdp.replaceAll(regex, `a=unknownattr`); + } + } + + function getExtmap(sdp, uri) { + const regex = new RegExp(`a=extmap:([0-9]+)(\/[a-z]+)? ${uri}`); + return sdp.match(regex)[1]; + } + + function replaceExtUri(sdp, oldUri, newUri) { + const regex = new RegExp(`(a=extmap:[0-9]+\/[a-z]+)? ${oldUri}`, 'g'); + return sdp.replaceAll(regex, `$1 ${newUri}`); + } + + const tests = [ + async function checkAudioMidChange() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14); + info(`New reoffer: ${reoffer.sdp}`); + await pc2.setRemoteDescription(reoffer); + await pc2.setLocalDescription(); + await wait(2000); + }, + + async function checkVideoMidChange() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14); + info(`New reoffer: ${reoffer.sdp}`); + await pc2.setRemoteDescription(reoffer); + await pc2.setLocalDescription(); + await wait(2000); + }, + + async function checkAudioMidSwap() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid"); + const ssrcLevelId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", ssrcLevelId); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId); + info(`New reoffer: ${reoffer.sdp}`); + try { + await pc2.setRemoteDescription(reoffer); + ok(false, "sRD should fail when it attempts extension id remapping"); + } catch (e) { + ok(true, "sRD should fail when it attempts extension id remapping"); + } + }, + + async function checkVideoMidSwap() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid"); + const toffsetId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset"); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", toffsetId); + reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", midId); + info(`New reoffer: ${reoffer.sdp}`); + try { + await pc2.setRemoteDescription(reoffer); + ok(false, "sRD should fail when it attempts extension id remapping"); + } catch (e) { + ok(true, "sRD should fail when it attempts extension id remapping"); + } + }, + + async function checkAudioIdReuse() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + // Change uri, but not the id, so the id now refers to foo. + reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo"); + info(`New reoffer: ${reoffer.sdp}`); + try { + await pc2.setRemoteDescription(reoffer); + ok(false, "sRD should fail when it attempts extension id remapping"); + } catch (e) { + ok(true, "sRD should fail when it attempts extension id remapping"); + } + }, + + async function checkVideoIdReuse() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + // Sadly, there's no way to tell the offerer to change the extmap. Other + // types of endpoint could conceivably do this, so we at least don't want + // to crash. + // TODO: Would be nice to be able to test this with an endpoint that + // actually changes the ids it uses. + const reoffer = await pc1.createOffer(); + // Change uri, but not the id, so the id now refers to foo. + reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", "foo"); + info(`New reoffer: ${reoffer.sdp}`); + try { + await pc2.setRemoteDescription(reoffer); + ok(false, "sRD should fail when it attempts extension id remapping"); + } catch (e) { + ok(true, "sRD should fail when it attempts extension id remapping"); + } + }, + + // What happens when remote answer uses an extmap id, and then a remote + // reoffer tries to use the same id for something else? + async function checkAudioIdReuseOffererThenAnswerer() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + await connect(pc1, pc2, 32000, "Initial connection"); + + const reoffer = await pc2.createOffer(); + // Change uri, but not the id, so the id now refers to foo. + reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo"); + info(`New reoffer: ${reoffer.sdp}`); + try { + await pc1.setRemoteDescription(reoffer); + ok(false, "sRD should fail when it attempts extension id remapping"); + } catch (e) { + ok(true, "sRD should fail when it attempts extension id remapping"); + } + }, + + // What happens when a remote offer uses a different extmap id than the + // default? Does the answerer remember the new id in reoffers? + async function checkAudioIdReuseOffererThenAnswerer() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + // Negotiate, but change id for ssrc-audio-level to something pc2 would + // not typically use. + await pc1.setLocalDescription(); + const mungedOffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 12); + await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"}); + await pc2.setLocalDescription(); + + const reoffer = await pc2.createOffer(); + is(getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"), "12"); + }, + + async function checkAudioUnnegotiatedIdReuse1() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + // Negotiate, but remove ssrc-audio-level from answer + await pc1.setLocalDescription(); + const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + const answerNoExt = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined); + await pc1.setRemoteDescription({sdp: answerNoExt, type: "answer"}); + + // Renegotiate, and use the id that offerer used for ssrc-audio-level for + // something different (while making sure we don't use it twice) + await pc2.setLocalDescription(); + const mungedReoffer = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId); + const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined); + await pc1.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"}); + }, + + async function checkAudioUnnegotiatedIdReuse2() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + // Negotiate, but remove ssrc-audio-level from offer. pc2 has never seen + // |levelId| in extmap yet, but internally probably wants to use that for + // ssrc-audio-level + await pc1.setLocalDescription(); + const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"); + const offerNoExt = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined); + await pc2.setRemoteDescription({sdp: offerNoExt, type: "offer"}); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + // Renegotiate, but use |levelId| for something other than + // ssrc-audio-level. pc2 should not throw. + await pc1.setLocalDescription(); + const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId); + const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined); + await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"}); + }, + + async function checkAudioUnnegotiatedIdReuse3() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + pc1.addTrack(stream.getTracks()[0]); + pc2.addTrack(stream.getTracks()[0]); + + // Negotiate, but replace ssrc-audio-level with something pc2 won't + // support in offer. + await pc1.setLocalDescription(); + const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"); + const mungedOffer = replaceExtUri(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "fooba"); + await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"}); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + // Renegotiate, and use levelId for something pc2 _will_ support. + await pc1.setLocalDescription(); + const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId); + const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined); + await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"}); + }, + + ]; + + runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html new file mode 100644 index 0000000000..84b53a123b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "931903", + title: "Forwarding a stream from a combined audio/video peerconnection to another" + }); + +runNetworkTest(function() { + var gumTest = new PeerConnectionTest(); + + var forwardingOptions = { config_local: { label_suffix: "forwarded" }, + config_remote: { label_suffix: "forwarded" } }; + var forwardingTest = new PeerConnectionTest(forwardingOptions); + + gumTest.setMediaConstraints([{audio: true, video: true}], []); + forwardingTest.setMediaConstraints([{audio: true, video: true}], []); + forwardingTest.chain.replace("PC_LOCAL_GUM", [ + function PC_FORWARDING_CAPTUREVIDEO(test) { + var streams = gumTest.pcRemote._pc.getRemoteStreams(); + is(streams.length, 1, "One stream to forward"); + is(streams[0].getTracks().length, 2, "Forwarded stream has 2 tracks"); + forwardingTest.pcLocal.attachLocalStream(streams[0]); + return Promise.resolve(); + } + ]); + gumTest.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW"); + return gumTest.chain.execute() + .then(() => forwardingTest.chain.execute()) + .then(() => gumTest.close()) + .then(() => forwardingTest.close()); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html new file mode 100644 index 0000000000..6710e628aa --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html @@ -0,0 +1,450 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="iceTestUtils.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script></head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1253706", + title: "Test ICE gathering when setConfiguration is used to change the ICE config" + }); + + const tests = [ + async function baselineV4Cases() { + await checkSrflx([{urls:[`stun:${turnAddressV4}`]}]); + await checkRelayUdp([{urls:[`turn:${turnAddressV4}`], username, credential}]); + await checkRelayTcp([{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]); + await checkRelayUdpTcp([{urls:[`turn:${turnAddressV4}`, `turn:${turnAddressV4}?transport=tcp`], username, credential}]); + await checkNoSrflx(); + await checkNoRelay(); + }, + + async function addStunServerBeforeOffer() { + const pc = new RTCPeerConnection(); + try { + pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + const candidates = await gatherWithTimeout(pc, 32000, `just a stun server`); + ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function addTurnServerBeforeOffer() { + const pc = new RTCPeerConnection(); + try { + pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`); + ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + pc.close(); + } + }, + + async function addTurnTcpServerBeforeOffer() { + const pc = new RTCPeerConnection(); + try { + pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + const candidates = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + pc.close(); + } + }, + + async function addStunServerAfterOffer() { + const pc = new RTCPeerConnection(); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + const candidates2 = await gatherWithTimeout(pc, 32000, `just a stun server`); + ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function addTurnServerAfterOffer() { + const pc = new RTCPeerConnection(); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`); + ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + pc.close(); + } + }, + + async function addTurnTcpServerAfterOffer() { + const pc = new RTCPeerConnection(); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`); + ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates"); + ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + pc.close(); + } + }, + + async function removeStunServerBeforeOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + pc.setConfiguration({}); + const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function removeTurnServerBeforeOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + try { + pc.setConfiguration({}); + const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function removeTurnTcpServerBeforeOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + try { + pc.setConfiguration({}); + const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function removeStunServerAfterOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `just a stun server`); + ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({}); + const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function removeTurnServerAfterOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`); + ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({}); + const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function removeTurnTcpServerAfterOffer() { + const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + try { + const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`); + ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates"); + ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + await pc.setLocalDescription({type: "rollback"}); + + pc.setConfiguration({}); + const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`); + ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + pc.close(); + } + }, + + async function addStunServerAfterNegotiation() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + const candidatePromise = trickleIce(offerer); + await connect(offerer, answerer, 32000, `no ICE servers`); + const candidates = await candidatePromise; + const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment))); + is(ufrags.length, 1, "Should have one ufrag in candidate set"); + + offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + const candidates2 = await gatherWithTimeout(offerer, 32000, `just a stun server`); + ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment))); + is(ufrags2.length, 1, "Should have one ufrag in candidate set"); + isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addTurnServerAfterNegotiation() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + const candidatePromise = trickleIce(offerer); + await connect(offerer, answerer, 32000, `no ICE servers`); + const candidates = await candidatePromise; + const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment))); + is(ufrags.length, 1, "Should have one ufrag in candidate set"); + + offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (udp) server`); + ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment))); + is(ufrags2.length, 1, "Should have one ufrag in candidate set"); + isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addTurnTcpServerAfterNegotiation() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + const candidatePromise = trickleIce(offerer); + await connect(offerer, answerer, 32000, `no ICE servers`); + const candidates = await candidatePromise; + const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment))); + is(ufrags.length, 1, "Should have one ufrag in candidate set"); + + offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (tcp) server`); + ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment))); + is(ufrags2.length, 1, "Should have one ufrag in candidate set"); + isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addStunServerBeforeCreateAnswer() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + try { + await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true})); + + answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + const candidates = await gatherWithTimeout(answerer, 32000, `just a stun server`); + ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addTurnServerBeforeCreateAnswer() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + try { + await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true})); + + answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + const candidates = await gatherWithTimeout(answerer, 32000, `a turn (udp) server`); + ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate"); + ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addTurnTcpServerBeforeCreateAnswer() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + try { + await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true})); + + answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]}); + const candidates = await gatherWithTimeout(answerer, 32000, `a turn (tcp) server`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates"); + ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function relayPolicyPreventsSrflx() { + const pc = new RTCPeerConnection(); + try { + pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}], iceTransportPolicy: "relay"}); + const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`); + ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get a srflx candidate"); + ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate"); + } finally { + pc.close(); + } + }, + + async function addOffererStunServerAllowsIceToConnect() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + try { + try { + // Both ends are behind a simulated endpoint-independent NAT, which + // requires at least one side to have a srflx candidate to work. + await connect(offerer, answerer, 2000, `no ICE servers`); + ok(false, "ICE should either have failed, or timed out!"); + } catch (e) { + if (!(e instanceof Error)) throw e; + ok(true, "ICE should either have failed, or timed out!"); + } + + offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + await connect(offerer, answerer, 32000, `just a STUN server`); + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addAnswererStunServerDoesNotAllowIceToConnect() { + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + try { + try { + // Both ends are behind a simulated endpoint-independent NAT, which + // requires at least one side to have a srflx candidate to work. + await connect(offerer, answerer, 2000, `no ICE servers`); + ok(false, "ICE should either have failed, or timed out!"); + } catch (e) { + if (!(e instanceof Error)) throw e; + ok(true, "ICE should either have failed, or timed out!"); + } + + // This _won't_ help, because the answerer does not get to decide to + // trigger an ICE restart. + answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + try { + await connectNoTrickleWait(offerer, answerer, 2000, `no ICE servers`); + ok(false, "ICE should either have failed, or timed out!"); + } catch (e) { + if (!(e instanceof Error)) throw e; + ok(true, "ICE should either have failed, or timed out!"); + } + } finally { + offerer.close(); + answerer.close(); + } + }, + + async function addOffererTurnServerAllowsIceToConnect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT']); + + const offerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]}); + + try { + try { + // Both ends are behind a simulated port-dependent NAT, which + // requires at least one side to have a relay candidate to work. + await connect(offerer, answerer, 2000, `just a STUN server`); + ok(false, "ICE should either have failed, or timed out!"); + } catch (e) { + if (!(e instanceof Error)) throw e; + ok(true, "ICE should either have failed, or timed out!"); + } + + offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]}); + await connect(offerer, answerer, 32000, `a TURN (udp) server`); + } finally { + offerer.close(); + answerer.close(); + await SpecialPowers.popPrefEnv(); + } + }, + + ]; + + runNetworkTest(async () => { + const turnServer = iceServersArray.find(server => "username" in server); + username = turnServer.username; + credential = turnServer.credential; + // Just use the first url. It might make sense to look for TURNS first, + // since that will always use a hostname, but on CI we don't have TURNS + // support anyway (see bug 1323439). + const turnHostname = getTurnHostname(turnServer.urls[0]); + turnAddressV4 = await dnsLookupV4(turnHostname); + + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + + for (const test of tests) { + info(`Running test: ${test.name}`); + try { + await test(); + } catch (e) { + ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`); + } + info(`Done running test: ${test.name}`); + // Make sure we don't build up a pile of GC work, and also get PCImpl to + // print their timecards. + await new Promise(r => SpecialPowers.exactGC(r)); + } + + await SpecialPowers.popPrefEnv(); + }, { useIceServer: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html new file mode 100644 index 0000000000..50bc4a6553 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html @@ -0,0 +1,269 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="iceTestUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "857668", + title: "RTCPeerConnection check STUN gathering with STUN/300 responses" + }); + + /* This is pretty hairy, so some background: + * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10 + * STUN/300 responses allow a server to redirect STUN requests to one or + more other servers, as ALTERNATE-SERVER attributes. + * The server specifies the IP address, IP version, and port for each + ALTERNATE-SERVER. + * The spec allows multiple rounds of redirects, and requires the client to + remember the servers it has already tried to avoid redirect loops. + * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute, + which the client MUST use for the TLS handshake on the new target. The + client does _not_ use this as an FQDN; it always uses the address in the + ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case. + * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate + message type (at least in the context of ICE). Clients are supposed to + treat STUN/300 as an unrecoverable error in all other cases. The TURN spec + does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs. + We just take the first one that we have not already tried, and that is the + same IP version that we started with. This is because switching the IP + version is problematic for ICE. + * The test TURN server opens extra ports that will respond with redirects to + the _real_ ports, but the address remains the same. This is because we + cannot know ahead of time whether the machine we're running on has more + than one IP address of each version. This means the test TURN server is not + useful for testing cases where the address changes. Also, the test TURN + server does not currently know how to respond with multiple + ALTERNATE-SERVERs. + * To test cases where the _address_ changes, we instead use a feature in the + NAT simulator to respond with fake redirects when the destination address + matches an address that we configure with a pref. This feature can add + multiple ALTERNATE-SERVERs. + * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY, + but the NAT simulator's do _not_. For now, we want both cases to work, + because some servers respond with STUN/300 without including + MESSAGE-INTEGRITY. This is a spec violation, even though the spec + contradicts itself in non-normative language elsewhere. + * Right now, neither the NAT simulator nor the test TURN server support + ALTERNATE-DOMAIN. + */ + + // These are the ports the test TURN server will respond with redirects on. + // The test TURN server tells us what these are in the JSON it spits out when + // we start it. + let turnRedirectPort; + let turnsRedirectPort; + + // These are the addresses that we will configure the NAT simulator to + // redirect to. We do DNS lookups of the host in iceServersArray (provided + // by the test TURN server), and put the results here. On some platforms this + // will be 127.0.0.1 and ::1, but on others we may use a real address. + let redirectTargetV4; + + // Test TURN server tells us these in the JSON it spits out when we start it + let username; + let credential; + + // This is the address we will configure the NAT simulator to respond with + // redirects for. We use an address from TEST-NET since it is really unlikely + // we'll see that on a real machine, and also because we do not have + // special-case code in nICEr for TEST-NET (like we do for link-local, for + // example). + const redirectAddressV4 = '198.51.100.1'; + + const tests = [ + async function baselineV4Cases() { + await checkSrflx([{urls:[`stun:${redirectTargetV4}`]}]); + await checkRelayUdp([{urls:[`turn:${redirectTargetV4}`], username, credential}]); + await checkRelayTcp([{urls:[`turn:${redirectTargetV4}?transport=tcp`], username, credential}]); + await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}`, `turn:${redirectTargetV4}?transport=tcp`], username, credential}]); + }, + + async function stunV4Redirect() { + // This test uses the test TURN server, because nICEr drops responses + // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to + // an Allocate request. If we tried to use the NAT simulator for this, we + // would have to wait for nICEr to time out, since the NAT simulator does + // not know how to do MESSAGE-INTEGRITY. + await checkNoSrflx( + [{urls:[`stun:${redirectTargetV4}:${turnRedirectPort}`]}]); + }, + + async function turnV4UdpPortRedirect() { + await checkRelayUdp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`], username, credential}]); + }, + + async function turnV4TcpPortRedirect() { + await checkRelayTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]); + }, + + async function turnV4UdpTcpPortRedirect() { + await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`, `turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]); + }, + + async function turnV4UdpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4TcpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpTcpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4TcpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpTcpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayUdp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4TcpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpTcpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]); + await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`, `turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4TcpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpTcpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]); + await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4TcpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]); + await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV4UdpTcpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]); + await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + ]; + + runNetworkTest(async () => { + const turnServer = iceServersArray.find(server => "username" in server); + username = turnServer.username; + credential = turnServer.credential; + // Special props, non-standard + turnRedirectPort = turnServer.turn_redirect_port; + turnsRedirectPort = turnServer.turns_redirect_port; + // Just use the first url. It might make sense to look for TURNS first, + // since that will always use a hostname, but on CI we don't have TURNS + // support anyway (see bug 1323439). + const turnHostname = getTurnHostname(turnServer.urls[0]); + redirectTargetV4 = await dnsLookupV4(turnHostname); + + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + + await SpecialPowers.popPrefEnv(); + }, { useIceServer: true }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html new file mode 100644 index 0000000000..16f8f39978 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html @@ -0,0 +1,283 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="iceTestUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "857668", + title: "RTCPeerConnection check STUN gathering with STUN/300 responses (IPv6)" + }); + + /* This is pretty hairy, so some background: + * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10 + * STUN/300 responses allow a server to redirect STUN requests to one or + more other servers, as ALTERNATE-SERVER attributes. + * The server specifies the IP address, IP version, and port for each + ALTERNATE-SERVER. + * The spec allows multiple rounds of redirects, and requires the client to + remember the servers it has already tried to avoid redirect loops. + * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute, + which the client MUST use for the TLS handshake on the new target. The + client does _not_ use this as an FQDN; it always uses the address in the + ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case. + * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate + message type (at least in the context of ICE). Clients are supposed to + treat STUN/300 as an unrecoverable error in all other cases. The TURN spec + does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs. + We just take the first one that we have not already tried, and that is the + same IP version that we started with. This is because switching the IP + version is problematic for ICE. + * The test TURN server opens extra ports that will respond with redirects to + the _real_ ports, but the address remains the same. This is because we + cannot know ahead of time whether the machine we're running on has more + than one IP address of each version. This means the test TURN server is not + useful for testing cases where the address changes. Also, the test TURN + server does not currently know how to respond with multiple + ALTERNATE-SERVERs. + * To test cases where the _address_ changes, we instead use a feature in the + NAT simulator to respond with fake redirects when the destination address + matches an address that we configure with a pref. This feature can add + multiple ALTERNATE-SERVERs. + * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY, + but the NAT simulator's do _not_. For now, we want both cases to work, + because some servers respond with STUN/300 without including + MESSAGE-INTEGRITY. This is a spec violation, even though the spec + contradicts itself in non-normative language elsewhere. + * Right now, neither the NAT simulator nor the test TURN server support + ALTERNATE-DOMAIN. + */ + + // These are the ports the test TURN server will respond with redirects on. + // The test TURN server tells us what these are in the JSON it spits out when + // we start it. + let turnRedirectPort; + let turnsRedirectPort; + + // These are the addresses that we will configure the NAT simulator to + // redirect to. We do DNS lookups of the host in iceServersArray (provided + // by the test TURN server), and put the results here. On some platforms this + // will be 127.0.0.1 and ::1, but on others we may use a real address. + let redirectTargetV6; + + // Test TURN server tells us these in the JSON it spits out when we start it + let username; + let credential; + + // This is the address we will configure the NAT simulator to respond with + // redirects for. We use an address from TEST-NET since it is really unlikely + // we'll see that on a real machine, and also because we do not have + // special-case code in nICEr for TEST-NET (like we do for link-local, for + // example). + const redirectAddressV6 = '::ffff:198.51.100.1'; + + const tests = [ + async function baselineV6Cases() { + await checkSrflx([{urls:[`stun:[${redirectTargetV6}]`]}]); + await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]`], username, credential}]); + await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]); + await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]`, `turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]); + }, + + async function stunV6Redirect() { + // This test uses the test TURN server, because nICEr drops responses + // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to + // an Allocate request. If we tried to use the NAT simulator for this, we + // would have to wait for nICEr to time out, since the NAT simulator does + // not know how to do MESSAGE-INTEGRITY. + await checkNoSrflx( + [{urls:[`stun:[${redirectTargetV6}]:${turnRedirectPort}`]}]); + }, + + async function turnV6UdpPortRedirect() { + await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`], username, credential}]); + }, + + async function turnV6TcpPortRedirect() { + await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]); + }, + + async function turnV6UdpTcpPortRedirect() { + await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`, `turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]); + }, + + async function turnV6UdpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6TcpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpTcpAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6TcpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpTcpEmptyRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', '']); + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6TcpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpTcpAddressAndPortRedirect() { + // This should result in two rounds of redirection; the first is by + // address, the second is by port. + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]); + await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`, `turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6TcpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpTcpRedirectLoop() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]); + // If we don't detect the loop, gathering will not finish + await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]); + await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6TcpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]); + await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + + async function turnV6UdpTcpMultipleAddressRedirect() { + await pushPrefs( + ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`], + ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]); + await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]); + await SpecialPowers.popPrefEnv(); + }, + ]; + + runNetworkTest(async () => { + const turnServer = iceServersArray.find(server => "username" in server); + username = turnServer.username; + credential = turnServer.credential; + // Special props, non-standard + turnRedirectPort = turnServer.turn_redirect_port; + turnsRedirectPort = turnServer.turns_redirect_port; + // Just use the first url. It might make sense to look for TURNS first, + // since that will always use a hostname, but on CI we don't have TURNS + // support anyway (see bug 1323439). + const turnHostname = getTurnHostname(turnServer.urls[0]); + + await pushPrefs( + ['media.peerconnection.ice.obfuscate_host_addresses', false], + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.ice.loopback', true], + ['media.getusermedia.insecure.enabled', true]); + + if (await ipv6Supported()) { + redirectTargetV6 = await dnsLookupV6(turnHostname); + if (redirectTargetV6 == '' && turnHostname == 'localhost') { + // Our testers don't seem to have IPv6 DNS resolution for localhost + // set up... + redirectTargetV6 = '::1'; + } + + if (redirectTargetV6 != '') { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + } else { + ok(false, `This machine has an IPv6 address, but ${turnHostname} does not resolve to an IPv6 address`); + } + } else { + ok(false, 'This machine appears to not have an IPv6 address'); + } + + await SpecialPowers.popPrefEnv(); + }, { useIceServer: true }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html new file mode 100644 index 0000000000..1faf464566 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html @@ -0,0 +1,596 @@ +<!DOCTYPE HTML> +<html> + +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> + +<body> + <pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1401592", + title: "Test that glean is recording stats as expected", + visible: true + }); + + const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + async function gleanResetTestValues() { + return SpecialPowers.spawnChrome([], async () => { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + } + ) + }; + + + async function getAllWarningRates() { + return { + warnNoGetparameters: + await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(), + warnLengthChanged: + await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(), + warnNoTransactionid: + await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(), + }; + } + + const tests = [ + async function checkRTCRtpSenderCount() { + const pc = new RTCPeerConnection(); + const oldCount = await GleanTest.rtcrtpsender.count.testGetValue() ?? 0; + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + const countDiff = await GleanTest.rtcrtpsender.count.testGetValue() - oldCount; + is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender"); + }, + + async function checkRTCRtpSenderSetParametersCompatCount() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const oldCount = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() ?? 0; + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + const countDiff = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() - oldCount; + is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender that uses the setParameters compat mode"); + }, + + async function checkSendEncodings() { + const pc = new RTCPeerConnection(); + const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender"); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded the use of sendEncodings"); + }, + + async function checkAddTransceiverNoSendEncodings() { + const pc = new RTCPeerConnection(); + const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + const { sender } = pc.addTransceiver('video'); + const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender"); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings"); + }, + + async function checkAddTrack() { + const pc = new RTCPeerConnection(); + const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc.addTrack(stream.getTracks()[0]); + const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue(); + is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender"); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings"); + }, + + async function checkGoodSetParametersCompatMode() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + const oldWarningRates = await getAllWarningRates(); + await sender.setParameters(sender.getParameters()); + const newWarningRates = await getAllWarningRates(); + isDeeply(oldWarningRates, newWarningRates); + }, + + async function checkBadSetParametersNoGetParametersWarning() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + + let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(); + let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0; + + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + + let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(); + let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning in setParameters due to lack of a getParameters call"); + + if (AppConstants.EARLY_BETA_OR_EARLIER) { + is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning"); + } else { + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel"); + } + + oldRate = newRate; + oldBlameCount = newBlameCount; + + // Glean should only record the warning once per sender! + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + + newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(); + newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning in setParameters due to lack of a getParameters call"); + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender"); + }, + + async function checkBadSetParametersLengthChangedWarning() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + + let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(); + let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0; + + let params = sender.getParameters(); + params.encodings.pop(); + await sender.setParameters(params); + + let newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(); + let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to a length change in encodings"); + + if (AppConstants.EARLY_BETA_OR_EARLIER) { + is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning"); + } else { + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel"); + } + + oldRate = newRate; + oldBlameCount = newBlameCount; + + // Glean should only record the warning once per sender! + params = sender.getParameters(); + params.encodings.pop(); + await sender.setParameters(params); + + newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(); + newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to a length change in encodings"); + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender"); + }, + + async function checkBadSetParametersRidChangedWarning() { + // This pref does not let rid change errors slide anymore + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + + let params = sender.getParameters(); + params.encodings[1].rid = "foo"; + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings"); + + // Glean should only record the error once per sender! + params = sender.getParameters(); + params.encodings[1].rid = "bar"; + oldRate = newRate; + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings"); + }, + + async function checkBadSetParametersNoTransactionIdWarning() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + + let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(); + let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0; + + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + + let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(); + let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to missing transactionId in setParameters"); + + if (AppConstants.EARLY_BETA_OR_EARLIER) { + is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning"); + } else { + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel"); + } + + oldRate = newRate; + oldBlameCount = newBlameCount; + + // Glean should only record the warning once per sender! + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + + newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(); + newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0; + + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to missing transactionId in setParameters"); + is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender"); + }, + + async function checkBadSetParametersLengthChangedError() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", false]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue(); + let params = sender.getParameters(); + params.encodings.pop(); + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a length change in encodings"); + + // Glean should only record the error once per sender! + params = sender.getParameters(); + params.encodings.pop(); + oldRate = newRate; + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a length change in encodings"); + }, + + async function checkBadSetParametersRidChangedError() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", false]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + let params = sender.getParameters(); + params.encodings[1].rid = "foo"; + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings"); + + // Glean should only record the error once per sender! + params = sender.getParameters(); + params.encodings[1].rid = "bar"; + oldRate = newRate; + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings"); + }, + + async function checkBadSetParametersNoGetParametersError() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", false]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue(); + try { + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error in setParameters due to lack of a getParameters call"); + + // Glean should only record the error once per sender! + oldRate = newRate; + try { + await sender.setParameters({ encodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] }); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error in setParameters due to lack of a getParameters call"); + }, + + async function checkBadSetParametersStaleTransactionIdError() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", false]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue(); + let params = sender.getParameters(); + // Cause transactionId to be stale + await pc.createOffer(); + // ...but make sure there is a recent getParameters call + sender.getParameters(); + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to stale transactionId in setParameters"); + + // Glean should only record the error once per sender! + oldRate = newRate; + params = sender.getParameters(); + // Cause transactionId to be stale + await pc.createOffer(); + // ...but make sure there is a recent getParameters call + sender.getParameters(); + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to stale transactionId in setParameters"); + }, + + async function checkBadSetParametersNoEncodingsError() { + // If we do not allow the old setParameters, this will fail the length check + // instead. + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue(); + let params = sender.getParameters(); + params.encodings = []; + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due to empty encodings in setParameters"); + + // Glean should only record the error once per sender! + oldRate = newRate; + params = sender.getParameters(); + params.encodings = []; + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due empty encodings in setParameters"); + }, + + async function checkBadSetParametersOtherError() { + const pc = new RTCPeerConnection(); + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{ rid: "0" }, { rid: "1" }, { rid: "2" }] + }); + let oldRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue(); + let params = sender.getParameters(); + params.encodings[0].scaleResolutionDownBy = 0.5; + try { + await sender.setParameters(params); + } catch (e) { + } + let newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue(); + is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to some other failure"); + + // Glean should only record the error once per sender! + oldRate = newRate; + params = sender.getParameters(); + params.encodings[0].scaleResolutionDownBy = 0.5; + try { + await sender.setParameters(params); + } catch (e) { + } + newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue(); + is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to some other failure"); + }, + + async function checkUlpfecNegotiated() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + let offer = await pc1.createOffer(); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // Validate logging shows ulpfec negotiated and preferred video VP8 + let ulpfecNotNegotiated = await GleanTest.codecStats.ulpfecNegotiated.not_negotiated.testGetValue() || 0; + ok(ulpfecNotNegotiated == 0, "checkUlpfecNegotiated glean should not count not_negotiated"); + let ulpfecNegotiated = await GleanTest.codecStats.ulpfecNegotiated.negotiated.testGetValue() || 0; + ok(ulpfecNegotiated == 2, "checkUlpfecNegotiated glean should show ulpfec negotiated"); + let preferredVideoCodec = await GleanTest.codecStats.videoPreferredCodec.VP8.testGetValue() || 0; + ok(preferredVideoCodec == 2, "checkUlpfecNegotiated glean should show preferred video codec VP8"); + }, + + async function checkNoUlpfecNegotiated() { + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + let offer = await pc1.createOffer(); + const payloadType = sdputils.findCodecId(offer.sdp, "ulpfec"); + offer.sdp = sdputils.removeCodec(offer.sdp, payloadType); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // Validate logging shows ulpfec not negotiated and preferred video VP8 + let ulpfecNotNegotiated = await GleanTest.codecStats.ulpfecNegotiated.not_negotiated.testGetValue() || 0; + ok(ulpfecNotNegotiated == 2, "checkNoUlpfecNegotiated glean should count not_negotiated"); + let ulpfecNegotiated = await GleanTest.codecStats.ulpfecNegotiated.negotiated.testGetValue() || 0; + ok(ulpfecNegotiated == 0, "checkNoUlpfecNegotiated glean should not show ulpfec negotiated " + ulpfecNegotiated); + let preferredVideoCodec = await GleanTest.codecStats.videoPreferredCodec.VP8.testGetValue() || 0; + ok(preferredVideoCodec == 2, "checkNoUlpfecNegotiated glean should show preferred video codec VP8"); + }, + + async function checkFlexfecOffered() { + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + let offer = await pc1.createOffer(); + offer.sdp = offer.sdp.replaceAll('VP8','flexfec'); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // Validate logging shows flexfec counted once ulpfec negotiated twice and preferred video VP9 since no VP8 was offered. + let flexfecOffered = await GleanTest.codecStats.otherFecSignaled.flexfec.testGetValue() || 0; + ok(flexfecOffered == 1, "checkFlexfecOffered glean should count flexfec being offered" + flexfecOffered); + let ulpfecNegotiated = await GleanTest.codecStats.ulpfecNegotiated.negotiated.testGetValue() || 0; + ok(ulpfecNegotiated == 2, "checkUlpfecNegotiated glean should show ulpfec negotiated"); + let preferredVideoCodec = await GleanTest.codecStats.videoPreferredCodec.VP9.testGetValue() || 0; + ok(preferredVideoCodec == 2, "checkFlexfecOffered glean should show preferred video codec VP9"); + }, + + async function checkPreferredVideoCodec() { + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + let offer = await pc1.createOffer(); + // Set a video codec that does not exist to simulate receiving codecs we dont support + // and verify it gets logged. + offer.sdp = offer.sdp.replaceAll('VP8','AVADA1'); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // We should show AVADA1 as the preferred codec from the offer and the answer should prefer VP9 since VP8 was removed. + let preferredVideoCodecAVADA1 = await GleanTest.codecStats.videoPreferredCodec.AVADA1.testGetValue() || 0; + ok(preferredVideoCodecAVADA1 == 1, "checkPreferredVideoCodec glean should show preferred video codec AVADA1" + preferredVideoCodecAVADA1); + let preferredVideoCodecVP9 = await GleanTest.codecStats.videoPreferredCodec.VP9.testGetValue() || 0; + ok(preferredVideoCodecVP9 == 1, "checkPreferredVideoCodec glean should show preferred video codec VP9" + preferredVideoCodecVP9); + }, + + async function checkPreferredAudioCodec() { + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + let offer = await pc1.createOffer(); + // Set an audio codec that does not exist to simulate receiving codecs we dont support + // and verify it gets logged. + offer.sdp = offer.sdp.replaceAll('opus','FAKECodec'); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // We should show CN as the preferred codec from the offer and the answer should prefer G722 since opus was removed. + let preferredAudioCodecFAKECodec = await GleanTest.codecStats.audioPreferredCodec.FAKECodec.testGetValue() || 0; + ok(preferredAudioCodecFAKECodec == 1, "checkPreferredAudioCodec Glean should show preferred audio codec FAKECodec " + preferredAudioCodecFAKECodec); + let preferredAudioCodecG722 = await GleanTest.codecStats.audioPreferredCodec.G722.testGetValue() || 0; + ok(preferredAudioCodecG722 == 1, "checkPreferredAudioCodec Glean should show preferred audio codec G722 " + preferredAudioCodecG722); + }, + + async function checkLoggingMultipleTransceivers() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + await gleanResetTestValues(); + + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + pc1.addTransceiver(stream.getTracks()[0]); + pc1.addTransceiver(stream.getTracks()[0]); + + let offer = await pc1.createOffer(); + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + let answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + pc1.setLocalDescription(offer); + pc2.setRemoteDescription(offer); + answer = await pc2.createAnswer(); + pc2.setLocalDescription(answer); + pc1.setRemoteDescription(answer); + + // Validate logging shows for each transciever but is not duplicated with the renegotiation + let ulpfecNotNegotiated = await GleanTest.codecStats.ulpfecNegotiated.not_negotiated.testGetValue() || 0; + ok(ulpfecNotNegotiated == 0, "checkLoggingMultipleTransceivers glean should not count not_negotiated"); + let ulpfecNegotiated = await GleanTest.codecStats.ulpfecNegotiated.negotiated.testGetValue() || 0; + ok(ulpfecNegotiated == 6, "checkLoggingMultipleTransceivers glean should show ulpfec negotiated " + ulpfecNegotiated); + let preferredVideoCodec = await GleanTest.codecStats.videoPreferredCodec.VP8.testGetValue() || 0; + ok(preferredVideoCodec == 6, "checkLoggingMultipleTransceivers glean should show preferred video codec VP8 " + preferredVideoCodec); + }, + + ]; + + runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html new file mode 100644 index 0000000000..1b82473997 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1087629", + title: "Wait for ICE failure" + }); + +// Test iceFailure + +function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {}); +} +function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) { + test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {}); +} +function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) { + var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0}; + test.pcRemote.storeOrAddIceCandidate(cand); + info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand)); +} +function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) { + var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0}; + test.pcLocal.storeOrAddIceCandidate(cand); + info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand)); +} +function PC_LOCAL_WAIT_FOR_ICE_FAILURE(test) { + return test.pcLocal.iceFailed.then(() => { + ok(true, this.pcLocal + " Ice Failure Reached."); + }); +} +function PC_REMOTE_WAIT_FOR_ICE_FAILURE(test) { + return test.pcRemote.iceFailed.then(() => { + ok(true, this.pcRemote + " Ice Failure Reached."); + }); +} +function PC_LOCAL_WAIT_FOR_ICE_FAILED(test) { + var resolveIceFailed; + test.pcLocal.iceFailed = new Promise(r => resolveIceFailed = r); + test.pcLocal.ice_connection_callbacks.checkIceStatus = () => { + if (test.pcLocal._pc.iceConnectionState === "failed") { + resolveIceFailed(); + } + } +} +function PC_REMOTE_WAIT_FOR_ICE_FAILED(test) { + var resolveIceFailed; + test.pcRemote.iceFailed = new Promise(r => resolveIceFailed = r); + test.pcRemote.ice_connection_callbacks.checkIceStatus = () => { + if (test.pcRemote._pc.iceConnectionState === "failed") { + resolveIceFailed(); + } + } +} + +runNetworkTest(async () => { + await pushPrefs( + ['media.peerconnection.ice.stun_client_maximum_transmits', 3], + ['media.peerconnection.ice.trickle_grace_period', 3000], + ); + var test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER); + test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER); + test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_FAILED); + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_FAILED", PC_REMOTE_WAIT_FOR_ICE_FAILED); + test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION"); + test.chain.append([ + PC_REMOTE_ADD_FAKE_ICE_CANDIDATE, + PC_LOCAL_ADD_FAKE_ICE_CANDIDATE, + PC_LOCAL_WAIT_FOR_ICE_FAILURE, + PC_REMOTE_WAIT_FOR_ICE_FAILURE + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html new file mode 100644 index 0000000000..d5265470e9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1291715", + title: "Test insertDTMF on sender", + visible: true +}); + +function insertdtmftest(pc) { + ok(pc.getSenders().length, "have senders"); + var sender = pc.getSenders()[0]; + ok(sender.dtmf, "sender has dtmf object"); + + ok(sender.dtmf.toneBuffer === "", "sender should start with empty tonebuffer"); + + // These will trigger assertions on debug builds if we do not enforce the + // specified minimums and maximums for duration and interToneGap. + sender.dtmf.insertDTMF("A", 10); + sender.dtmf.insertDTMF("A", 10000); + sender.dtmf.insertDTMF("A", 70, 10); + + var threw = false; + try { + sender.dtmf.insertDTMF("bad tones"); + } catch (ex) { + threw = true; + is(ex.code, DOMException.INVALID_CHARACTER_ERR, "Expected InvalidCharacterError"); + } + ok(threw, "Expected exception"); + + sender.dtmf.insertDTMF("A"); + sender.dtmf.insertDTMF("B"); + ok(!sender.dtmf.toneBuffer.includes("A"), "calling insertDTMF should replace current characters"); + + sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + ok(sender.dtmf.toneBuffer.includes("A"), "lowercase characters should be normalized"); + + pc.removeTrack(sender); + threw = false; + try { + sender.dtmf.insertDTMF("AAA"); + } catch (ex) { + threw = true; + is(ex.code, DOMException.INVALID_STATE_ERR, "Expected InvalidStateError"); + } + ok(threw, "Expected exception"); +} + +runNetworkTest(() => { + test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW"); + + // Test sender dtmf. + test.chain.append([ + function PC_LOCAL_INSERT_DTMF(test) { + // We want to call removeTrack + test.pcLocal.expectNegotiationNeeded(); + return insertdtmftest(test.pcLocal._pc); + } + ]); + + return test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html new file mode 100644 index 0000000000..16406ece6e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "952145", + title: "Rollback local reoffer" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}]); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + + function PC_LOCAL_CREATE_AND_SET_OFFER(test) { + return test.createOffer(test.pcLocal).then(offer => { + return test.setLocalDescription(test.pcLocal, offer, HAVE_LOCAL_OFFER); + }); + }, + + function PC_LOCAL_ROLLBACK(test) { + // the negotiationNeeded slot should have been true both before and + // after this SLD, so the event should fire again. + test.pcLocal.expectNegotiationNeeded(); + return test.setLocalDescription(test.pcLocal, + { type: "rollback", sdp: "" }, + STABLE); + }, + ]); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html new file mode 100644 index 0000000000..5bdc8cc029 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "952145", + title: "Rollback local offer" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [ + function PC_REMOTE_CREATE_AND_SET_OFFER(test) { + return test.createOffer(test.pcRemote).then(offer => { + return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER); + }); + }, + + function PC_REMOTE_ROLLBACK(test) { + // the negotiationNeeded slot should have been true both before and + // after this SLD, so the event should fire again. + test.pcRemote.expectNegotiationNeeded(); + return test.setLocalDescription(test.pcRemote, + { type: "rollback", sdp: "" }, + STABLE); + }, + + // Rolling back should shut down gathering + function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcRemote.endOfTrickleIce; + }, + + function PC_REMOTE_SETUP_ICE_HANDLER(test) { + test.pcRemote.setupIceCandidateHandler(test); + }, + ]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html new file mode 100644 index 0000000000..7f7431aa80 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1393687", + title: "Enforce max-fs constraint on a PeerConnection", + visible: true + }); + + var mustRejectWith = (msg, reason, f) => + f().then(() => ok(false, msg), + e => is(e.name, reason, msg)); + + var removeAllButCodec = (d, codec) => { + d.sdp = d.sdp.replace(/m=video (\w) UDP\/TLS\/RTP\/SAVPF \w.*\r\n/, + "m=video $1 UDP/TLS/RTP/SAVPF " + codec + "\r\n"); + return d; + }; + + var mungeSDP = (d, forceH264) => { + if (forceH264) { + removeAllButCodec(d, 126); + d.sdp = d.sdp.replace(/a=fmtp:126 (.*);packetization-mode=1/, "a=fmtp:126 $1;packetization-mode=1;max-fs=100"); + } else { + d.sdp = d.sdp.replace(/max-fs=\d+/, "max-fs=100"); + } + return d; + }; + + const checkForH264Support = async () => { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/); + }; + + let resolutionAlignment = 1; + + function testScale(codec) { + var v1 = createMediaElement('video', 'v1'); + var v2 = createMediaElement('video', 'v2'); + + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + info("testing max-fs with" + codec); + + pc1.onnegotiationneeded = e => + pc1.createOffer() + .then(d => pc1.setLocalDescription(mungeSDP(d, codec == "H264"))) + .then(() => pc2.setRemoteDescription(pc1.localDescription)) + .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(mungeSDP(d, codec =="H264"))) + .then(() => pc1.setRemoteDescription(pc2.localDescription)) + .catch(generateErrorCallback()); + + pc2.ontrack = e => { + v2.srcObject = e.streams[0]; + }; + + var stream; + + return navigator.mediaDevices.getUserMedia({ video: true }) + .then(s => { + stream = s; + v1.srcObject = stream; + let track = stream.getVideoTracks()[0]; + let sender = pc1.addTrack(track, stream); + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + }) + .then(() => wait(5000)) + .then(() => { + if (v2.videoWidth == 0 && v2.videoHeight == 0) { + info("Skipping test, insufficient time for video to start."); + } else { + const expectedWidth = 184 - 184 % resolutionAlignment; + const expectedHeight = 138 - 138 % resolutionAlignment; + is(v2.videoWidth, expectedWidth, + `sink width should be ${expectedWidth} for ${codec}`); + is(v2.videoHeight, expectedHeight, + `sink height should be ${expectedHeight} for ${codec}`); + }}) + .then(() => { + stream.getTracks().forEach(track => track.stop()); + v1.srcObject = v2.srcObject = null; + }).catch(generateErrorCallback()); + } + + runNetworkTest(async () => { + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + if (await checkForH264Support()) { + if (navigator.userAgent.includes("Android")) { + // Android only has a hw encoder for h264 + resolutionAlignment = 16; + } + await matchPlatformH264CodecPrefs(); + await testScale("H264"); + } + + // Disable h264 hardware support, to ensure it is not prioritized over VP8 + await pushPrefs(["media.webrtc.hw.h264.enabled", false]); + await testScale("VP8"); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html new file mode 100644 index 0000000000..9ad25e7852 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1166832", + title: "Canvas(2D)::Multiple CaptureStream as video-only input to peerconnection", + visible: true +}); + +/** + * Test to verify using multiple capture streams concurrently. + */ +runNetworkTest(async () => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]); + await pushPrefs(["media.webrtc.hw.h264.enabled", false]); + } + + var test = new PeerConnectionTest(); + var h = new CaptureStreamTestHelper2D(50, 50); + + var vremote1; + var stream1; + var canvas1 = h.createAndAppendElement('canvas', 'source_canvas1'); + + var vremote2; + var stream2; + var canvas2 = h.createAndAppendElement('canvas', 'source_canvas2'); + + const threshold = 128; + + test.setMediaConstraints([{video: true}, {video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + h.drawColor(canvas1, h.green); + h.drawColor(canvas2, h.blue); + stream1 = canvas1.captureStream(0); // fps = 0 to capture single frame + test.pcLocal.attachLocalStream(stream1); + stream2 = canvas2.captureStream(0); // fps = 0 to capture single frame + test.pcLocal.attachLocalStream(stream2); + var i = 0; + return setInterval(function() { + try { + info("draw " + i ? "green" : "red/blue"); + h.drawColor(canvas1, i ? h.green : h.red); + h.drawColor(canvas2, i ? h.green : h.blue); + i = 1 - i; + stream1.requestFrame(); + stream2.requestFrame(); + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + ]); + + test.chain.append([ + function CHECK_REMOTE_VIDEO() { + is(test.pcRemote.remoteMediaElements.length, 2, "pcRemote Should have 2 remote media elements"); + vremote1 = test.pcRemote.remoteMediaElements[0]; + vremote2 = test.pcRemote.remoteMediaElements[1]; + + // since we don't know which remote video is created first, we don't know + // which should be blue or red, but this will make sure that one is + // green and one is blue + return Promise.race([ + Promise.all([ + h.pixelMustBecome(vremote1, h.red, { + threshold, + infoString: "pcRemote's remote1 should become red", + }), + h.pixelMustBecome(vremote2, h.blue, { + threshold, + infoString: "pcRemote's remote2 should become blue", + }), + ]), + Promise.all([ + h.pixelMustBecome(vremote2, h.red, { + threshold, + infoString: "pcRemote's remote2 should become red", + }), + h.pixelMustBecome(vremote1, h.blue, { + threshold, + infoString: "pcRemote's remote1 should become blue", + }), + ]) + ]); + }, + function WAIT_FOR_REMOTE_BOTH_GREEN() { + return Promise.all([ + h.pixelMustBecome(vremote1, h.green, { + threshold, + infoString: "pcRemote's remote1 should become green", + }), + h.pixelMustBecome(vremote2, h.green, { + threshold, + infoString: "pcRemote's remote2 should become green", + }), + ]) + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html new file mode 100644 index 0000000000..7e3fd78430 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1060102", + title: "Basic audio only SDP answer without trickle ICE" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html new file mode 100644 index 0000000000..12b2a95596 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1060102", + title: "Basic audio only SDP offer without trickle ICE" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + makeOffererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html new file mode 100644 index 0000000000..554750e975 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1060102", + title: "Basic audio only SDP offer and answer without trickle ICE" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html new file mode 100644 index 0000000000..ad9414cef2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html @@ -0,0 +1,200 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ title: "PeerConnection feed to a graph with non default rate", bug: "1387454" }); + /** + * Run a test to verify that when we use the streem with nonDefault rate to/from a PC + * the connection fails. (PC is always on default rate). + */ + + let pc1; + let pc2; + + const offerOptions = { + offerToReceiveAudio: 1, + }; + + function getName(pc) { + return (pc === pc1) ? 'pc1' : 'pc2'; + } + + function getOtherPc(pc) { + return (pc === pc1) ? pc2 : pc1; + } + + function onAddIceCandidateSuccess(pc) { + ok(true, getName(pc) + ' addIceCandidate success'); + } + + function onAddIceCandidateError(pc, error) { + ok(false, getName(pc) + ' failed to add ICE Candidate: ' + error.toString()); + } + + function onIceCandidate(pc, event, done) { + if (!event.candidate) { + ok(pc.iceGatheringState === 'complete', getName(pc) + " ICE gathering state has reached complete"); + done(); + return; + } + getOtherPc(pc).addIceCandidate(event.candidate) + .then(() => { + onAddIceCandidateSuccess(pc); + }, + (err) => { + onAddIceCandidateError(pc, err); + }); + info(getName(pc) + ' ICE candidate: ' + event.candidate.candidate); + } + + function onIceStateChange(pc, event) { + if (pc) { + info(getName(pc) + ' ICE state: ' + pc.iceConnectionState); + info('ICE state change event: ', event); + } + } + + function onCreateOfferSuccess(desc) { + info('Offer from pc1\n' + desc.sdp); + info('pc1 setLocalDescription start'); + + pc1.setLocalDescription(desc) + .then(() => { + onSetLocalSuccess(pc1); + }, + onSetSessionDescriptionError); + + info('pc2 setRemoteDescription start'); + pc2.setRemoteDescription(desc).then(() => { + onSetRemoteSuccess(pc2); + }, + onSetSessionDescriptionError); + + info('pc2 createAnswer start'); + + // Since the 'remote' side has no media stream we need + // to pass in the right constraints in order for it to + // accept the incoming offer of audio and video. + pc2.createAnswer() + .then(onCreateAnswerSuccess, onCreateSessionDescriptionError); + } + + function onSetSessionDescriptionError(error) { + ok(false, 'Failed to set session description: ' + error.toString()); + } + + function onSetLocalSuccess(pc) { + ok(true, getName(pc) + ' setLocalDescription complete'); + } + + function onCreateSessionDescriptionError(error) { + ok(false, 'Failed to create session description: ' + error.toString()); + } + + function onSetRemoteSuccess(pc) { + ok(true, getName(pc) + ' setRemoteDescription complete'); + } + + function onCreateAnswerSuccess(desc) { + info('Answer from pc2:\n' + desc.sdp); + info('pc2 setLocalDescription start'); + pc2.setLocalDescription(desc).then(() => { + onSetLocalSuccess(pc2); + }, + onSetSessionDescriptionError); + info('pc1 setRemoteDescription start'); + pc1.setRemoteDescription(desc).then(() => { + onSetRemoteSuccess(pc1); + }, + onSetSessionDescriptionError); + } + + async function getRemoteStream(localStream) { + info("got local stream") + const audioTracks = localStream.getAudioTracks(); + + const servers = null; + + pc1 = new RTCPeerConnection(servers); + info('Created local peer connection object pc1'); + const iceComplete1 = new Promise((resolve, reject) => { + pc1.onicecandidate = (e) => { + onIceCandidate(pc1, e, resolve); + }; + }); + + pc2 = new RTCPeerConnection(servers); + info('Created remote peer connection object pc2'); + const iceComplete2 = new Promise((resolve, reject) => { + pc2.onicecandidate = (e) => { + onIceCandidate(pc2, e, resolve); + }; + }); + + pc1.oniceconnectionstatechange = (e) => { + onIceStateChange(pc1, e); + }; + pc2.oniceconnectionstatechange = (e) => { + onIceStateChange(pc2, e); + }; + + const remoteStreamPromise = new Promise((resolve, reject) => { + pc2.ontrack = (e) => { + info('pc2 received remote stream ' + e.streams[0]); + resolve(e.streams[0]); + }; + }); + + localStream.getTracks().forEach((track) => { + pc1.addTrack(track, localStream); + }); + info('Added local stream to pc1'); + + info('pc1 createOffer start'); + pc1.createOffer(offerOptions) + .then(onCreateOfferSuccess,onCreateSessionDescriptionError); + + let promise_arr = await Promise.all([remoteStreamPromise, iceComplete1, iceComplete2]); + return promise_arr[0]; + } + + runTest(async () => { + // Local stream operates at non default rate (32000) + const nonDefaultRate = 32000; + const nonDefault_ctx = new AudioContext({sampleRate: nonDefaultRate}); + oscillator = nonDefault_ctx.createOscillator(); + const dest = nonDefault_ctx.createMediaStreamDestination(); + oscillator.connect(dest); + oscillator.start(); + + // Wait for remote stream + const remoteStream = await getRemoteStream(dest.stream) + ok(true, 'Got remote stream ' + remoteStream); + + // remoteStream now comes from PC so operates at default + // rates. Verify that by adding to a default context + const ac = new AudioContext; + const source_default_rate = ac.createMediaStreamSource(remoteStream); + + // Now try to add the remoteStream on a non default context + mustThrowWith( + "Connect stream with graph of different sample rate", + "NotSupportedError", () => { + nonDefault_ctx.createMediaStreamSource(remoteStream); + } + ); + + // Close peer connections to make sure we don't get error: + // "logged result after SimpleTest.finish(): pc1 addIceCandidate success" + // See Bug 1626814. + pc1.close(); + pc2.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html new file mode 100644 index 0000000000..1f936714f1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "850275", + title: "Simple offer media constraint test with audio" + }); + + runNetworkTest(function() { + var test = new PeerConnectionTest(); + test.setMediaConstraints([], [{audio: true}]); + test.setOfferOptions({ offerToReceiveAudio: true }); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html new file mode 100644 index 0000000000..c5afbb5c1f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "850275", + title: "Simple offer media constraint test with video" + }); + + runNetworkTest(function() { + var test = new PeerConnectionTest(); + test.setMediaConstraints([], [{video: true}]); + test.setOfferOptions({ offerToReceiveVideo: true }); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html new file mode 100644 index 0000000000..d7bc29c6d3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "850275", + title: "Simple offer media constraint test with video/audio" + }); + + runNetworkTest(function() { + var test = new PeerConnectionTest(); + test.setMediaConstraints([], [{audio: true, video: true}]); + test.setOfferOptions({ offerToReceiveVideo: true, offerToReceiveAudio: true }); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html new file mode 100644 index 0000000000..7cd695ff54 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1677046", + title: "RTCPeerConnection check restricted ports" + }); + +var makePC = (config, expected_error) => { + var exception; + try { + new RTCPeerConnection(config).close(); + } catch (e) { + exception = e; + } + is((exception? exception.name : "success"), expected_error || "success", + "RTCPeerConnection(" + JSON.stringify(config) + ")"); +}; + +// This is a test of the iceServers parsing code + readable errors +runNetworkTest(() => { + var exception = null; + + // check various ports on the blocklist + makePC({ iceServers: [ + { urls:"turn:[::1]:6666", username:"p", credential:"p" }] }, "NS_ERROR_UNEXPECTED"); + makePC({ iceServers: [ + { urls:"turns:localhost:6667?transport=udp", username:"p", credential:"p" }] }, + "NS_ERROR_UNEXPECTED"); + makePC({ iceServers: [ + { urls:"stun:localhost:21", foo:"" }] }, "NS_ERROR_UNEXPECTED"); + makePC({ iceServers: [ + { urls:"stun:[::1]:22", foo:"" }] }, "NS_ERROR_UNEXPECTED"); + makePC({ iceServers: [ + { urls:"turn:localhost:5060", username:"p", credential:"p" }] }, + "NS_ERROR_UNEXPECTED"); + + // check various ports on the good list for webrtc (or default port) + makePC({ iceServers: [ + { urls:"turn:[::1]:53", username:"p", credential:"p" }, + { urls:"turn:[::1]:5349", username:"p", credential:"p" }, + { urls:"turn:[::1]:3478", username:"p", credential:"p" }, + { urls:"turn:[::1]", username:"p", credential:"p" }, + { urls:"turn:localhost:53?transport=udp", username:"p", credential:"p" }, + { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" }, + { urls:"turn:localhost:53?transport=tcp", username:"p", credential:"p" }, + { urls:"turn:localhost:3478?transport=tcp", username:"p", credential:"p" }, + { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" }, + { urls:"stun:localhost", foo:"" } + ]}); + + // not in the known good ports and not on the generic block list + makePC({ iceServers: [{ urls:"turn:localhost:6664", username:"p", credential:"p" }] }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html new file mode 100644 index 0000000000..a3fbb5753c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1091898", + title: "PeerConnection with promises (sendonly)", + visible: true + }); + + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + var v1, v2; + var delivered = new Promise(resolve => pc2.ontrack = e => { + // Test RTCTrackEvent here. + ok(e.streams.length, "has streams"); + ok(e.streams[0].getTrackById(e.track.id), "has track"); + ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver"); + if (e.streams[0].getTracks().length == 2) { + // Test RTCTrackEvent required args here. + mustThrowWith("RTCTrackEvent wo/required args", + "TypeError", () => new RTCTrackEvent("track", {})); + v2.srcObject = e.streams[0]; + resolve(); + } + }); + + runNetworkTest(function() { + v1 = createMediaElement('video', 'v1'); + v2 = createMediaElement('video', 'v2'); + var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve()); + + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + + return navigator.mediaDevices.getUserMedia({ video: true, audio: true }) + .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream))) + .then(() => pc1.createOffer({})) // check that createOffer accepts arg. + .then(offer => pc1.setLocalDescription(offer)) + .then(() => pc2.setRemoteDescription(pc1.localDescription)) + .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg. + .then(answer => pc2.setLocalDescription(answer)) + .then(() => pc1.setRemoteDescription(pc2.localDescription)) + .then(() => delivered) +// .then(() => canPlayThrough) // why doesn't this fire? + .then(() => waitUntil(() => v2.currentTime > 0)) + .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")")) + .then(() => ok(true, "Connected.")); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html new file mode 100644 index 0000000000..d5cb91b048 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> +<script src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script> +createHTML({ + bug: "1212237", + title: "Recording a fresh receive track should not throw", + visible: true, +}); + +/** + * Called when a fresh track is available, and tests that the track can be + * recorded until it ends without any thrown errors or fired error events. + */ +let generation = 0; +async function testTrackAccessible(track) { + const id = ++generation; + info(`Testing accessibility for ${track.kind} track ${id}`); + const recorder = new MediaRecorder(new MediaStream([track])); + recorder.start(); + let haveError = new Promise((_, rej) => recorder.onerror = e => rej(e.error)); + await Promise.race([ + new Promise(r => recorder.onstart = r), + haveError, + ]); + info(`Recording of ${track.kind} track ${id} started`); + + const {data} = await Promise.race([ + new Promise(r => recorder.ondataavailable = r), + haveError, + ]); + info(`Recording of ${track.kind} track ${id} finished at size ${data.size}`); + + await Promise.race([ + new Promise(r => recorder.onstop = r), + haveError, + ]); + info(`Recording of ${track.kind} track ${id} stopped`); + + const element = createMediaElement(track.kind, `recording_${track.id}`); + const url = URL.createObjectURL(data); + try { + element.src = url; + element.preload = "metadata"; + haveError = new Promise( + (_, rej) => element.onerror = e => rej(element.error)); + await Promise.race([ + new Promise(r => element.onloadeddata = r), + haveError, + ]); + info(`Playback of recording of ${track.kind} track ${id} loaded data`); + + element.play(); + await Promise.race([ + new Promise(r => element.onended = r), + haveError, + ]); + info(`Playback of recording of ${track.kind} track ${id} ended`); + } finally { + URL.revokeObjectURL(data); + } +} + +runNetworkTest(async options => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{audio: true}]); + test.setOfferOptions({offerToReceiveAudio: true}); + const freshVideoTrackIsAccessible = new Promise( + r => test.pcRemote._pc.addEventListener("track", r, {once: true}) + ).then(({track}) => testTrackAccessible(track)); + const freshAudioTrackIsAccessible = new Promise( + r => test.pcLocal._pc.addEventListener("track", r, {once: true}) + ).then(({track}) => testTrackAccessible(track)); + test.chain.append([ + function PC_CLOSE_TO_END_TRACKS() { + return test.close(); + }, + async function FRESH_VIDEO_TRACK_IS_ACCESSIBLE() { + await freshVideoTrackIsAccessible; + ok(true, "A freshly received video track is accessible by MediaRecorder"); + }, + async function FRESH_AUDIO_TRACK_IS_ACCESSIBLE() { + await freshAudioTrackIsAccessible; + ok(true, "A freshly received audio track is accessible by MediaRecorder"); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html new file mode 100644 index 0000000000..3b07783c04 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1187775", + title: "peer connection ICE fails on relay-only without TURN" +}); + +function PC_LOCAL_NO_CANDIDATES(test) { + var isnt = can => is(can, null, "No candidates: " + JSON.stringify(can)); + test.pcLocal._pc.addEventListener("icecandidate", e => isnt(e.candidate)); +} + +function PC_BOTH_WAIT_FOR_ICE_FAILED(test) { + var isFail = (f, reason, msg) => + f().then(() => { throw new Error(msg + " must fail"); }, + e => is(e.message, reason, msg + " must fail with: " + e.message)); + + return Promise.all([ + isFail(() => test.pcLocal.waitForIceConnected(), "ICE failed", "Local ICE"), + isFail(() => test.pcRemote.waitForIceConnected(), "ICE failed", "Remote ICE") + ]) + .then(() => ok(true, "ICE on both sides must fail.")); +} + +runNetworkTest(async options => { + await pushPrefs( + ['media.peerconnection.ice.stun_client_maximum_transmits', 3], + ['media.peerconnection.ice.trickle_grace_period', 5000] + ); + options = options || {}; + options.config_local = options.config_local || {}; + const servers = options.config_local.iceServers || []; + // remove any turn servers + options.config_local.iceServers = servers.filter(server => + server.urls.every(u => !u.toLowerCase().startsWith('turn'))); + + // Here's the setting we're testing. Comment out and this test should fail: + options.config_local.iceTransportPolicy = "relay"; + + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + test.chain.remove("PC_LOCAL_SETUP_ICE_LOGGER"); // Needed to suppress failing + test.chain.remove("PC_REMOTE_SETUP_ICE_LOGGER"); // on ICE-failure. + test.chain.insertAfter("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_NO_CANDIDATES); + test.chain.replace("PC_LOCAL_WAIT_FOR_ICE_CONNECTED", PC_BOTH_WAIT_FOR_ICE_FAILED); + test.chain.removeAfter("PC_BOTH_WAIT_FOR_ICE_FAILED"); + await test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html new file mode 100644 index 0000000000..80aa30beaa --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "952145", + title: "Rollback remote reoffer" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}]); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + ] + ); + test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION', + [ + function PC_REMOTE_ROLLBACK(test) { + return test.setRemoteDescription(test.pcRemote, { type: "rollback" }, + STABLE); + }, + + function PC_LOCAL_ROLLBACK(test) { + // We haven't negotiated the new stream yet. + test.pcLocal.expectNegotiationNeeded(); + return test.setLocalDescription( + test.pcLocal, + new RTCSessionDescription({ type: "rollback", sdp: ""}), + STABLE); + }, + ], + 1 // Second PC_REMOTE_SET_REMOTE_DESCRIPTION + ); + test.chain.append(commandsPeerConnectionOfferAnswer); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html new file mode 100644 index 0000000000..827646b0de --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "952145", + title: "Rollback remote offer" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter('PC_REMOTE_CHECK_CAN_TRICKLE_SYNC'); + test.chain.append([ + function PC_REMOTE_ROLLBACK(test) { + // We still haven't negotiated the tracks + test.pcRemote.expectNegotiationNeeded(); + return test.setRemoteDescription(test.pcRemote, { type: "rollback" }, + STABLE); + }, + + function PC_REMOTE_CHECK_CAN_TRICKLE_REVERT_SYNC(test) { + is(test.pcRemote._pc.canTrickleIceCandidates, null, + "Remote canTrickleIceCandidates is reverted to null"); + }, + + function PC_LOCAL_ROLLBACK(test) { + // We still haven't negotiated the tracks + test.pcLocal.expectNegotiationNeeded(); + return test.setLocalDescription( + test.pcLocal, + new RTCSessionDescription({ type: "rollback", sdp: ""}), + STABLE); + }, + + // Rolling back should shut down gathering + function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleIce; + }, + ]); + test.chain.append(commandsPeerConnectionOfferAnswer); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html new file mode 100644 index 0000000000..05d7cff434 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove audio track" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + let receivedTrack, analyser, freq; + addRenegotiation(test.chain, + [ + function PC_REMOTE_SETUP_ANALYSER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver before renegotiation"); + + receivedTrack = test.pcRemote._pc.getReceivers()[0].track; + is(receivedTrack.readyState, "live", + "The received track should be live"); + + analyser = new AudioStreamAnalyser( + audioContext, new MediaStream([receivedTrack])); + freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + + return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200); + }, + function PC_LOCAL_REMOVE_AUDIO_TRACK(test) { + test.setOfferOptions({ offerToReceiveAudio: true }); + return test.pcLocal.removeSender(0); + }, + ], + [ + function PC_REMOTE_CHECK_FLOW_STOPPED(test) { + // Simply removing a track is not enough to cause it to be + // signaled as ended. Spec may change though. + // TODO: One last check of the spec is in order + is(receivedTrack.readyState, "live", + "The received track should not have ended"); + + return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50); + }, + ] + ); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run() + .finally(() => tone.stop()); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html new file mode 100644 index 0000000000..9ac607e533 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove then add audio track" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + let originalTrack; + let haveMuteEvent = new Promise(() => {}); + let haveUnmuteEvent = new Promise(() => {}); + addRenegotiation(test.chain, + [ + function PC_REMOTE_FIND_RECEIVER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver"); + originalTrack = test.pcRemote._pc.getReceivers()[0].track; + }, + function PC_LOCAL_REMOVE_AUDIO_TRACK(test) { + return test.pcLocal.removeSender(0); + }, + function PC_LOCAL_ADD_AUDIO_TRACK(test) { + // The new track's pipeline will start with a packet count of + // 0, but the remote side will keep its old pipeline and packet + // count. + test.pcLocal.disableRtpCountChecking = true; + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + ], + [ + function PC_REMOTE_WAIT_FOR_UNMUTE() { + return haveUnmuteEvent; + }, + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[1].receiver.track; + + const analyser = new AudioStreamAnalyser( + audioContext, new MediaStream([track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200); + }, + function PC_REMOTE_WAIT_FOR_MUTE() { + return haveMuteEvent; + }, + function PC_REMOTE_CHECK_REMOVED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const analyser = new AudioStreamAnalyser( + audioContext, new MediaStream([track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50); + } + ] + ); + + // The first track should mute when the connection is closed. + test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONMUTE(test) { + haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute"); + } + ]); + + // Second negotiation should cause the second track to unmute. + test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONUNMUTE(test) { + haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute"); + } + ], false, 1); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run() + .finally(() => tone.stop()); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html new file mode 100644 index 0000000000..274de6848f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove then add audio track" + }); + + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + const test = new PeerConnectionTest(options); + const audioContext = new AudioContext(); + // Start a tone so that the gUM call will record something even with + // --use-test-media-devices. + const tone = new LoopbackTone(audioContext, TEST_AUDIO_FREQ); + tone.start(); + let originalTrack; + addRenegotiation(test.chain, + [ + function PC_REMOTE_FIND_RECEIVER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver"); + originalTrack = test.pcRemote._pc.getReceivers()[0].track; + }, + function PC_LOCAL_REMOVE_AUDIO_TRACK(test) { + // The new track's pipeline will start with a packet count of + // 0, but the remote side will keep its old pipeline and packet + // count. + test.pcLocal.disableRtpCountChecking = true; + return test.pcLocal.removeSender(0); + }, + function PC_LOCAL_ADD_AUDIO_TRACK(test) { + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + }, + ], + [ + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[1].receiver.track; + + const analyser = new AudioStreamAnalyser( + new AudioContext(), new MediaStream([track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200); + }, + function PC_REMOTE_CHECK_REMOVED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const analyser = new AudioStreamAnalyser( + new AudioContext(), new MediaStream([track])); + const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ); + return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50); + } + ] + ); + + test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER', + PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + return test.run() + .finally(() => tone.stop()); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html new file mode 100644 index 0000000000..b1be690e5b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove then add video track" + }); + + runNetworkTest(async function (options) { + // Use fake video here since the native fake device on linux doesn't + // change color as needed by checkVideoPlaying() below. + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(options); + const helper = new VideoStreamHelper(); + var originalTrack; + let haveMuteEvent = new Promise(() => {}); + let haveUnmuteEvent = new Promise(() => {}); + addRenegotiation(test.chain, + [ + function PC_REMOTE_FIND_RECEIVER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver"); + originalTrack = test.pcRemote._pc.getReceivers()[0].track; + }, + function PC_LOCAL_REMOVE_VIDEO_TRACK(test) { + // The new track's pipeline will start with a packet count of + // 0, but the remote side will keep its old pipeline and packet + // count. + test.pcLocal.disableRtpCountChecking = true; + return test.pcLocal.removeSender(0); + }, + function PC_LOCAL_ADD_VIDEO_TRACK(test) { + return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]); + }, + ], + [ + function PC_REMOTE_WAIT_FOR_UNMUTE() { + return haveUnmuteEvent; + }, + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[1].receiver.track; + + const vAdded = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + return helper.checkVideoPlaying(vAdded); + }, + function PC_REMOTE_WAIT_FOR_MUTE() { + return haveMuteEvent; + }, + function PC_REMOTE_CHECK_REMOVED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const vAdded = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000); + } + ] + ); + + // The first track should mute when the connection is closed. + test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONMUTE(test) { + haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute"); + } + ]); + + // Second negotiation should cause the second track to unmute. + test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONUNMUTE(test) { + haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute"); + } + ], false, 1); + + test.setMediaConstraints([{video: true}], [{video: true}]); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html new file mode 100644 index 0000000000..dcaf7943e2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove then add video track, no bundle" + }); + + runNetworkTest(async function (options) { + // Use fake video here since the native fake device on linux doesn't + // change color as needed by checkVideoPlaying() below. + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + options = options || { }; + options.bundle = false; + const test = new PeerConnectionTest(options); + const helper = new VideoStreamHelper(); + var originalTrack; + addRenegotiation(test.chain, + [ + function PC_REMOTE_FIND_RECEIVER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver"); + originalTrack = test.pcRemote._pc.getReceivers()[0].track; + }, + function PC_LOCAL_REMOVE_VIDEO_TRACK(test) { + // The new track's pipeline will start with a packet count of + // 0, but the remote side will keep its old pipeline and packet + // count. + test.pcLocal.disableRtpCountChecking = true; + return test.pcLocal.removeSender(0); + }, + function PC_LOCAL_ADD_VIDEO_TRACK(test) { + // Use fake:true here since the native fake device on linux doesn't + // change color as needed by checkVideoPlaying() below. + return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + }, + ], + [ + function PC_REMOTE_CHECK_ADDED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[1].receiver.track; + + const vAdded = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + return helper.checkVideoPlaying(vAdded); + }, + function PC_REMOTE_CHECK_REMOVED_TRACK(test) { + is(test.pcRemote._pc.getTransceivers().length, 2, + "pcRemote should have two transceivers"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const vAdded = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000); + }, + ] + ); + + test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER', + PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER); + + test.setMediaConstraints([{video: true}], [{video: true}]); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html new file mode 100644 index 0000000000..4c4e7905e1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: remove video track" + }); + + runNetworkTest(async (options) => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(options); + let receivedTrack, element; + addRenegotiation(test.chain, + [ + function PC_REMOTE_SETUP_HELPER(test) { + is(test.pcRemote._pc.getReceivers().length, 1, + "pcRemote should have one receiver before renegotiation"); + + receivedTrack = test.pcRemote._pc.getReceivers()[0].track; + is(receivedTrack.readyState, "live", + "The received track should be live"); + + element = createMediaElement("video", "pcRemoteReceivedVideo"); + element.srcObject = new MediaStream([receivedTrack]); + return haveEvent(element, "loadeddata"); + }, + function PC_LOCAL_REMOVE_VIDEO_TRACK(test) { + test.setOfferOptions({ offerToReceiveVideo: true }); + test.setMediaConstraints([], [{video: true}]); + return test.pcLocal.removeSender(0); + }, + ], + [ + function PC_REMOTE_CHECK_FLOW_STOPPED(test) { + is(test.pcRemote._pc.getTransceivers().length, 1, + "pcRemote should have one transceiver"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const vAdded = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + const helper = new VideoStreamHelper(); + return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000); + }, + ] + ); + + test.setMediaConstraints([{video: true}], [{video: true}]); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html new file mode 100644 index 0000000000..c8091d7a9e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1273652", + title: "Video receiver still renders after renegotiation", + visible: true + }); + + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + var v1, v2; + var delivered = new Promise(resolve => pc2.ontrack = e => { + // Test RTCTrackEvent here. + ok(e.streams.length, "has streams"); + ok(e.streams[0].getTrackById(e.track.id), "has track"); + ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver"); + if (e.streams[0].getTracks().length == 1) { + // Test RTCTrackEvent required args here. + mustThrowWith("RTCTrackEvent wo/required args", + "TypeError", () => new RTCTrackEvent("track", {})); + v2.srcObject = e.streams[0]; + resolve(); + } + }); + + runNetworkTest(async () => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]); + await pushPrefs(["media.webrtc.hw.h264.enabled", false]); + } + + v2 = createMediaElement('video', 'v2'); + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + + const emitter = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue, + CaptureStreamTestHelper.prototype.green, + 16, 16); + emitter.start(); + emitter.stream().getTracks().forEach(t => pc1.addTrack(t, emitter.stream())); + let h = emitter.helper(); + + let offer = await pc1.createOffer({}); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + // check that createAnswer accepts arg. + let answer = await pc2.createAnswer({}); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + + // re-negotiate to trigger the race condition in the jitter buffer + offer = await pc1.createOffer({}); // check that createOffer accepts arg. + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + answer = await pc2.createAnswer({}); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + await delivered; + + // now verify that actually something gets rendered into the remote video + // element. + await h.pixelMustBecome(v2, h.blue, { + threshold: 128, + infoString: "pcRemote's video should become blue", + }); + // This will verify that new changes to the canvas propagate through + // the peerconnection + emitter.colors(h.red, h.green) + await h.pixelMustBecome(v2, h.red, { + threshold: 128, + infoString: "pcRemote's video should become red", + }); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html new file mode 100644 index 0000000000..83ba76cab0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1763832", + title: "Renegotiation (audio): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate" + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.audio_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + const transceiverSend = offerer.addTransceiver('audio', {direction: 'recvonly'}); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + await offerer.setLocalDescription(); + await answerer.setRemoteDescription(offerer.localDescription); + await answerer.setLocalDescription(); + await offerer.setRemoteDescription(answerer.localDescription); + + // add audio with replaceTrack, set send bit, and renegotiate + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + const [track] = stream.getAudioTracks(); + transceiverSend.sender.replaceTrack(track); + transceiverSend.direction = "sendrecv"; + const remoteStreamAvailable = new Promise(r => { + answerer.ontrack = ({track}) => r(new MediaStream([track])); + }); + + await offerer.setLocalDescription(); + await answerer.setRemoteDescription(offerer.localDescription); + await answerer.setLocalDescription(); + await offerer.setRemoteDescription(answerer.localDescription); + + const remoteStream = await remoteStreamAvailable; + const h = new AudioStreamFlowingHelper(); + await h.checkAudioFlowing(remoteStream); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html new file mode 100644 index 0000000000..d7bd6d8a37 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="simulcast.js"></script></head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1763832", + title: "Renegotiation (video): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate" + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + const transceiverSend = offerer.addTransceiver('video', {direction: 'recvonly'}); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + await offerer.setLocalDescription(); + await answerer.setRemoteDescription(offerer.localDescription); + await answerer.setLocalDescription(); + await offerer.setRemoteDescription(answerer.localDescription); + + // add video with replaceTrack, set send bit, and renegotiate + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getVideoTracks(); + transceiverSend.sender.replaceTrack(track); + transceiverSend.direction = "sendrecv"; + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + await offerer.setLocalDescription(); + await answerer.setRemoteDescription(offerer.localDescription); + await answerer.setLocalDescription(); + await offerer.setRemoteDescription(answerer.localDescription); + + const elems = await Promise.all(metadataToBeLoaded); + is(elems.length, 1, "Should have one video element"); + + const helper = new VideoStreamHelper(); + await helper.checkVideoPlaying(elems[0]); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html new file mode 100644 index 0000000000..98240295fe --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html @@ -0,0 +1,186 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1032839", + title: "Replace video and audio (with WebAudio) tracks", + visible: true + }); + + function allLocalStreamsHaveSender(pc) { + return pc.getLocalStreams() + .every(s => s.getTracks() // Every local stream, + .some(t => pc.getSenders() // should have some track, + .some(sn => sn.track == t))) // that's being sent over |pc|. + } + + function allRemoteStreamsHaveReceiver(pc) { + return pc.getRemoteStreams() + .every(s => s.getTracks() // Every remote stream, + .some(t => pc.getReceivers() // should have some track, + .some(sn => sn.track == t))) // that's being received over |pc|. + } + + function replacetest(wrapper) { + var pc = wrapper._pc; + var oldSenderCount = pc.getSenders().length; + var sender = pc.getSenders().find(sn => sn.track.kind == "video"); + var oldTrack = sender.track; + 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 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 + return wrapper.senderReplaceTrack(sender, newTrack, newStream); + }) + .then(() => { + is(pc.getSenders().length, oldSenderCount, "same sender count"); + is(sender.track, newTrack, "sender.track has been replaced"); + ok(!pc.getSenders().map(sn => sn.track).some(t => t == oldTrack), + "old track not among senders"); + // Spec does not say we add this new track to any stream + ok(!pc.getLocalStreams().some(s => s.getTracks() + .some(t => t == sender.track)), + "track does not exist among pc's local streams"); + return sender.replaceTrack(audiotrack) + .then(() => ok(false, "replacing with different kind should fail"), + e => is(e.name, "TypeError", + "replacing with different kind should fail")); + }); + } + + runNetworkTest(function () { + test = new PeerConnectionTest(); + test.audioCtx = new AudioContext(); + test.setMediaConstraints([{video: true, audio: true}], [{video: true}]); + test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW"); + + // Test replaceTrack on pcRemote separately since it's video only. + test.chain.append([ + function PC_REMOTE_VIDEOONLY_REPLACE_VIDEOTRACK(test) { + return replacetest(test.pcRemote); + }, + function PC_LOCAL_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) { + return test.pcLocal.waitForMediaFlow(); + } + ]); + + // Replace video twice on pcLocal to make sure it still works + // (does audio twice too, but hey) + test.chain.append([ + function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_1(test) { + return replacetest(test.pcLocal); + }, + function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_1(test) { + return test.pcRemote.waitForMediaFlow(); + }, + function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_2(test) { + return replacetest(test.pcLocal); + }, + function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_2(test) { + return test.pcRemote.waitForMediaFlow(); + } + ]); + + test.chain.append([ + function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_WITHSAME(test) { + var pc = test.pcLocal._pc; + var sender = pc.getSenders().find(sn => sn.track.kind == "video"); + ok(sender, "should still have a sender of video"); + return sender.replaceTrack(sender.track) + .then(() => ok(true, "replacing with itself should succeed")); + }, + function PC_REMOTE_NEW_SAME_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) { + return test.pcRemote.waitForMediaFlow(); + } + ]); + + // Replace the gUM audio track on pcLocal with a WebAudio track. + test.chain.append([ + function PC_LOCAL_AUDIOVIDEO_REPLACE_AUDIOTRACK_WEBAUDIO(test) { + var pc = test.pcLocal._pc; + var sender = pc.getSenders().find(sn => sn.track.kind == "audio"); + ok(sender, "track has a sender"); + var oldSenderCount = pc.getSenders().length; + var oldTrack = sender.track; + + var sourceNode = test.audioCtx.createOscillator(); + sourceNode.type = 'sine'; + // We need a frequency not too close to the fake audio track (1kHz). + sourceNode.frequency.value = 2000; + sourceNode.start(); + + var destNode = test.audioCtx.createMediaStreamDestination(); + sourceNode.connect(destNode); + var newTrack = destNode.stream.getAudioTracks()[0]; + + return test.pcLocal.senderReplaceTrack( + sender, newTrack, destNode.stream) + .then(() => { + is(pc.getSenders().length, oldSenderCount, "same sender count"); + ok(!pc.getSenders().some(sn => sn.track == oldTrack), + "Replaced track should be removed from senders"); + // TODO: Should PC remove local streams when there are no senders + // associated with it? getLocalStreams() isn't in the spec anymore, + // so I guess it is pretty arbitrary? + is(sender.track, newTrack, "sender.track has been replaced"); + // Spec does not say we add this new track to any stream + ok(!pc.getLocalStreams().some(s => s.getTracks() + .some(t => t == sender.track)), + "track exists among pc's local streams"); + }); + } + ]); + test.chain.append([ + function PC_LOCAL_CHECK_WEBAUDIO_FLOW_PRESENT(test) { + return test.pcRemote.checkReceivingToneFrom(test.audioCtx, test.pcLocal); + } + ]); + test.chain.append([ + function PC_LOCAL_INVALID_ADD_VIDEOTRACKS(test) { + let videoTransceivers = test.pcLocal._pc.getTransceivers() + .filter(transceiver => { + return !transceiver.stopped && + transceiver.receiver.track.kind == "video" && + transceiver.sender.track; + }); + + ok(videoTransceivers.length, + "There is at least one non-stopped video transceiver with a track."); + + videoTransceivers.forEach(transceiver => { + var stream = test.pcLocal._pc.getLocalStreams()[0];; + var track = transceiver.sender.track; + try { + test.pcLocal._pc.addTrack(track, stream); + ok(false, "addTrack existing track should fail"); + } catch (e) { + is(e.name, "InvalidAccessError", + "addTrack existing track should fail"); + } + }); + } + ]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html new file mode 100644 index 0000000000..356517e79f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> + +<video id="video" width="160" height="120" autoplay></video> + +<script type="application/javascript"> + createHTML({ + bug: "1709481", + title: "replaceTrack (null -> camera) test", + visible: true + }); + + runNetworkTest(async () => { + // Make sure we use the fake video device, and not loopback + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(); + pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); + pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); + pc2.ontrack = ({track}) => video.srcObject = new MediaStream([track]); + pc1.addTransceiver("audio"); + const tc1 = pc1.addTransceiver("video"); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getVideoTracks(); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + await wait(100); + await tc1.sender.replaceTrack(track); + const h = new VideoStreamHelper(); + await h.checkVideoPlaying(video); + pc1.close(); + pc2.close(); + await SpecialPowers.popPrefEnv(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html new file mode 100644 index 0000000000..11b2762d96 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> +<script src="pc.js"></script> +<script src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script> +createHTML({ + bug: "1576771", + title: "Replace a disabled video track with an enabled one", + visible: true, +}); + +runNetworkTest(() => { + const helper = new CaptureStreamTestHelper2D(240, 160); + const emitter = new VideoFrameEmitter(helper.green, helper.green, 240, 160); + const test = new PeerConnectionTest(); + test.setMediaConstraints([{video: true}], []); + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [ + function PC_LOCAL_DISABLE_SENDTRACK(test) { + test.pcLocal._pc.getSenders()[0].track.enabled = false; + }, + function PC_REMOTE_WAIT_FOR_BLACK(test) { + return helper.pixelMustBecome( + test.pcRemote.remoteMediaElements[0], helper.black, { + threshold: 128, + infoString: "Remote disabled track becomes black", + cancel: wait(10000).then( + () => new Error("Timeout waiting for black"))}); + }, + function PC_LOCAL_REPLACETRACK_WITH_ENABLED_TRACK(test) { + emitter.start(); + test.pcLocal._pc.getSenders()[0].replaceTrack( + emitter.stream().getTracks()[0]); + }, + ]); + test.chain.append([ + function PC_REMOTE_WAIT_FOR_GREEN(test) { + return helper.pixelMustBecome( + test.pcRemote.remoteMediaElements[0], helper.green, { + threshold: 128, + infoString: "Remote disabled track becomes green", + cancel: wait(10000).then( + () => new Error("Timeout waiting for green"))}); + }, + function CLEANUP(test) { + emitter.stop(); + for (const track of emitter.stream().getTracks()) { + track.stop(); + } + }, + ]); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html new file mode 100644 index 0000000000..d24fcc4130 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> + +<script type="application/javascript"> + createHTML({ + bug: "1709481", + title: "replaceTrack (null -> microphone) test", + visible: true + }); + + runNetworkTest(async () => { + // Make sure we use the fake audio device, and not loopback + await pushPrefs( + ['media.audio_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(); + pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); + pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); + let remoteStream; + pc2.ontrack = ({track}) => remoteStream = new MediaStream([track]); + const tc1 = pc1.addTransceiver("audio"); + const stream = await navigator.mediaDevices.getUserMedia({audio: true}); + const [track] = stream.getAudioTracks(); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + await wait(100); + await tc1.sender.replaceTrack(track); + const h = new AudioStreamFlowingHelper(); + await h.checkAudioFlowing(remoteStream); + pc1.close(); + pc2.close(); + await SpecialPowers.popPrefEnv(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html new file mode 100644 index 0000000000..070cb42fcb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1017888", + title: "Renegotiation: replaceTrack followed by adding a second video stream" + }); + + runNetworkTest(async (options) => { + await pushPrefs(['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video:true}], [{video:true}]); + const helper = new VideoStreamHelper(); + const emitter1 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.red, + CaptureStreamTestHelper.prototype.green); + const emitter2 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue, + CaptureStreamTestHelper.prototype.grey); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_ADDTRACK(test) { + test.pcLocal.attachLocalStream(emitter1.stream()); + emitter1.start(); + }, + ]); + addRenegotiation(test.chain, + [ + function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) { + emitter1.stop(); + emitter2.start(); + const newstream = emitter2.stream(); + const newtrack = newstream.getVideoTracks()[0]; + var sender = test.pcLocal._pc.getSenders()[0]; + return test.pcLocal.senderReplaceTrack(sender, newtrack, newstream) + .then(() => { + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}]); + }); + }, + ], + [ + function PC_REMOTE_CHECK_ORIGINAL_TRACK_NOT_ENDED(test) { + is(test.pcRemote._pc.getTransceivers().length, 1, + "pcRemote should have one transceiver"); + const track = test.pcRemote._pc.getTransceivers()[0].receiver.track; + + const vremote = test.pcRemote.remoteMediaElements.find( + elem => elem.id.includes(track.id)); + if (!vremote) { + return Promise.reject(new Error("Couldn't find video element")); + } + ok(!vremote.ended, "Original track should not have ended after renegotiation (replaceTrack is not signalled!)"); + return helper.checkVideoPlaying(vremote); + } + ] + ); + + await test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html new file mode 100644 index 0000000000..d94bb084b7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ] + ); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html new file mode 100644 index 0000000000..b71001b0db --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1413709", + title: "Renegotiation: bad answer ICE credentials" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}], + []); + return test.pcLocal.getAllUserMedia([{audio: true}]); + }, + ] + ); + + // If the offerer hasn't indicated ICE restart, then an answer + // arriving during renegotiation that has modified ICE credentials + // should cause an error + test.chain.replaceAfter("PC_LOCAL_GET_ANSWER", + [ + function PC_LOCAL_REWRITE_REMOTE_SDP_ICE_CREDS(test) { + test._remote_answer.sdp = + test._remote_answer.sdp.replace(/a=ice-pwd:.*\r\n/g, + "a=ice-pwd:bad-pwd\r\n") + .replace(/a=ice-ufrag:.*\r\n/g, + "a=ice-ufrag:bad-ufrag\r\n"); + }, + + function PC_LOCAL_EXPECT_SET_REMOTE_DESCRIPTION_FAIL(test) { + return test.setRemoteDescription(test.pcLocal, + test._remote_answer, + STABLE) + .then(() => ok(false, "setRemoteDescription must fail"), + e => is(e.name, "InvalidAccessError", + "setRemoteDescription must fail and did")); + } + ], 1 // replace after the second PC_LOCAL_GET_ANSWER + ); + + test.setMediaConstraints([{audio: true}], []); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html new file mode 100644 index 0000000000..6bbf9440fc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, local and remote rollback" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + async function PC_LOCAL_SETUP_ICE_HANDLER(test) { + await test.pcLocal.endOfTrickleIce; + test.pcLocal.setupIceCandidateHandler(test); + }, + + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + } + ] + ); + + test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER', + [ + function PC_LOCAL_EXPECT_ICE_CONNECTED(test) { + test.pcLocal.iceCheckingIceRollbackExpected = true; + }, + + function PC_REMOTE_ROLLBACK(test) { + return test.setRemoteDescription(test.pcRemote, { type: "rollback" }, + STABLE); + }, + + async function PC_LOCAL_ROLLBACK(test) { + await test.pcLocal.endOfTrickleIce; + // We haven't negotiated the new stream yet. + test.pcLocal.expectNegotiationNeeded(); + return test.setLocalDescription( + test.pcLocal, + new RTCSessionDescription({ type: "rollback", sdp: ""}), + STABLE); + }, + + // Rolling back should shut down gathering for the offerer, + // but because the answerer never set a local description, no ICE + // gathering has happened yet, so there's no changes to ICE gathering + // state + function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleIce; + }, + + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ], + 1 // Replaces after second PC_REMOTE_CREATE_ANSWER + ); + test.chain.append(commandsPeerConnectionOfferAnswer); + + // for now, only use one stream, because rollback doesn't seem to + // like multiple streams. See bug 1259465. + test.setMediaConstraints([{audio: true}], + [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html new file mode 100644 index 0000000000..37b0fc68fc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, local and remote rollback, without a subsequent ICE restart" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + function PC_LOCAL_SETUP_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test); + }, + function PC_REMOTE_SETUP_ICE_HANDLER(test) { + test.pcRemote.setupIceCandidateHandler(test); + }, + + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + } + ] + ); + + test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER', + [ + function PC_REMOTE_ROLLBACK(test) { + return test.setRemoteDescription(test.pcRemote, { type: "rollback" }, + STABLE); + }, + + async function PC_LOCAL_ROLLBACK(test) { + await test.pcLocal.endOfTrickleIce; + // We haven't negotiated the new stream yet. + test.pcLocal.expectNegotiationNeeded(); + return test.setLocalDescription( + test.pcLocal, + new RTCSessionDescription({ type: "rollback", sdp: ""}), + STABLE); + }, + + // Rolling back should shut down gathering for the offerer, + // but because the answerer never set a local description, no ICE + // gathering has happened yet, so there's no changes to ICE gathering + // state + function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleIce; + }, + + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: false }); + } + ], + 1 // Replaces after second PC_REMOTE_CREATE_ANSWER + ); + test.chain.append(commandsPeerConnectionOfferAnswer); + + // for now, only use one stream, because rollback doesn't seem to + // like multiple streams. See bug 1259465. + test.setMediaConstraints([{audio: true}], + [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html new file mode 100644 index 0000000000..f5f9a1f220 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, local rollback" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + // causes an ice restart and then rolls it back + // (does not result in sending an offer) + function PC_LOCAL_SETUP_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test, () => {}); + }, + function PC_LOCAL_CREATE_AND_SET_OFFER(test) { + return test.createOffer(test.pcLocal).then(offer => { + return test.setLocalDescription(test.pcLocal, + offer, + HAVE_LOCAL_OFFER); + }); + }, + function PC_LOCAL_EXPECT_ICE_CONNECTED(test) { + test.pcLocal.iceCheckingIceRollbackExpected = true; + }, + function PC_LOCAL_WAIT_FOR_GATHERING(test) { + return new Promise(r => { + test.pcLocal._pc.addEventListener("icegatheringstatechange", () => { + if (test.pcLocal._pc.iceGatheringState == "gathering") { + r(); + } + }); + }); + }, + function PC_LOCAL_ROLLBACK(test) { + return test.setLocalDescription(test.pcLocal, + { type: "rollback", sdp: ""}, + STABLE); + }, + // Rolling back should shut down gathering + function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleIce; + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ] + ); + + // for now, only use one stream, because rollback doesn't seem to + // like multiple streams. See bug 1259465. + test.setMediaConstraints([{audio: true}], + [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html new file mode 100644 index 0000000000..8e27864aae --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, local rollback, then renegotiation without ICE restart" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + // causes an ice restart and then rolls it back + // (does not result in sending an offer) + function PC_LOCAL_SETUP_ICE_HANDLER(test) { + test.pcLocal.setupIceCandidateHandler(test, () => {}); + }, + function PC_LOCAL_CREATE_AND_SET_OFFER(test) { + return test.createOffer(test.pcLocal).then(offer => { + return test.setLocalDescription(test.pcLocal, + offer, + HAVE_LOCAL_OFFER); + }); + }, + function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) { + return test.pcLocal.endOfTrickleIce; + }, + function PC_LOCAL_ROLLBACK(test) { + return test.setLocalDescription(test.pcLocal, + { type: "rollback", sdp: ""}, + STABLE); + }, + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: false }); + } + ] + ); + + // for now, only use one stream, because rollback doesn't seem to + // like multiple streams. See bug 1259465. + test.setMediaConstraints([{audio: true}], + [{audio: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html new file mode 100644 index 0000000000..134fa97cc0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, no bundle" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ] + ); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html new file mode 100644 index 0000000000..06a3a3c980 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, no bundle and disabled RTCP-Mux" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + options.rtcpmux = false; + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ] + ); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html new file mode 100644 index 0000000000..5d4780211a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "906986", + title: "Renegotiation: restart ice, with disabled RTCP-Mux" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.rtcpmux = false; + test = new PeerConnectionTest(options); + + addRenegotiation(test.chain, + [ + // causes a full, normal ice restart + function PC_LOCAL_SET_OFFER_OPTION(test) { + test.setOfferOptions({ iceRestart: true }); + }, + function PC_LOCAL_EXPECT_ICE_CHECKING(test) { + test.pcLocal.expectIceChecking(); + }, + function PC_REMOTE_EXPECT_ICE_CHECKING(test) { + test.pcRemote.expectIceChecking(); + } + ] + ); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + return test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html new file mode 100644 index 0000000000..ff9fb1fc22 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "Bug 1404250", + title: "Extremely bitrate restricted video-only peer connection" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + test.chain.insertAfter('PC_REMOTE_GET_OFFER', [ + function PC_REMOTE_ADD_TIAS(test) { + test._local_offer.sdp = sdputils.addTiasBps( + test._local_offer.sdp, 25000); + info("Offer with TIAS: " + JSON.stringify(test._local_offer)); + } + ]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html new file mode 100644 index 0000000000..85b831e9a8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1359854", + title: "500Kb restricted video-only peer connection" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], [{video: true}]); + test.chain.insertAfter('PC_REMOTE_GET_OFFER', [ + function PC_REMOTE_ADD_TIAS(test) { + test._local_offer.sdp = sdputils.addTiasBps( + test._local_offer.sdp, 250000); + info("Offer with TIAS: " + JSON.stringify(test._local_offer)); + } + ]); + // TODO it would be nice to verify the used bandwidth + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html new file mode 100644 index 0000000000..25270984ea --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1279153", + title: "rtcp-rsize", + visible: true + }); + + // 0) Use webrtc-sdp + // 1) ADD RTCP-RISZE to all video m-sections + // 2) Check for RTCP-RSIZE in ANSWER + // 3) Wait for media to flow + // 4) Wait for RTCP stats + + runNetworkTest(async function (options) { + const test = new PeerConnectionTest(options); + + let mSectionsAltered = 0; + + test.chain.insertAfter("PC_LOCAL_CREATE_OFFER", [ + function PC_LOCAL_ADD_RTCP_RSIZE(test) { + const lines = test.originalOffer.sdp.split("\r\n"); + info(`SDP before rtcp-rsize: ${lines.join('\n')}`); + // Insert an rtcp-rsize for each m section + const rsizeAdded = lines.flatMap(line => { + if (line.startsWith("m=video")) { + mSectionsAltered = mSectionsAltered + 1; + return [line, "a=rtcp-rsize"]; + } + return [line]; + }); + test.originalOffer.sdp = rsizeAdded.join("\r\n"); + info(`SDP with rtcp-rsize: ${rsizeAdded.join("\n")}`); + is(mSectionsAltered, 1, "We only altered 1 msection") + }]); + + // Check that the rtcp-rsize makes into the answer + test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", [ + function PC_LOCAL_CHECK_RTCP_RSIZE(test) { + const msections = sdputils.getMSections(test.pcLocal._pc.currentRemoteDescription.sdp); + var alteredMSectionsFound = 0; + for (msection of msections) { + if (msection.startsWith("m=video")) { + ok(msection.includes("\r\na=rtcp-rsize\r\n"), "video m-section includes RTCP-RSIZE"); + alteredMSectionsFound = alteredMSectionsFound + 1; + } else { + ok(!msection.includes("\r\na=rtcp-rsize\r\n"), "audio m-section does not include RTCP-RSIZE"); + } + } + is(alteredMSectionsFound, mSectionsAltered, "correct number of msections found"); + } + ]); + + // Make sure that we are still getting RTCP stats + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + async function PC_LOCAL_AND_REMOTE_CHECK_FOR_RTCP_STATS(test) { + await Promise.all([ + waitForSyncedRtcp(test.pcLocal._pc), + waitForSyncedRtcp(test.pcRemote._pc), + ]); + // The work is done by waitForSyncedRtcp which will throw if + // RTCP stats are not received. + info("RTCP stats received!"); + }, + ); + test.setMediaConstraints([{audio: true}, {video: true}], []); + await test.run(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html new file mode 100644 index 0000000000..4be6873fa6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1244913", + title: "Scale resolution down on a PeerConnection", + visible: true + }); + + async function checkForH264Support() { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/); + } + + let resolutionAlignment = 1; + + var mustRejectWith = (msg, reason, f) => + f().then(() => ok(false, msg), + e => is(e.name, reason, msg)); + + async function testScale(codec) { + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + info("testing scaling with " + codec); + + let stream = await navigator.mediaDevices.getUserMedia({ video: true }); + + var v1 = createMediaElement('video', 'v1'); + var v2 = createMediaElement('video', 'v2'); + + var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e)); + var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve); + + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + + v1.srcObject = stream; + var sender = pc1.addTrack(stream.getVideoTracks()[0], stream); + let parameters = sender.getParameters(); + is(parameters.encodings.length, 1, "Default number of encodings should be 1"); + parameters.encodings[0].scaleResolutionDownBy = 0.5; + + await mustRejectWith( + "Invalid scaleResolutionDownBy must reject", "RangeError", + () => sender.setParameters(parameters) + ); + + parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 2; + parameters.encodings[0].maxBitrate = 60000; + + await sender.setParameters(parameters); + + parameters = sender.getParameters(); + is(parameters.encodings[0].scaleResolutionDownBy, 2, "Should be able to set scaleResolutionDownBy"); + is(parameters.encodings[0].maxBitrate, 60000, "Should be able to set maxBitrate"); + + let offer = await pc1.createOffer(); + if (codec == "VP8") { + offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126); + } + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + + let answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + let trackevent = await ontrackfired; + + v2.srcObject = trackevent.streams[0]; + + await v2loadedmetadata; + + await waitUntil(() => v2.currentTime > 0); + ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"); + + ok(v1.videoWidth > 0, "source width is positive"); + ok(v1.videoHeight > 0, "source height is positive"); + const expectedWidth = + v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment); + const expectedHeight = + v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment); + is(v2.videoWidth, expectedWidth, + "sink is half the width of the source"); + is(v2.videoHeight, expectedHeight, + "sink is half the height of the source"); + stream.getTracks().forEach(track => track.stop()); + v1.srcObject = v2.srcObject = null; + pc1.close() + pc2.close() + } + + runNetworkTest(async () => { + await matchPlatformH264CodecPrefs(); + const hasH264 = await checkForH264Support(); + if (hasH264 && navigator.userAgent.includes("Android")) { + // Android only has a hw encoder for h264 + resolutionAlignment = 16; + } + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + await testScale("VP8"); + if (hasH264) { + await testScale("H264"); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html new file mode 100644 index 0000000000..85a989ba32 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1244913", + title: "Scale resolution down on a PeerConnection", + visible: true + }); + + async function checkForH264Support() { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/); + } + + let resolutionAlignment = 1; + + var mustRejectWith = (msg, reason, f) => + f().then(() => ok(false, msg), + e => is(e.name, reason, msg)); + + async function testScale(codec) { + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + info("testing scaling with " + codec); + + let stream = await navigator.mediaDevices.getUserMedia({ video: true }); + + var v1 = createMediaElement('video', 'v1'); + var v2 = createMediaElement('video', 'v2'); + + var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e)); + var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve); + + is(v2.currentTime, 0, "v2.currentTime is zero at outset"); + + v1.srcObject = stream; + var sender = pc1.addTrack(stream.getVideoTracks()[0], stream); + + const otherErrorStart = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue(); + const noTransactionIdWarningStart = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(); + + await mustRejectWith( + "Invalid scaleResolutionDownBy must reject", "RangeError", + () => sender.setParameters( + { encodings:[{ scaleResolutionDownBy: 0.5 } ] }) + ); + + const otherErrorEnd = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue(); + const noTransactionIdWarningEnd = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(); + + // Make sure Glean is recording these statistics + is(otherErrorEnd.denominator, otherErrorStart.denominator, "No new RTCRtpSenders were created during this time"); + is(otherErrorEnd.numerator, otherErrorStart.numerator + 1, "RTCRtpSender.setParameters reported a failure via Glean"); + is(noTransactionIdWarningEnd.denominator, noTransactionIdWarningStart.denominator, "No new RTCRtpSenders were created during this time"); + is(noTransactionIdWarningEnd.numerator, noTransactionIdWarningStart.numerator + 1, "Glean should have recorded a warning due to missing transactionId"); + + await sender.setParameters({ encodings: [{ maxBitrate: 60000, + scaleResolutionDownBy: 2 }] }); + + let offer = await pc1.createOffer(); + if (codec == "VP8") { + offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126); + } + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + + let answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + let trackevent = await ontrackfired; + + v2.srcObject = trackevent.streams[0]; + + await v2loadedmetadata; + + await waitUntil(() => v2.currentTime > 0); + ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"); + + ok(v1.videoWidth > 0, "source width is positive"); + ok(v1.videoHeight > 0, "source height is positive"); + const expectedWidth = + v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment); + const expectedHeight = + v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment); + is(v2.videoWidth, expectedWidth, + "sink is half the width of the source"); + is(v2.videoHeight, expectedHeight, + "sink is half the height of the source"); + stream.getTracks().forEach(track => track.stop()); + v1.srcObject = v2.srcObject = null; + pc1.close() + pc2.close() + } + + runNetworkTest(async () => { + await matchPlatformH264CodecPrefs(); + const hasH264 = await checkForH264Support(); + if (hasH264 && navigator.userAgent.includes("Android")) { + // Android only has a hw encoder for h264 + resolutionAlignment = 16; + } + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + await testScale("VP8"); + if (hasH264) { + await testScale("H264"); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html new file mode 100644 index 0000000000..72749e8c50 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1355220", + title: "RTCRtpSender.getStats() and RTCRtpReceiver.getStats()", + visible: true + }); + + const checkStats = (sndReport, rcvReport, kind) => { + ok(sndReport instanceof window.RTCStatsReport, "sender stats are a RTCStatsReport"); + ok(rcvReport instanceof window.RTCStatsReport, "receiver stats are a RTCStatsReport"); + // Returns SSRCs and performs some checks + let getSsrcs = (report, kind) => { + return [...report.values()] + .filter(stat => stat.type.endsWith("bound-rtp")).map(stat =>{ + isnot(parseInt(stat.id, 16), NaN, + `id ${stat.id} is an opaque (hex) number`); + is(stat.kind, kind, "kind of " + stat.id + + " is expected type " + kind); + return stat.ssrc; + }).sort().join("|"); + }; + let sndSsrcs = getSsrcs(sndReport, kind); + let rcvSsrcs = getSsrcs(rcvReport, kind); + ok(sndSsrcs, "sender SSRCs is not empty"); + ok(rcvSsrcs, "receiver SSRCs is not empty"); + is(sndSsrcs, rcvSsrcs, "sender SSRCs match receiver SSRCs"); + }; + + // This MUST be run after PC_*_WAIT_FOR_MEDIA_FLOW to ensure that we have RTP + // before checking for RTCP. + // It will throw UnsyncedRtcpError if it times out waiting for sync. + + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + async function PC_LOCAL_AND_REMOTE_CHECK_SENDER_RECEIVER_STATS(test) { + await Promise.all([ + waitForSyncedRtcp(test.pcLocal._pc), + waitForSyncedRtcp(test.pcRemote._pc), + ]); + let senders = test.pcLocal.getSenders(); + let receivers = test.pcRemote.getReceivers(); + is(senders.length, 2, "Have exactly two senders."); + is(receivers.length, 2, "Have exactly two receivers."); + for(let kind of ["audio", "video"]) { + let senderStats = + await senders.find(s => s.track.kind == kind).getStats(); + is(senders.filter(s => s.track.kind == kind).length, 1, + "Exactly 1 sender of kind " + kind); + let receiverStats = + await receivers.find(r => r.track.kind == kind).getStats(); + is(receivers.filter(r => r.track.kind == kind).length, 1, + "Exactly 1 receiver of kind " + kind); + + checkStats(senderStats, receiverStats, kind); + } + } + ); + test.setMediaConstraints([{audio: true}, {video: true}], []); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html new file mode 100644 index 0000000000..07cdd7d6bd --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setLocalDescription (answer) in 'have-local-offer'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION"); + + test.chain.append([ + function PC_LOCAL_SET_LOCAL_ANSWER(test) { + test.pcLocal._latest_offer.type = "answer"; + return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer) + .then(err => { + is(err.name, "InvalidModificationError", "Error is InvalidModificationError"); + }); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html new file mode 100644 index 0000000000..e57c0640f4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setLocalDescription (answer) in 'stable'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_LOCAL_CREATE_OFFER"); + + test.chain.append([ + function PC_LOCAL_SET_LOCAL_ANSWER(test) { + test.pcLocal._latest_offer.type = "answer"; + return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer) + .then(err => { + is(err.name, "InvalidModificationError", "Error is InvalidModificationError"); + }); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html new file mode 100644 index 0000000000..bd98a83635 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setLocalDescription (offer) in 'have-remote-offer'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION"); + + test.chain.append([ + async function PC_REMOTE_SET_LOCAL_OFFER(test) { + const err = await test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._latest_offer); + is(err.name, "InvalidModificationError", "Error is InvalidModificationError"); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html new file mode 100644 index 0000000000..5df97e39f5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html @@ -0,0 +1,470 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1230184", + title: "Set parameters on sender", + visible: true +}); + +const simulcastOffer = `v=0 +o=- 3840232462471583827 0 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 +a=msid-semantic: WMS +m=video 9 UDP/TLS/RTP/SAVPF 96 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:Li6+ +a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT +a=ice-options:trickle +a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3 +a=setup:actpass +a=mid:0 +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=recvonly +a=rtcp-mux +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rid:foo recv +a=rid:bar recv +a=simulcast:recv foo;bar +`; + +function buildMaximumSendEncodings() { + const sendEncodings = []; + while (true) { + // isDeeply does not see identical string primitives and String objects + // as the same, so we make this a string primitive. + sendEncodings.push({rid: `${sendEncodings.length}`}); + const pc = new RTCPeerConnection(); + const {sender} = pc.addTransceiver('video', {sendEncodings}); + const {encodings} = sender.getParameters(); + if (encodings.length < sendEncodings.length) { + sendEncodings.pop(); + return sendEncodings; + } + } +} + +async function queueAWebrtcTask() { + const pc = new RTCPeerConnection(); + pc.addTransceiver('audio'); + await new Promise(r => pc.onnegotiationneeded = r); + pc.close(); +} + +// setParameters is mostly tested in wpt, but we test a few +// implementation-specific things here. Other mochitests check whether the +// set parameters actually have the desired effect on the media streams. +const tests = [ + + // wpt currently does not assume support for 3 encodings, which limits the + // effectiveness of its powers-of-2 test (since it can test only for 1 and 2) + async function checkScaleResolutionDownByAutoFillPowersOf2() { + const pc = new RTCPeerConnection(); + const {sender} = pc.addTransceiver('video', { + sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}] + }); + const {encodings} = sender.getParameters(); + const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy); + isDeeply(scaleValues, [4, 2, 1]); + }, + + // wpt currently does not assume support for 3 encodings, which limits the + // effectiveness of its fill-with-1 test + async function checkScaleResolutionDownByAutoFillWith1() { + const pc = new RTCPeerConnection(); + const {sender} = pc.addTransceiver('video', { + sendEncodings: [ + {rid: "0"},{rid: "1", scaleResolutionDownBy: 3},{rid: "2"} + ] + }); + const {encodings} = sender.getParameters(); + const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy); + isDeeply(scaleValues, [1, 3, 1]); + }, + + async function checkVideoEncodingLimit() { + const pc = new RTCPeerConnection(); + const maxSendEncodings = buildMaximumSendEncodings(); + const sendEncodings = maxSendEncodings.concat({rid: "a"}); + const {sender} = pc.addTransceiver('video', {sendEncodings}); + const {encodings} = sender.getParameters(); + + const rids = encodings.map(({rid}) => rid); + const expectedRids = maxSendEncodings.map(({rid}) => rid); + isDeeply(rids, expectedRids); + + const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy); + const expectedScaleValues = []; + let scale = 1; + while (expectedScaleValues.length < maxSendEncodings.length) { + expectedScaleValues.push(scale); + scale *= 2; + } + isDeeply(scaleValues, expectedScaleValues.reverse()); + }, + + async function checkScaleDownByInTrimmedEncoding() { + const pc = new RTCPeerConnection(); + const maxSendEncodings = buildMaximumSendEncodings(); + const sendEncodings = maxSendEncodings.concat({rid: "a", scaleResolutionDownBy: 3}); + const {sender} = pc.addTransceiver('video', {sendEncodings}); + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + const expectedRids = maxSendEncodings.map(({rid}) => rid); + isDeeply(rids, expectedRids); + const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy); + const expectedScaleValues = maxSendEncodings.map(() => 1); + isDeeply(scaleValues, expectedScaleValues); + }, + + async function checkLibwebrtcRidLengthLimit() { + const pc = new RTCPeerConnection(); + try { + pc.addTransceiver('video', { + sendEncodings: [{rid: "wibblywobblyjeremybearimy"}]} + ); + ok(false, "Rid should be too long for libwebrtc!"); + } catch (e) { + is(e.name, "TypeError", + "Rid that is too long for libwebrtc should result in a TypeError"); + } + }, + + async function checkErrorsInTrimmedEncodings() { + const pc = new RTCPeerConnection(); + const maxSendEncodings = buildMaximumSendEncodings(); + try { + const sendEncodings = maxSendEncodings.concat({rid: "foo-bar"}); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw due to invalid rid characters"); + } catch (e) { + is(e.name, "TypeError") + } + try { + const sendEncodings = maxSendEncodings.concat({rid: "wibblywobblyjeremybearimy"}); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw because rid too long"); + } catch (e) { + is(e.name, "TypeError") + } + try { + const sendEncodings = maxSendEncodings.concat({scaleResolutionDownBy: 2}); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw due to missing rid"); + } catch (e) { + is(e.name, "TypeError") + } + try { + const sendEncodings = maxSendEncodings.concat(maxSendEncodings[0]); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw due to duplicate rid"); + } catch (e) { + is(e.name, "TypeError") + } + try { + const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, scaleResolutionDownBy: 0}); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw due to invalid scaleResolutionDownBy"); + } catch (e) { + is(e.name, "RangeError") + } + try { + const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, maxFramerate: -1}); + pc.addTransceiver('video', { sendEncodings }); + ok(false, "Should throw due to invalid maxFramerate"); + } catch (e) { + is(e.name, "RangeError") + } + }, + + async function checkCompatModeUnicastSetParametersAllowsSimulcastOffer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 3.0; + await sender.setParameters(parameters); + + await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer}); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo", "bar"]); + is(encodings[0].scaleResolutionDownBy, 2.0); + is(encodings[1].scaleResolutionDownBy, 1.0); + }, + + async function checkCompatModeUnicastSetParametersInterruptAllowsSimulcastOffer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 3.0; + + const offerDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer}); + await sender.setParameters(parameters); + await offerDone; + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo", "bar"]); + is(encodings[0].scaleResolutionDownBy, 2.0); + is(encodings[1].scaleResolutionDownBy, 1.0); + }, + + async function checkCompatModeSimulcastSetParametersSetsSimulcastEnvelope() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].rid = "1"; + parameters.encodings.push({rid: "2"}); + await sender.setParameters(parameters); + + await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer}); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + // No overlap in rids -> unicast + isDeeply(rids, [undefined]); + }, + + async function checkCompatModeSimulcastSetParametersRacesLocalUnicastOffer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + // unicast offer + const offer = await pc1.createOffer(); + const aTask = queueAWebrtcTask(); + const sldPromise = pc1.setLocalDescription(offer); + + // Right now, we have aTask queued. The success task for sLD is not queued + // yet, because Firefox performs the initial steps on the microtask queue, + // which we have not allowed to run yet. Awaiting aTask will first clear + // the microtask queue, then run the task queue until aTask is finished. + // That _should_ result in the success task for sLD(offer) being queued. + await aTask; + + const parameters = sender.getParameters(); + parameters.encodings[0].rid = "foo"; + parameters.encodings.push({rid: "bar"}); + // simulcast setParameters; the task to update [[SendEncodings]] should be + // queued after the success task for sLD(offer) + await sender.setParameters(parameters); + await sldPromise; + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + // Compat mode lets this slide, but won't try to negotiate it since we've + // already applied a unicast local offer. + isDeeply(rids, ["foo", "bar"]); + + // Let negotiation finish, so we can generate a new offer + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + const reoffer = await pc1.createOffer(); + ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast"); + }, + + async function checkCompatModeSimulcastSetParametersRacesRemoteOffer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].rid = "foo"; + parameters.encodings.push({rid: "bar"}); + const p = sender.setParameters(parameters); + await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer}); + await p; + const answer = await pc1.createAnswer(); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo", "bar"]); + ok(answer.sdp.includes("a=simulcast:send foo;bar"), "answer should be simulcast"); + }, + + async function checkCompatModeSimulcastSetParametersRacesLocalAnswer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + // We do an initial negotiation, and while the local answer is pending, + // perform a setParameters on a not-yet-negotiated video sender. The intent + // here is to have the success task for sLD(answer) run while the + // setParameters is pending. + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const audioStream = await navigator.mediaDevices.getUserMedia({audio: true}); + // We use this later on, but set it up now so we don't inadvertently + // crank the event loop more than we intend below. + const videoStream = await navigator.mediaDevices.getUserMedia({video: true}); + pc2.addTrack(audioStream.getTracks()[0]); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + const answer = await pc1.createAnswer(); + const aTask = queueAWebrtcTask(); + const sldPromise = pc1.setLocalDescription(answer); + + // Right now, we have aTask queued. The success task for sLD is not queued + // yet, because Firefox performs the initial steps on the microtask queue, + // which we have not allowed to run yet. Awaiting aTask will first clear + // the microtask queue, then run the task queue until aTask is finished. + // That _should_ result in the success task for sLD(answer) being queued. + await aTask; + + // The success task for sLD(answer) should be queued now. Don't relinquish + // the event loop! + + // New sender that has nothing to do with the negotiation in progress. + const sender = pc1.addTrack(videoStream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].rid = "foo"; + parameters.encodings.push({rid: "bar"}); + + // We have not relinquished the event loop, so the sLD(answer) should still + // be queued. The task that updates [[SendEncodings]] (from setParameters) + // should be queued behind it. Let them both run. + await sender.setParameters(parameters); + await sldPromise; + + const offer = await pc1.createOffer(); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo", "bar"]); + ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast"); + }, + + async function checkCompatModeSimulcastSetParametersRacesRemoteAnswer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + // We do an initial negotiation, and while the remote answer is pending, + // perform a setParameters on a not-yet-negotiated video sender. The intent + // here is to have the success task for sRD(answer) run while the + // setParameters is pending. + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + const audioStream = await navigator.mediaDevices.getUserMedia({audio: true}); + // We use this later on, but set it up now so we don't inadvertently + // crank the event loop more than we intend below. + const videoStream = await navigator.mediaDevices.getUserMedia({video: true}); + pc1.addTrack(audioStream.getTracks()[0]); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + const aTask = queueAWebrtcTask(); + const srdPromise = pc1.setRemoteDescription(pc2.localDescription); + + // Right now, we have aTask queued. The success task for sRD is not queued + // yet, because Firefox performs the initial steps on the microtask queue, + // which we have not allowed to run yet. Awaiting aTask will first clear + // the microtask queue, then run the task queue until aTask is finished. + // That _should_ result in the success task for sRD(answer) being queued. + await aTask; + + // The success task for sRD(answer) should be queued now. Don't relinquish + // the event loop! + + const sender = pc1.addTrack(videoStream.getTracks()[0]); + const parameters = sender.getParameters(); + parameters.encodings[0].rid = "foo"; + parameters.encodings.push({rid: "bar"}); + + // We have not relinquished the event loop, so the sRD(answer) should still + // be queued. The task that updates [[SendEncodings]] (from setParameters) + // should be queued behind it. Let them both run. + await sender.setParameters(parameters); + await srdPromise; + + const offer = await pc1.createOffer(); + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + isDeeply(rids, ["foo", "bar"]); + ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast"); + }, + + async function checkCompatModeSimulcastRidlessSetParametersRacesLocalOffer() { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", true]); + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const sender = pc1.addTrack(stream.getTracks()[0]); + // unicast offer + const aTask = queueAWebrtcTask(); + const sldPromise = pc1.setLocalDescription(); + + // Right now, we have aTask queued. The success task for sLD is not queued + // yet, because Firefox performs the initial steps on the microtask queue, + // which we have not allowed to run yet. Awaiting aTask will first clear + // the microtask queue, then run the task queue until aTask is finished. + // That _should_ result in the success task for sLD(offer) being queued. + await aTask; + + // simulcast setParameters; the task to update [[SendEncodings]] should be + // queued after the success task for sLD(offer) + try { + await sender.setParameters({"encodings": [{}, {}]}); + ok(false, "setParameters with two ridless encodings should fail"); + } catch (e) { + ok(true, "setParameters with two ridless encodings should fail"); + } + await sldPromise; + + const {encodings} = sender.getParameters(); + const rids = encodings.map(({rid}) => rid); + // Compat mode lets this slide, but won't try to negotiate it since we've + // already applied a unicast local offer. + isDeeply(rids, [undefined]); + + // Let negotiation finish, so we can generate a new offer + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + const reoffer = await pc1.createOffer(); + ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast"); + }, + +]; + +runNetworkTest(async () => { + await pushPrefs( + ["media.peerconnection.allow_old_setParameters", false]); + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html new file mode 100644 index 0000000000..8047719775 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1611957", + title: "Live-updating maxFramerate" +}); + +let sender, receiver; + +async function checkMaxFrameRate(rate) { + const parameters = sender.getParameters(); + parameters.encodings[0].maxFramerate = rate; + await sender.setParameters(parameters); + await wait(2000); + const stats = Array.from((await receiver.getStats()).values()); + const inboundRtp = stats.find(stat => stat.type == "inbound-rtp"); + info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`); + const fps = inboundRtp.framesPerSecond; + ok(fps <= (rate * 1.1) + 1, + `fps is an appropriate value (${fps}) for rate (${rate})`); +} + +runNetworkTest(async function (options) { + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + test.chain.append([ + function CHECK_PRECONDITIONS() { + is(test.pcLocal._pc.getSenders().length, 1, + "Should have 1 local sender"); + is(test.pcRemote._pc.getReceivers().length, 1, + "Should have 1 remote receiver"); + + sender = test.pcLocal._pc.getSenders()[0]; + receiver = test.pcRemote._pc.getReceivers()[0]; + }, + function PC_LOCAL_SET_MAX_FRAMERATE_2() { + return checkMaxFrameRate(2); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_4() { + return checkMaxFrameRate(4); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_15() { + return checkMaxFrameRate(15); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_8() { + return checkMaxFrameRate(8); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_1() { + return checkMaxFrameRate(1); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html new file mode 100644 index 0000000000..9c68a31c0a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1611957", + title: "Live-updating maxFramerate" +}); + +let sender, receiver; + +async function checkMaxFrameRate(rate) { + sender.setParameters({ encodings: [{ maxFramerate: rate }] }); + await wait(2000); + const stats = Array.from((await receiver.getStats()).values()); + const inboundRtp = stats.find(stat => stat.type == "inbound-rtp"); + info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`); + const fps = inboundRtp.framesPerSecond; + ok(fps <= (rate * 1.1) + 1, `fps is an appropriate value (${fps}) for rate (${rate})`); +} + +runNetworkTest(async function (options) { + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + test.chain.append([ + function CHECK_PRECONDITIONS() { + is(test.pcLocal._pc.getSenders().length, 1, + "Should have 1 local sender"); + is(test.pcRemote._pc.getReceivers().length, 1, + "Should have 1 remote receiver"); + + sender = test.pcLocal._pc.getSenders()[0]; + receiver = test.pcRemote._pc.getReceivers()[0]; + }, + function PC_LOCAL_SET_MAX_FRAMERATE_2() { + return checkMaxFrameRate(2); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_4() { + return checkMaxFrameRate(4); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_15() { + return checkMaxFrameRate(15); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_8() { + return checkMaxFrameRate(8); + }, + function PC_LOCAL_SET_MAX_FRAMERATE_1() { + return checkMaxFrameRate(1); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html new file mode 100644 index 0000000000..2b55ec46e6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1230184", + title: "Set parameters on sender", + visible: true +}); + +function parameterstest(pc) { + ok(pc.getSenders().length, "have senders"); + var sender = pc.getSenders()[0]; + + var testParameters = (params, errorName, errorMsg) => { + info("Trying to set " + JSON.stringify(params)); + + var validateParameters = (a, b) => { + var validateEncoding = (a, b) => { + is(a.rid, b.rid, "same rid"); + is(a.maxBitrate, b.maxBitrate, "same maxBitrate"); + is(a.maxFramerate, b.maxFramerate, "same maxFramerate"); + is(a.scaleResolutionDownBy, b.scaleResolutionDownBy, + "same scaleResolutionDownBy"); + }; + is(a.encodings.length, (b.encodings || []).length, "same encodings"); + a.encodings.forEach((en, i) => validateEncoding(en, b.encodings[i])); + }; + + var before = JSON.stringify(sender.getParameters()); + isnot(JSON.stringify(params), before, "starting condition"); + + var p = sender.setParameters(params) + .then(() => { + isnot(JSON.stringify(sender.getParameters()), before, "parameters changed"); + validateParameters(sender.getParameters(), params); + is(null, errorName || null, "is success expected"); + }, e => { + is(e.name, errorName, "correct error name"); + is(e.message, errorMsg, "correct error message"); + }); + is(JSON.stringify(sender.getParameters()), before, "parameters not set yet"); + return p; + }; + + return [ + [{ encodings: [ { rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: "bar", maxBitrate: 10000, scaleResolutionDownBy: 4 }] + }], + [{ encodings: [{ maxBitrate: 10000, scaleResolutionDownBy: 4 }]}], + [{ encodings: [{ maxFramerate: 0.0, scaleResolutionDownBy: 1 }]}], + [{ encodings: [{ maxFramerate: 30.5, scaleResolutionDownBy: 1 }]}], + [{ encodings: [{ maxFramerate: -1, scaleResolutionDownBy: 1 }]}, "RangeError", "maxFramerate must be non-negative"], + [{ encodings: [{ maxBitrate: 40000 }, + { rid: "bar", maxBitrate: 10000 }] }, "TypeError", "Missing rid"], + [{ encodings: [{ rid: "foo", maxBitrate: 40000 }, + { rid: "bar", maxBitrate: 10000 }, + { rid: "bar", maxBitrate: 20000 }] }, "TypeError", "Duplicate rid"], + [{}, "TypeError", `RTCRtpSender.setParameters: Missing required 'encodings' member of RTCRtpSendParameters.`] + ].reduce((p, args) => p.then(() => testParameters.apply(this, args)), + Promise.resolve()); +} + +runNetworkTest(() => { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{video: true}], [{video: true}]); + test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW"); + + // Test sender parameters. + test.chain.append([ + function PC_LOCAL_SET_PARAMETERS(test) { + return parameterstest(test.pcLocal._pc); + } + ]); + + return test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html new file mode 100644 index 0000000000..d1275d6523 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1253499", + title: "Live-updating scaleResolutionDownBy" +}); + +async function checkForH264Support() { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/); +} + +let sender, localElem, remoteElem; +let originalWidth, originalHeight; +let resolutionAlignment = 1; + +async function checkScaleDownBy(scale) { + const parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = scale; + await sender.setParameters(parameters); + await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout"))); + + // Find the expected resolution. Internally we floor the exact scaling, then + // shrink each dimension to the alignment requested by the encoder. + let expectedWidth = + originalWidth / scale - (originalWidth / scale % resolutionAlignment); + let expectedHeight = + originalHeight / scale - (originalHeight / scale % resolutionAlignment); + + is(remoteElem.videoWidth, expectedWidth, + `Width should have scaled down by ${scale}`); + is(remoteElem.videoHeight, expectedHeight, + `Height should have scaled down by ${scale}`); +} + +runNetworkTest(async function (options) { + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + if (await checkForH264Support()) { + // Android only has h264 in hw, so now we know it will use vp8 in hw too. + resolutionAlignment = 16; + } + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + test.chain.append([ + function CHECK_PRECONDITIONS() { + is(test.pcLocal._pc.getSenders().length, 1, + "Should have 1 local sender"); + is(test.pcLocal.localMediaElements.length, 1, + "Should have 1 local sending media element"); + is(test.pcRemote.remoteMediaElements.length, 1, + "Should have 1 remote media element"); + + sender = test.pcLocal._pc.getSenders()[0]; + localElem = test.pcLocal.localMediaElements[0]; + remoteElem = test.pcRemote.remoteMediaElements[0]; + + remoteElem.addEventListener("resize", () => + info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`)); + + originalWidth = localElem.videoWidth; + originalHeight = localElem.videoHeight; + info(`Original width is ${originalWidth}`); + }, + function PC_LOCAL_SCALEDOWNBY_2() { + return checkScaleDownBy(2); + }, + function PC_LOCAL_SCALEDOWNBY_4() { + return checkScaleDownBy(4); + }, + function PC_LOCAL_SCALEDOWNBY_15() { + return checkScaleDownBy(15); + }, + function PC_LOCAL_SCALEDOWNBY_8() { + return checkScaleDownBy(8); + }, + function PC_LOCAL_SCALEDOWNBY_1() { + return checkScaleDownBy(1); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html new file mode 100644 index 0000000000..4d515bd5c1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1253499", + title: "Live-updating scaleResolutionDownBy" +}); + +async function checkForH264Support() { + const pc = new RTCPeerConnection(); + const offer = await pc.createOffer({offerToReceiveVideo: true}); + return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/); +} + +let sender, localElem, remoteElem; +let originalWidth, originalHeight; +let resolutionAlignment = 1; + +async function checkScaleDownBy(scale) { + sender.setParameters({ encodings: [{ scaleResolutionDownBy: scale }] }); + await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout"))); + + // Find the expected resolution. Internally we floor the exact scaling, then + // shrink each dimension to the alignment requested by the encoder. + let expectedWidth = + originalWidth / scale - (originalWidth / scale % resolutionAlignment); + let expectedHeight = + originalHeight / scale - (originalHeight / scale % resolutionAlignment); + + is(remoteElem.videoWidth, expectedWidth, + `Width should have scaled down by ${scale}`); + is(remoteElem.videoHeight, expectedHeight, + `Height should have scaled down by ${scale}`); +} + +runNetworkTest(async function (options) { + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + if (await checkForH264Support()) { + // Android only has h264 in hw, so now we know it will use vp8 in hw too. + resolutionAlignment = 16; + } + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + test.chain.append([ + function CHECK_PRECONDITIONS() { + is(test.pcLocal._pc.getSenders().length, 1, + "Should have 1 local sender"); + is(test.pcLocal.localMediaElements.length, 1, + "Should have 1 local sending media element"); + is(test.pcRemote.remoteMediaElements.length, 1, + "Should have 1 remote media element"); + + sender = test.pcLocal._pc.getSenders()[0]; + localElem = test.pcLocal.localMediaElements[0]; + remoteElem = test.pcRemote.remoteMediaElements[0]; + + remoteElem.addEventListener("resize", () => + info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`)); + + originalWidth = localElem.videoWidth; + originalHeight = localElem.videoHeight; + info(`Original width is ${originalWidth}`); + }, + function PC_LOCAL_SCALEDOWNBY_2() { + return checkScaleDownBy(2); + }, + function PC_LOCAL_SCALEDOWNBY_4() { + return checkScaleDownBy(4); + }, + function PC_LOCAL_SCALEDOWNBY_15() { + return checkScaleDownBy(15); + }, + function PC_LOCAL_SCALEDOWNBY_8() { + return checkScaleDownBy(8); + }, + function PC_LOCAL_SCALEDOWNBY_1() { + return checkScaleDownBy(1); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html new file mode 100644 index 0000000000..1912835160 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setRemoteDescription (answer) in 'have-remote-offer'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION"); + + test.chain.append([ + function PC_REMOTE_SET_REMOTE_ANSWER(test) { + test.pcLocal._latest_offer.type = "answer"; + test.pcRemote._pc.setRemoteDescription(test.pcLocal._latest_offer) + .then(generateErrorCallback('setRemoteDescription should fail'), + err => + is(err.name, "InvalidStateError", "Error is InvalidStateError")); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html new file mode 100644 index 0000000000..6208fdea3e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setRemoteDescription (answer) in 'stable'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_LOCAL_CREATE_OFFER"); + + test.chain.append([ + function PC_LOCAL_SET_REMOTE_ANSWER(test) { + test.pcLocal._latest_offer.type = "answer"; + test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer) + .then(generateErrorCallback('setRemoteDescription should fail'), + err => + is(err.name, "InvalidStateError", "Error is InvalidStateError")); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html new file mode 100644 index 0000000000..20236f442c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "784519", + title: "setRemoteDescription (offer) in 'have-local-offer'" + }); + +runNetworkTest(function () { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{audio: true}], [{audio: true}]); + test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION"); + + test.chain.append([ + async function PC_LOCAL_SET_REMOTE_OFFER(test) { + const p = test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer); + await new Promise(r => test.pcLocal.onsignalingstatechange = r); + is(test.pcLocal._pc.signalingState, 'stable', 'should fire stable'); + await new Promise(r => test.pcLocal.onsignalingstatechange = r); + is(test.pcLocal._pc.signalingState, 'have-remote-offer', + 'should fire have-remote-offer'); + await p; + ok(true, 'setRemoteDescription should succeed'); + } + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_sillyCodecPriorities.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_sillyCodecPriorities.html new file mode 100644 index 0000000000..c8354356b0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_sillyCodecPriorities.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> + +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> +</head> + +<body> + <pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1873801", + title: "Test that weird codec priorities don't trip us up", + visible: true + }); + + function makeCodecTopPriority(sdp, codec) { + const ptToMove = sdputils.findCodecId(sdp, codec); + return sdp.replace( + // m=video port type pts ptToMove more-pts? + new RegExp(`(m=video [^ ]+ [^ ]+)(.*)( ${ptToMove})( [^ ]+)?`, "g"), + '$1$3$2$4'); + } + + function isCodecFirst(sdp, codec) { + const pt = sdputils.findCodecId(sdp, codec); + return !!sdp.match(`m=video [^ ]+ [^ ]+ ${pt}\w`); + } + + async function checkTopPriorityOffer(codec, isPseudoCodec) { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + await pc1.setLocalDescription(); + let mungedOffer = pc1.localDescription; + mungedOffer.sdp = makeCodecTopPriority(mungedOffer.sdp, codec); + await pc2.setRemoteDescription(mungedOffer); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + is(isCodecFirst((await pc2.createOffer()).sdp, codec), !isPseudoCodec, + "Top-priority codecs should come first in reoffers, unless they are pseudo codecs (eg; ulpfec)"); + } + + async function checkTopPriorityAnswer(codec, isPseudoCodec) { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + const sender = pc1.addTrack(stream.getTracks()[0]); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + let mungedAnswer = pc2.localDescription; + mungedAnswer.sdp = makeCodecTopPriority(mungedAnswer.sdp, codec); + await pc1.setRemoteDescription(mungedAnswer); + is(isCodecFirst((await pc1.createOffer()).sdp, codec), !isPseudoCodec, + "Top-priority codecs should come first in reoffers, unless they are pseudo codecs (eg; ulpfec)"); + } + + const tests = [ + async function checkTopPriorityUlpfecInOffer() { + await checkTopPriorityOffer("ulpfec", true); + }, + + async function checkTopPriorityUlpfecInAnswer() { + await checkTopPriorityAnswer("ulpfec", true); + }, + + async function checkTopPriorityUlpfecInOffer() { + await checkTopPriorityOffer("red", true); + }, + + async function checkTopPriorityUlpfecInAnswer() { + await checkTopPriorityAnswer("red", true); + }, + + async function checkTopPriorityUlpfecInOffer() { + await checkTopPriorityOffer("rtx", true); + }, + + async function checkTopPriorityUlpfecInAnswer() { + await checkTopPriorityAnswer("rtx", true); + }, + + ]; + + runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + await test(); + info(`Done running test: ${test.name}`); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html new file mode 100644 index 0000000000..ba75c72022 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast answer", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + offerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // Two recv transceivers, one for each simulcast stream + offerer.addTransceiver('video', { direction: 'recvonly' }); + offerer.addTransceiver('video', { direction: 'recvonly' }); + + const offer = await offerer.createOffer(); + + const mungedOffer = midToRid(offer); + info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + const sender = answerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + let parameters = sender.getParameters(); + is(parameters.encodings.length, 2); + is(answerer.getSenders().length, 1); + emitter.start(); + + await offerer.setLocalDescription(offer); + + const rids = offerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + parameters = sender.getParameters(); + info(`parameters: ${JSON.stringify(parameters)}`); + const observedRids = parameters.encodings.map(({rid}) => rid); + isDeeply(observedRids, rids); + parameters.encodings[0].maxBitrate = 40000; + parameters.encodings[0].scaleResolutionDownBy = 1; + parameters.encodings[1].maxBitrate = 40000; + parameters.encodings[1].scaleResolutionDownBy = 2; + await sender.setParameters(parameters); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = ridToMid(answer); + info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[0].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[1].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + info("Stats ready"); + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html new file mode 100644 index 0000000000..00c6e4ad3a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + offerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // Two recv transceivers, one for each simulcast stream + offerer.addTransceiver('video', { direction: 'recvonly' }); + offerer.addTransceiver('video', { direction: 'recvonly' }); + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + answerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + emitter.start(); + + const offer = await offerer.createOffer(); + + const mungedOffer = midToRid(offer); + info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = offerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const sender = answerer.getSenders()[0]; + const parameters = sender.getParameters(); + parameters.encodings[0].maxBitrate = 40000; + parameters.encodings[0].scaleResolutionDownBy = 2; + parameters.encodings[1].maxBitrate = 40000; + await sender.setParameters(parameters); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = ridToMid(answer); + info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[1].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[0].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html new file mode 100644 index 0000000000..c2aafc4575 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.peerconnection.simulcast', true], + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + offerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // Two recv transceivers, one for each simulcast stream + offerer.addTransceiver('video', { direction: 'recvonly' }); + offerer.addTransceiver('video', { direction: 'recvonly' }); + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + answerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + emitter.start(); + + const offer = await offerer.createOffer(); + + const mungedOffer = midToRid(offer); + info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = offerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const sender = answerer.getSenders()[0]; + sender.setParameters({ + encodings: [ + { rid: rids[0], maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: rids[1], maxBitrate: 40000 } + ] + }); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = ridToMid(answer); + info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[1].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[0].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html new file mode 100644 index 0000000000..bc0b9f71cc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast answer", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.peerconnection.simulcast', true], + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + offerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // Two recv transceivers, one for each simulcast stream + offerer.addTransceiver('video', { direction: 'recvonly' }); + offerer.addTransceiver('video', { direction: 'recvonly' }); + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + answerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + emitter.start(); + + const offer = await offerer.createOffer(); + + const mungedOffer = midToRid(offer); + info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = offerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const sender = answerer.getSenders()[0]; + await sender.setParameters({ + encodings: [ + { rid: rids[0], maxBitrate: 40000 }, + { rid: rids[1], maxBitrate: 40000, scaleResolutionDownBy: 2 } + ] + }); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = ridToMid(answer); + info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[0].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[1].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html new file mode 100644 index 0000000000..c380b34f1a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html @@ -0,0 +1,183 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1432793", + title: "Simulcast with odd resolution", + visible: true + }); + + runNetworkTest(async () => { + const helper = new VideoStreamHelper(); + const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528); + + async function checkVideoElement(senderElement, receiverElement, encoding) { + info(`Waiting for receiver video element ${encoding.rid} to start playing`); + await helper.checkVideoPlaying(receiverElement); + const srcWidth = senderElement.videoWidth; + const srcHeight = senderElement.videoHeight; + info(`Source resolution is ${srcWidth}x${srcHeight}`); + + const scaleDownBy = encoding.scaleResolutionDownBy; + const expectedWidth = srcWidth / scaleDownBy; + const expectedHeight = srcHeight / scaleDownBy; + const margin = srcWidth * 0.1; + const width = receiverElement.videoWidth; + const height = receiverElement.videoHeight; + const rid = encoding.rid; + ok(width >= expectedWidth - margin && width <= expectedWidth + margin, + `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`); + ok(height >= expectedHeight - margin && height <= expectedHeight + margin, + `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`); + } + + async function checkVideoElements(senderElement, receiverElements, encodings) { + is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings'); + info('Waiting for sender video element to start playing'); + await helper.checkVideoPlaying(senderElement); + for (let i = 0; i < encodings.length; i++) { + await checkVideoElement(senderElement, receiverElements[i], encodings[i]); + } + } + + const sendEncodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 }, + { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 }, + { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }]; + + async function checkSenderStats(sender) { + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, sendEncodings.length); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + } + + async function waitForResizeEvents(elements) { + return Promise.all(elements.map(elem => haveEvent(elem, 'resize'))); + } + + await pushPrefs( + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const videoStream = emitter.stream(); + offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings}); + const senderElement = document.createElement('video'); + senderElement.autoplay = true; + senderElement.srcObject = videoStream; + senderElement.id = videoStream.id + + const sender = offerer.getSenders()[0]; + let parameters = sender.getParameters(); + is(parameters.encodings[0].maxBitrate, sendEncodings[0].maxBitrate); + isfuzzy(parameters.encodings[0].scaleResolutionDownBy, + sendEncodings[0].scaleResolutionDownBy, 0.01); + is(parameters.encodings[1].maxBitrate, sendEncodings[1].maxBitrate); + isfuzzy(parameters.encodings[1].scaleResolutionDownBy, + sendEncodings[1].scaleResolutionDownBy, 0.01); + is(parameters.encodings[2].maxBitrate, sendEncodings[2].maxBitrate); + isfuzzy(parameters.encodings[2].scaleResolutionDownBy, + sendEncodings[2].scaleResolutionDownBy, 0.01); + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 3, 'Should have 3 mids in offer'); + ok(rids[0], 'First mid should be non-empty'); + ok(rids[1], 'Second mid should be non-empty'); + ok(rids[2], 'Third mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events'); + emitter.start(); + info('Waiting for 3 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + await checkVideoElements(senderElement, videoElems, parameters.encodings); + emitter.stop(); + + await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + info(`Changing source resolution to 1280x720`); + emitter.size(1280, 720); + emitter.start(); + await waitForResizeEvents([senderElement, ...videoElems]); + await checkVideoElements(senderElement, videoElems, parameters.encodings); + await checkSenderStats(sender); + + parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 1; + parameters.encodings[1].scaleResolutionDownBy = 2; + parameters.encodings[2].scaleResolutionDownBy = 3; + info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`); + await sender.setParameters(parameters); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, parameters.encodings); + await checkSenderStats(sender); + + parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 6; + parameters.encodings[1].scaleResolutionDownBy = 5; + parameters.encodings[2].scaleResolutionDownBy = 4; + info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`); + await sender.setParameters(parameters); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, parameters.encodings); + await checkSenderStats(sender); + + parameters = sender.getParameters(); + parameters.encodings[0].scaleResolutionDownBy = 4; + parameters.encodings[1].scaleResolutionDownBy = 1; + parameters.encodings[2].scaleResolutionDownBy = 2; + info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`); + await sender.setParameters(parameters); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, parameters.encodings); + await checkSenderStats(sender); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html new file mode 100644 index 0000000000..0f6d3c8520 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html @@ -0,0 +1,172 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1432793", + title: "Simulcast with odd resolution", + visible: true + }); + + runNetworkTest(async () => { + const helper = new VideoStreamHelper(); + const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528); + + async function checkVideoElement(senderElement, receiverElement, encoding) { + info(`Waiting for receiver video element ${encoding.rid} to start playing`); + await helper.checkVideoPlaying(receiverElement); + const srcWidth = senderElement.videoWidth; + const srcHeight = senderElement.videoHeight; + info(`Source resolution is ${srcWidth}x${srcHeight}`); + + const scaleDownBy = encoding.scaleResolutionDownBy; + const expectedWidth = srcWidth / scaleDownBy; + const expectedHeight = srcHeight / scaleDownBy; + const margin = srcWidth * 0.1; + const width = receiverElement.videoWidth; + const height = receiverElement.videoHeight; + const rid = encoding.rid; + ok(width >= expectedWidth - margin && width <= expectedWidth + margin, + `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`); + ok(height >= expectedHeight - margin && height <= expectedHeight + margin, + `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`); + } + + async function checkVideoElements(senderElement, receiverElements, encodings) { + is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings'); + info('Waiting for sender video element to start playing'); + await helper.checkVideoPlaying(senderElement); + for (let i = 0; i < encodings.length; i++) { + await checkVideoElement(senderElement, receiverElements[i], encodings[i]); + } + } + + async function checkSenderStats(sender) { + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, encodings.length); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + } + + async function waitForResizeEvents(elements) { + return Promise.all(elements.map(elem => haveEvent(elem, 'resize'))); + } + + const encodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 }, + { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 }, + { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }]; + + await pushPrefs( + ['media.peerconnection.simulcast', true], + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const videoStream = emitter.stream(); + offerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + const senderElement = document.createElement('video'); + senderElement.autoplay = true; + senderElement.srcObject = videoStream; + senderElement.id = videoStream.id + + const sender = offerer.getSenders()[0]; + sender.setParameters({encodings}); + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 3, 'Should have 3 mids in offer'); + ok(rids[0], 'First mid should be non-empty'); + ok(rids[1], 'Second mid should be non-empty'); + ok(rids[2], 'Third mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events'); + emitter.start(); + info('Waiting for 3 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + await checkVideoElements(senderElement, videoElems, encodings); + emitter.stop(); + + await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + info(`Changing source resolution to 1280x720`); + emitter.size(1280, 720); + emitter.start(); + await waitForResizeEvents([senderElement, ...videoElems]); + await checkVideoElements(senderElement, videoElems, encodings); + await checkSenderStats(sender); + + encodings[0].scaleResolutionDownBy = 1; + encodings[1].scaleResolutionDownBy = 2; + encodings[2].scaleResolutionDownBy = 3; + info(`Changing encodings to ${JSON.stringify(encodings)}`); + await sender.setParameters({encodings}); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, encodings); + await checkSenderStats(sender); + + encodings[0].scaleResolutionDownBy = 6; + encodings[1].scaleResolutionDownBy = 5; + encodings[2].scaleResolutionDownBy = 4; + info(`Changing encodings to ${JSON.stringify(encodings)}`); + await sender.setParameters({encodings}); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, encodings); + await checkSenderStats(sender); + + encodings[0].scaleResolutionDownBy = 4; + encodings[1].scaleResolutionDownBy = 1; + encodings[2].scaleResolutionDownBy = 2; + info(`Changing encodings to ${JSON.stringify(encodings)}`); + await sender.setParameters({encodings}); + await waitForResizeEvents(videoElems); + await checkVideoElements(senderElement, videoElems, encodings); + await checkSenderStats(sender); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html new file mode 100644 index 0000000000..cb7c13a0d1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast offer", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + const sendEncodings = [ + { rid: '0', maxBitrate: 40000 }, + { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 } + ]; + offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings}); + emitter.start(); + + const sender = offerer.getSenders()[0]; + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[0].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[1].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html new file mode 100644 index 0000000000..93141311f1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + const sendEncodings = [ + { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: '1', maxBitrate: 40000 } + ]; + offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings}); + emitter.start(); + + const sender = offerer.getSenders()[0]; + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[1].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[0].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html new file mode 100644 index 0000000000..73e2d38eb2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.peerconnection.simulcast', true], + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + offerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + emitter.start(); + + const sender = offerer.getSenders()[0]; + sender.setParameters({ + encodings: [ + { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: '1', maxBitrate: 40000 } + ] + }); + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[1].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[0].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html new file mode 100644 index 0000000000..551273af5e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="parser_rtp.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> + <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script> + <script type="application/javascript" src="simulcast.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast offer", + visible: true + }); + + runNetworkTest(async () => { + await pushPrefs( + ['media.peerconnection.simulcast', true], + // 180Kbps was determined empirically, set well-higher than + // the 80Kbps+overhead needed for the two simulcast streams. + // 100Kbps was apparently too low. + ['media.peerconnection.video.min_bitrate_estimate', 180*1000]); + + + const offerer = new RTCPeerConnection(); + const answerer = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback()); + answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback()); + + const metadataToBeLoaded = []; + answerer.ontrack = (e) => { + metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track)); + }; + + // One send transceiver, that will be used to send both simulcast streams + const emitter = new VideoFrameEmitter(); + const videoStream = emitter.stream(); + offerer.addTrack(videoStream.getVideoTracks()[0], videoStream); + emitter.start(); + + const sender = offerer.getSenders()[0]; + sender.setParameters({ + encodings: [ + { rid: '0', maxBitrate: 40000 }, + { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 } + ] + }); + + const offer = await offerer.createOffer(); + + const mungedOffer = ridToMid(offer); + info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`); + + await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer}); + await offerer.setLocalDescription(offer); + + const rids = answerer.getTransceivers().map(t => t.mid); + is(rids.length, 2, 'Should have 2 mids in offer'); + ok(rids[0] != '', 'First mid should be non-empty'); + ok(rids[1] != '', 'Second mid should be non-empty'); + info(`rids: ${JSON.stringify(rids)}`); + + const answer = await answerer.createAnswer(); + + const mungedAnswer = midToRid(answer); + info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`); + await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer}); + await answerer.setLocalDescription(answer); + + is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events'); + info('Waiting for 2 loadedmetadata events'); + const videoElems = await Promise.all(metadataToBeLoaded); + + const statsReady = + Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]); + + const helper = new VideoStreamHelper(); + info('Waiting for first video element to start playing'); + await helper.checkVideoPlaying(videoElems[0]); + info('Waiting for second video element to start playing'); + await helper.checkVideoPlaying(videoElems[1]); + + is(videoElems[0].videoWidth, 50, + "sink is same width as source, modulo our cropping algorithm"); + is(videoElems[0].videoHeight, 50, + "sink is same height as source, modulo our cropping algorithm"); + is(videoElems[1].videoWidth, 25, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(videoElems[1].videoHeight, 25, + "sink is 1/2 height of source, modulo our cropping algorithm"); + + await statsReady; + const senderStats = await sender.getStats(); + checkSenderStats(senderStats, 2); + checkExpectedFields(senderStats); + pedanticChecks(senderStats); + + emitter.stop(); + videoStream.getVideoTracks()[0].stop(); + offerer.close(); + answerer.close(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html new file mode 100644 index 0000000000..2ef98dc9c8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1337525", + title: "webRtc Stats composition and sanity" +}); + +runNetworkTest(async function (options) { + // We don't know how to get QP value when using Android system codecs. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + // For accurate comparisons of `remoteTimestamp` (not using reduced precision) + // to `timestamp` (using reduced precision). + await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter", + false]); + + const test = new PeerConnectionTest(options); + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_LOCAL_STATS]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_REMOTE_STATS]); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html new file mode 100644 index 0000000000..6e1ef698b4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1672590", + title: "Jitter sanity check" + }); + +const checkJitter = stats => { + stats.forEach((stat, mapKey) => { + if (stat.type == "remote-inbound-rtp") { + // This should be much lower for audio, TODO: Bug 1330575 + const expectedJitter = stat.kind == "video" ? 0.5 : 1; + + ok(stat.jitter < expectedJitter, + stat.type + ".jitter is sane number for a local only test. value=" + + stat.jitter); + } + }); +}; + +const PC_LOCAL_TEST_LOCAL_JITTER = async test => { + checkJitter(await waitForSyncedRtcp(test.pcLocal._pc)); +} + +const PC_REMOTE_TEST_REMOTE_JITTER = async test => { + checkJitter(await waitForSyncedRtcp(test.pcRemote._pc)); +} + +runNetworkTest(async function (options) { + // We don't know how to get QP value when using Android system codecs. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(options); + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_LOCAL_JITTER]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_REMOTE_JITTER]); + + test.setMediaConstraints([{audio: true}, {video: true}], + [{audio: true}, {video: true}]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html new file mode 100644 index 0000000000..02ace530a9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1225722", + title: "WebRTC Stats composition and sanity for a one-way peer connection" +}); + +runNetworkTest(async function (options) { + // We don't know how to get QP value when using Android system codecs. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + // For accurate comparisons of `remoteTimestamp` (not using reduced precision) + // to `timestamp` (using reduced precision). + await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter", + false]); + + const test = new PeerConnectionTest(options); + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_LOCAL_STATS]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_REMOTE_STATS]); + + const testOneWayStats = (stats, codecType) => { + const codecs = []; + stats.forEach(stat => { + if (stat.type == "codec") { + codecs.push(stat); + is(stat.codecType, codecType, "One-way codec has specific codecType"); + } + }); + is(codecs.length, 2, "One audio and one video codec"); + if (codecs.length == 2) { + isnot(codecs[0].mimeType.slice(0, 5), codecs[1].mimeType.slice(0, 5), + "Different media type for audio vs video mime types"); + } + }; + + test.chain.append([ + async function PC_LOCAL_TEST_CODECTYPE_ENCODE(test) { + testOneWayStats(await test.pcLocal._pc.getStats(), "encode"); + }, + async function PC_REMOTE_TEST_CODECTYPE_DECODE(test) { + testOneWayStats(await test.pcRemote._pc.getStats(), "decode"); + }, + ]); + + test.setMediaConstraints([{audio: true}, {video: true}], []); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html new file mode 100644 index 0000000000..cdc328fd2b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="nonTrickleIce.js"></script> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1435789", + title: "WebRTC local-candidate relayProtocol stats attribute" +}); + +// This test uses the NAT simulator in order to get srflx candidates. +// It doesn't work in https, so we turn on getUserMedia in http, which requires +// a reload. +if (!("mediaDevices" in navigator)) { + SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, + () => location.reload()); +} else { + runNetworkTest(async (options = {}) => { + await pushPrefs( + ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], + ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], + ['media.getusermedia.insecure.enabled', true]); + const test = new PeerConnectionTest(options); + makeOffererNonTrickle(test.chain); + makeAnswererNonTrickle(test.chain); + + test.chain.removeAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW"); + test.chain.append([PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE]); + + test.setMediaConstraints([{ audio: true }], [{ audio: true }]); + await test.run(); + }, { useIceServer: true }); +} + +const PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE = test => { + return test.pcLocal.getStats().then(stats => { + let haveRelayProtocol = {}; + for (let [k, v] of stats) { + if (v.type == "local-candidate") { + haveRelayProtocol[v.candidateType + "-" + v.relayProtocol] = v.relayProtocol; + } + } + is(haveRelayProtocol["host-undefined"], undefined, "relayProtocol not set for host candidates"); + is(haveRelayProtocol["srflx-undefined"], undefined, "relayProtocol not set for server reflexive candidates"); + ok(haveRelayProtocol["relay-udp"], "Has UDP relay candidate"); + ok(haveRelayProtocol["relay-tcp"], "Has TCP relay candidate"); + ok(haveRelayProtocol["relay-tls"], "Has TLS relay candidate"); + is(Object.keys(haveRelayProtocol).length, 5, "All candidate types are accounted for"); + }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html new file mode 100644 index 0000000000..ab7811fe82 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1793776", + title: "Test that media.peerconnection.sdp.disable_stereo_fmtp works" + }); + + const tests = [ + async function testStereo() { + const offerer = new RTCPeerConnection(); + offerer.addTransceiver('audio'); + const answerer = new RTCPeerConnection(); + await offerer.setLocalDescription(); + ok(offerer.localDescription.sdp.includes('stereo=1'), + 'Offer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set'); + await answerer.setRemoteDescription(offerer.localDescription); + const {sdp} = await answerer.createAnswer(); + ok(sdp.includes('stereo=1'), 'Answer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set'); + }, + + async function testNoStereo() { + await pushPrefs( + ['media.peerconnection.sdp.disable_stereo_fmtp', true]); + + const offerer = new RTCPeerConnection(); + offerer.addTransceiver('audio'); + const answerer = new RTCPeerConnection(); + await offerer.setLocalDescription(); + ok(offerer.localDescription.sdp.includes('stereo=0'), + 'Offer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set'); + await answerer.setRemoteDescription(offerer.localDescription); + const {sdp} = await answerer.createAnswer(); + ok(sdp.includes('stereo=0'), 'Answer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set'); + }, + ]; + + runNetworkTest(async () => { + for (const test of tests) { + info(`Running test: ${test.name}`); + try { + await test(); + } catch (e) { + ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`); + } + info(`Done running test: ${test.name}`); + // Make sure we don't build up a pile of GC work, and also get PCImpl to + // print their timecards. + await new Promise(r => SpecialPowers.exactGC(r)); + } + + await SpecialPowers.popPrefEnv(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html new file mode 100644 index 0000000000..98f0de1b4a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1063971", + title: "Legacy sync setDescription calls", + visible: true + }); + +// Test setDescription without callbacks, which many webrtc examples still do + +function PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC(test) { + test.pcLocal.onsignalingstatechange = function() {}; + test.pcLocal._pc.setLocalDescription(test.originalOffer); +} + +function PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC(test) { + test.pcRemote.onsignalingstatechange = function() {}; + test.pcRemote._pc.setRemoteDescription(test._local_offer, + test.pcRemote.releaseIceCandidates, + generateErrorCallback("pcRemote._pc.setRemoteDescription() sync failed")); +} +function PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC(test) { + test.pcRemote.onsignalingstatechange = function() {}; + test.pcRemote._pc.setLocalDescription(test.originalAnswer); +} +function PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC(test) { + test.pcLocal.onsignalingstatechange = function() {}; + test.pcLocal._pc.setRemoteDescription(test._remote_answer, + test.pcLocal.releaseIceCandidates, + generateErrorCallback("pcLocal._pc.setRemoteDescription() sync failed")); +} + +runNetworkTest(() => { + const test = new PeerConnectionTest(); + test.setMediaConstraints([{video: true}], [{video: true}]); + test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC); + test.chain.replace("PC_REMOTE_SET_REMOTE_DESCRIPTION", PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC); + test.chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC"); + test.chain.replace("PC_REMOTE_SET_LOCAL_DESCRIPTION", PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC); + test.chain.replace("PC_LOCAL_SET_REMOTE_DESCRIPTION", PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC); + test.chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC"); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html new file mode 100644 index 0000000000..bde51c1fd0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "RTCPeerConnection with telephone-event codec first in SDP", + bug: "1581898", + visible: true +}); + +const test = async () => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const stream = await navigator.mediaDevices.getUserMedia({audio:true}); + pc1.addTrack(stream.getAudioTracks()[0], stream); + pc2.addTrack(stream.getAudioTracks()[0], stream); + + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + + const regex = /^(m=audio \d+ [^ ]+) (.*) 101(.*)$/m; + + // Rewrite offer so payload type 101 comes first + offer.sdp = offer.sdp.replace(regex, '$1 101 $2 $3'); + + ok(offer.sdp.match(/^m=audio \d+ [^ ]+ 101 /m), + "Payload type 101 should be first on the m-line"); + + await pc2.setRemoteDescription(offer); + const answer = await pc2.createAnswer(); + + pc1.onicecandidate = e => { pc2.addIceCandidate(e.candidate); } + pc2.onicecandidate = e => { pc1.addIceCandidate(e.candidate); } + + await pc1.setRemoteDescription(answer); + await pc2.setLocalDescription(answer); + await new Promise(resolve => { + pc1.oniceconnectionstatechange = e => { + if (pc1.iceConnectionState == "connected") { + resolve(); + } + }; + }); + await wait(1000); +}; + +runNetworkTest(test); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html new file mode 100644 index 0000000000..75f0d12463 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1342579", + title: "Unbundled PC connects to two different PCs", + visible: true + }); + + const fakeFingerPrint = "a=fingerprint:sha-256 11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11"; + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + const pc3 = new RTCPeerConnection(); + + const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => { + if (e.candidate) { + if (e.candidate.sdpMid === "1") { + add(pc2, e.candidate, generateErrorCallback()) + } else { + add(pc3, e.candidate, generateErrorCallback()) + } + } + }; + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + pc3.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + let ice1Finished, ice2Finished, ice3Finished; + const ice1Done = new Promise(r => ice1Finished = r); + const ice2Done = new Promise(r => ice2Finished = r); + const ice3Done = new Promise(r => ice3Finished = r); + + const icsc = (pc, str, resolve) => { + const state = pc.iceConnectionState; + info(str + " ICE connection state is: " + state); + if (state == "connected") { + ok(true, str + " ICE connected"); + resolve(); + } else if (state == "failed") { + ok(false, str + " ICE failed") + resolve(); + } + }; + + pc1.oniceconnectionstatechange = e => icsc(pc1, "PC1", ice1Finished); + pc2.oniceconnectionstatechange = e => icsc(pc2, "PC2", ice2Finished); + pc3.oniceconnectionstatechange = e => icsc(pc3, "PC3", ice3Finished); + + + function combineAnswer(origAnswer, answer) { + const sdplines = origAnswer.sdp.split('\r\n'); + const fpIndex = sdplines.findIndex(l => l.match('^a=fingerprint')); + const FP = sdplines[fpIndex]; + const audioIndex = sdplines.findIndex(l => l.match(/^m=audio [1-9]/)); + const videoIndex = sdplines.findIndex(l => l.match(/^m=video [1-9]/)); + if (audioIndex > -1) { + var ss = sdplines.slice(0, audioIndex); + ss.splice(fpIndex, 1); + answer.sessionSection = ss; + const rejectedVideoIndex = sdplines.findIndex(l => l.match('m=video 0')); + var ams = sdplines.slice(audioIndex, rejectedVideoIndex); + ams.push(FP); + ams.push(fakeFingerPrint); + answer.audioMsection = ams; + } + if (videoIndex > -1) { + var vms = sdplines.slice(videoIndex, sdplines.length -1); + vms.push(fakeFingerPrint); + vms.push(FP); + answer.videoMsection = vms; + } + return answer; + } + +runNetworkTest(async () => { + const v1 = createMediaElement('video', 'v1'); + const v2 = createMediaElement('video', 'v2'); + const v3 = createMediaElement('video', 'v3'); + + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream)); + + const stream2 = await navigator.mediaDevices.getUserMedia({ video: true }); + (v2.srcObject = stream2).getTracks().forEach(t => pc2.addTrack(t, stream2)); + + const stream3 = await navigator.mediaDevices.getUserMedia({ audio: true }); + (v3.srcObject = stream3).getTracks().forEach(t => pc3.addTrack(t, stream3)); + + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + + //info("Original OFFER: " + JSON.stringify(offer)); + offer.sdp = sdputils.removeBundle(offer.sdp); + //info("OFFER w/o BUNDLE: " + JSON.stringify(offer)); + const offerAudio = new RTCSessionDescription(JSON.parse(JSON.stringify(offer))); + offerAudio.sdp = offerAudio.sdp.replace('m=video 9', 'm=video 0'); + //info("offerAudio: " + JSON.stringify(offerAudio)); + const offerVideo = new RTCSessionDescription(JSON.parse(JSON.stringify(offer))); + offerVideo.sdp = offerVideo.sdp.replace('m=audio 9', 'm=audio 0'); + //info("offerVideo: " + JSON.stringify(offerVideo)); + + // We need to do these in parallel, otherwise pc1 will start firing + // icecandidate events before pc3 is ready. + await Promise.all([pc2.setRemoteDescription(offerVideo), pc3.setRemoteDescription(offerAudio)]); + + const answerVideo = await pc2.createAnswer(); + const answerAudio = await pc3.createAnswer(); + + const answer = combineAnswer(answerAudio, combineAnswer(answerVideo, {})); + const fakeAnswer = answer.sessionSection.concat(answer.audioMsection, answer.videoMsection).join('\r\n'); + info("ANSWER: " + fakeAnswer); + + // We want to do these in parallel, because if we do them seqentially, by the + // time pc3.sLD completes pc2 could have fired icecandidate events, when we + // haven't called pc1.sRD yet. + await Promise.all( + [pc2.setLocalDescription(answerVideo), + pc3.setLocalDescription(answerAudio), + pc1.setRemoteDescription({type: 'answer', sdp: fakeAnswer})]); + + await Promise.all([ice1Done, ice2Done, ice3Done]); + + ok(true, "Connected."); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html new file mode 100644 index 0000000000..c476c60161 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "857765", + title: "Throw in PeerConnection callbacks" + }); + +runNetworkTest(function () { + let finish; + const onfinished = new Promise(r => finish = async () => { + window.onerror = oldOnError; + is(error_count, 7, "Seven expected errors verified."); + r(); + }); + + function getFail() { + return err => { + window.onerror = oldOnError; + generateErrorCallback()(err); + }; + } + + let error_count = 0; + let oldOnError = window.onerror; + window.onerror = (errorMsg, url, lineNumber) => { + if (!errorMsg.includes("Expected")) { + getFail()(errorMsg); + } + error_count += 1; + info("onerror " + error_count + ": " + errorMsg); + if (error_count == 7) { + finish(); + } + throw new Error("window.onerror may throw"); + } + + let pc0, pc1, pc2; + // Test failure callbacks (limited to 1 for now) + pc0 = new RTCPeerConnection(); + pc0.close(); + pc0.createOffer(getFail(), function(err) { + pc1 = new RTCPeerConnection(); + pc2 = new RTCPeerConnection(); + + // Test success callbacks (happy path) + navigator.mozGetUserMedia({video:true}, function(video1) { + pc1.addStream(video1); + pc1.createOffer(function(offer) { + pc1.setLocalDescription(offer, function() { + pc2.setRemoteDescription(offer, function() { + pc2.createAnswer(function(answer) { + pc2.setLocalDescription(answer, function() { + pc1.setRemoteDescription(answer, function() { + throw new Error("Expected"); + }, getFail()); + throw new Error("Expected"); + }, getFail()); + throw new Error("Expected"); + }, getFail()); + throw new Error("Expected"); + }, getFail()); + throw new Error("Expected"); + }, getFail()); + throw new Error("Expected"); + }, getFail()); + }, getFail()); + throw new Error("Expected"); + }); + + return onfinished; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html new file mode 100644 index 0000000000..96c2c42b78 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "928304", + title: "test toJSON() on RTCSessionDescription and RTCIceCandidate" + }); + + runNetworkTest(function () { + /** Test for Bug 872377 **/ + + var rtcSession = new RTCSessionDescription({ sdp: "Picklechips!", + type: "offer" }); + var jsonCopy = JSON.parse(JSON.stringify(rtcSession)); + for (key in rtcSession) { + if (typeof(rtcSession[key]) == "function") continue; + is(rtcSession[key], jsonCopy[key], "key " + key + " should match."); + } + + /** Test for Bug 928304 **/ + + var rtcIceCandidate = new RTCIceCandidate({ candidate: "dummy", + sdpMid: "test", + sdpMLineIndex: 3 }); + jsonCopy = JSON.parse(JSON.stringify(rtcIceCandidate)); + for (key in rtcIceCandidate) { + if (typeof(rtcIceCandidate[key]) == "function") continue; + is(rtcIceCandidate[key], jsonCopy[key], "key " + key + " should match."); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html new file mode 100644 index 0000000000..311cd91fb1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1219711", + title: "Disabling locally should be reflected remotely" +}); + +runNetworkTest(async () => { + const test = new PeerConnectionTest(); + + await pushPrefs( + ["media.getusermedia.camera.stop_on_disable.enabled", true], + ["media.getusermedia.camera.stop_on_disable.delay_ms", 0], + ["media.getusermedia.microphone.stop_on_disable.enabled", true], + ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0], + // Always use fake tracks since we depend on video to be somewhat green and + // audio to have a large 1000Hz component. + ['media.audio_loopback_dev', ''], + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + + test.setMediaConstraints([{audio: true, video: true}], []); + test.chain.append([ + function CHECK_ASSUMPTIONS() { + is(test.pcLocal.localMediaElements.length, 2, + "pcLocal should have one media element"); + is(test.pcRemote.remoteMediaElements.length, 2, + "pcRemote should have one media element"); + is(test.pcLocal._pc.getLocalStreams().length, 1, + "pcLocal should have one stream"); + is(test.pcRemote._pc.getRemoteStreams().length, 1, + "pcRemote should have one stream"); + }, + async function CHECK_VIDEO() { + const h = new CaptureStreamTestHelper2D(); + const localVideo = test.pcLocal.localMediaElements + .find(e => e instanceof HTMLVideoElement); + const remoteVideo = test.pcRemote.remoteMediaElements + .find(e => e instanceof HTMLVideoElement); + // We check a pixel somewhere away from the top left corner since + // MediaEngineFake puts semi-transparent time indicators there. + const offsetX = 50; + const offsetY = 50; + const threshold = 128; + + // We're regarding black as disabled here, and we're setting the alpha + // channel of the pixel to 255 to disregard alpha when testing. + const checkVideoEnabled = video => h.waitForPixel(video, + px => { + px[3] = 255; + return h.isPixelNot(px, h.black, threshold); + }, + { offsetX, offsetY } + ); + const checkVideoDisabled = video => h.waitForPixel(video, + px => { + px[3] = 255; + return h.isPixel(px, h.black, threshold); + }, + { offsetX, offsetY } + ); + + info("Checking local video enabled"); + await checkVideoEnabled(localVideo); + info("Checking remote video enabled"); + await checkVideoEnabled(remoteVideo); + + info("Disabling original"); + test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false; + + info("Checking local video disabled"); + await checkVideoDisabled(localVideo); + info("Checking remote video disabled"); + await checkVideoDisabled(remoteVideo); + }, + async function CHECK_AUDIO() { + const ac = new AudioContext(); + const localAnalyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]); + const remoteAnalyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]); + + const checkAudio = (analyser, fun) => analyser.waitForAnalysisSuccess(fun); + + const freq = localAnalyser.binIndexForFrequency(TEST_AUDIO_FREQ); + const checkAudioEnabled = analyser => + checkAudio(analyser, array => array[freq] > 200); + const checkAudioDisabled = analyser => + checkAudio(analyser, array => array[freq] < 50); + + info("Checking local audio enabled"); + await checkAudioEnabled(localAnalyser); + info("Checking remote audio enabled"); + await checkAudioEnabled(remoteAnalyser); + + test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false; + + info("Checking local audio disabled"); + await checkAudioDisabled(localAnalyser); + info("Checking remote audio disabled"); + await checkAudioDisabled(remoteAnalyser); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html new file mode 100644 index 0000000000..6216b8e348 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html @@ -0,0 +1,168 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1219711", + title: "Disabling locally should be reflected remotely, individually for clones" +}); + +runNetworkTest(async () => { + var test = new PeerConnectionTest(); + + await pushPrefs( + ["media.getusermedia.camera.stop_on_disable.enabled", true], + ["media.getusermedia.camera.stop_on_disable.delay_ms", 0], + ["media.getusermedia.microphone.stop_on_disable.enabled", true], + ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0], + // Always use fake tracks since we depend on audio to have a large 1000Hz + // component. + ['media.audio_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + var originalStream; + var localVideoOriginal; + + test.setMediaConstraints([{audio: true, video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_GUM_CLONE() { + return getUserMedia(test.pcLocal.constraints[0]).then(stream => { + originalStream = stream; + localVideoOriginal = + createMediaElement("video", "local-original"); + localVideoOriginal.srcObject = stream; + test.pcLocal.attachLocalStream(originalStream.clone()); + }); + } + ]); + test.chain.append([ + function CHECK_ASSUMPTIONS() { + is(test.pcLocal.localMediaElements.length, 2, + "pcLocal should have one media element"); + is(test.pcRemote.remoteMediaElements.length, 2, + "pcRemote should have one media element"); + is(test.pcLocal._pc.getLocalStreams().length, 1, + "pcLocal should have one stream"); + is(test.pcRemote._pc.getRemoteStreams().length, 1, + "pcRemote should have one stream"); + }, + async function CHECK_VIDEO() { + info("Checking video"); + var h = new CaptureStreamTestHelper2D(); + var localVideoClone = test.pcLocal.localMediaElements + .find(e => e instanceof HTMLVideoElement); + var remoteVideoClone = test.pcRemote.remoteMediaElements + .find(e => e instanceof HTMLVideoElement); + + // We check a pixel somewhere away from the top left corner since + // MediaEngineFake puts semi-transparent time indicators there. + const offsetX = 50; + const offsetY = 50; + const threshold = 128; + const remoteDisabledColor = h.black; + + // We're regarding black as disabled here, and we're setting the alpha + // channel of the pixel to 255 to disregard alpha when testing. + var checkVideoEnabled = video => h.waitForPixel(video, + px => { + px[3] = 255; + return h.isPixelNot(px, h.black, threshold); + }, + { offsetX, offsetY } + ); + var checkVideoDisabled = video => h.waitForPixel(video, + px => { + px[3] = 255; + return h.isPixel(px, h.black, threshold); + }, + { offsetX, offsetY } + ); + + info("Checking local original enabled"); + await checkVideoEnabled(localVideoOriginal); + info("Checking local clone enabled"); + await checkVideoEnabled(localVideoClone); + info("Checking remote clone enabled"); + await checkVideoEnabled(remoteVideoClone); + + info("Disabling original"); + originalStream.getVideoTracks()[0].enabled = false; + + info("Checking local original disabled"); + await checkVideoDisabled(localVideoOriginal); + info("Checking local clone enabled"); + await checkVideoEnabled(localVideoClone); + info("Checking remote clone enabled"); + await checkVideoEnabled(remoteVideoClone); + + info("Re-enabling original; disabling clone"); + originalStream.getVideoTracks()[0].enabled = true; + test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false; + + info("Checking local original enabled"); + await checkVideoEnabled(localVideoOriginal); + info("Checking local clone disabled"); + await checkVideoDisabled(localVideoClone); + info("Checking remote clone disabled"); + await checkVideoDisabled(remoteVideoClone); + }, + async function CHECK_AUDIO() { + info("Checking audio"); + var ac = new AudioContext(); + var localAnalyserOriginal = new AudioStreamAnalyser(ac, originalStream); + var localAnalyserClone = + new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]); + var remoteAnalyserClone = + new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]); + + var freq = localAnalyserOriginal.binIndexForFrequency(TEST_AUDIO_FREQ); + var checkAudioEnabled = analyser => + analyser.waitForAnalysisSuccess(array => array[freq] > 200); + var checkAudioDisabled = analyser => + analyser.waitForAnalysisSuccess(array => array[freq] < 50); + + info("Checking local original enabled"); + await checkAudioEnabled(localAnalyserOriginal); + info("Checking local clone enabled"); + await checkAudioEnabled(localAnalyserClone); + info("Checking remote clone enabled"); + await checkAudioEnabled(remoteAnalyserClone); + + info("Disabling original"); + originalStream.getAudioTracks()[0].enabled = false; + + info("Checking local original disabled"); + await checkAudioDisabled(localAnalyserOriginal); + info("Checking local clone enabled"); + await checkAudioEnabled(localAnalyserClone); + info("Checking remote clone enabled"); + await checkAudioEnabled(remoteAnalyserClone); + + info("Re-enabling original; disabling clone"); + originalStream.getAudioTracks()[0].enabled = true; + test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false; + + info("Checking local original enabled"); + await checkAudioEnabled(localAnalyserOriginal); + info("Checking local clone disabled"); + await checkAudioDisabled(localAnalyserClone); + info("Checking remote clone disabled"); + await checkAudioDisabled(remoteAnalyserClone); + }, + ]); + await test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html new file mode 100644 index 0000000000..f0356f5655 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1452673", + title: "Trackless RTCRtpSender.getStats()", + visible: true + }); + + // Calling getstats() on a trackless RTCRtpSender should yield an empty + // stats report. When track stats are added in the future, the stats + // for the removed tracks should continue to appear. + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW"); + test.chain.append( + async function PC_LOCAL_AND_REMOTE_TRACKLESS_SENDER_STATS(test) { + await Promise.all([ + waitForSyncedRtcp(test.pcLocal._pc), + waitForSyncedRtcp(test.pcRemote._pc), + ]); + let senders = test.pcLocal.getSenders(); + let receivers = test.pcRemote.getReceivers(); + is(senders.length, 2, "Have exactly two senders."); + is(receivers.length, 2, "Have exactly two receivers."); + for(let kind of ["audio", "video"]) { + is(senders.filter(s => s.track.kind == kind).length, 1, + "Exactly 1 sender of kind " + kind); + is(receivers.filter(r => r.track.kind == kind).length, 1, + "Exactly 1 receiver of kind " + kind); + } + // Remove tracks from senders + for (const sender of senders) { + await sender.replaceTrack(null); + is(sender.track, null, "Sender track removed"); + let stats = await sender.getStats(); + ok(stats instanceof window.RTCStatsReport, "Stats is instance of RTCStatsReport"); + // Number of stats in the report. This should be 0. + is(stats.size, 0, "Trackless sender stats report is empty"); + } + } + ); + test.setMediaConstraints([{audio: true}, {video: true}], []); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html new file mode 100644 index 0000000000..7ea18ab3dd --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1091242", + title: "Multistream: Two audio streams" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}, {audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html new file mode 100644 index 0000000000..99d4ad625a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1145407", + title: "Multistream: Two audio tracks in one stream" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_GET_OFFER", [ + function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) { + test._local_offer.sdp = test._local_offer.sdp.replace( + /a=msid:[^\s]*/g, + "a=msid:foo"); + } + ]); + test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [ + function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) { + test._remote_answer.sdp = test._remote_answer.sdp.replace( + /a=msid:[^\s]*/g, + "a=msid:foo"); + } + ]); + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}, {audio: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html new file mode 100644 index 0000000000..5f4bd463d4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + + createHTML({ + bug: "1091242", + title: "Multistream: Two audio streams, two video streams" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}, {audio: true}, + {video: true}], + [{audio: true}, {video: true}, {audio: true}, + {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html new file mode 100644 index 0000000000..fcc9c6c8fa --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + + createHTML({ + bug: "1091242", + title: "Multistream: Two audio/video streams" + }); + + runNetworkTest(async (options) => { + // Disable platform encodre for SW MFT encoder causes some stats + // exceeding the test thresholds. + // E.g. inbound-rtp.packetsDiscarded value=118 >= 100. + await matchPlatformH264CodecPrefs(); + + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true, video: true}, + {audio: true, video: true}], + [{audio: true, video: true}, + {audio: true, video: true}]); + + // Test stats, including coalescing of codec stats. + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_LOCAL_STATS]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_REMOTE_STATS]); + + const testCoalescedCodecStats = stats => { + is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length, + 16, + "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound"); + const codecs = [...stats.values()] + .filter(({type}) => type == "codec") + .sort((a, b) => a.mimeType > b.mimeType); + is(codecs.length, 2, "Should have registered two codecs (coalesced)"); + is(new Set(codecs.map(({transportId}) => transportId)).size, 1, + "Should have registered only one transport with BUNDLE"); + const codecTypes = new Set(codecs.map(({codecType}) => codecType)); + is(codecTypes.size, 1, + "Should have identical encode and decode configurations (and stats)"); + is(codecTypes[0], undefined, + "Should have identical encode and decode configurations (and stats)"); + is(codecs[0].mimeType.slice(0, 5), "audio", + "Should have registered an audio codec"); + is(codecs[1].mimeType.slice(0, 5), "video", + "Should have registered a video codec"); + }; + + test.chain.append([ + async function PC_LOCAL_TEST_COALESCED_CODEC_STATS() { + testCoalescedCodecStats(await test.pcLocal._pc.getStats()); + }, + async function PC_REMOTE_TEST_COALESCED_CODEC_STATS() { + testCoalescedCodecStats(await test.pcRemote._pc.getStats()); + }, + ]); + + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html new file mode 100644 index 0000000000..8b825db617 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1225722", + title: "Multistream: Two audio/video streams without BUNDLE" +}); + +runNetworkTest(async (options = {}) => { + // Disable platform encodre for SW MFT encoder causes some stats + // exceeding the test thresholds. + // E.g. inbound-rtp.packetsDiscarded value=118 >= 100. + await matchPlatformH264CodecPrefs(); + + options.bundle = false; + const test = new PeerConnectionTest(options); + test.setMediaConstraints( + [{audio: true, video: true}, {audio: true, video: true}], + [{audio: true, video: true}, {audio: true, video: true}] + ); + + // Test stats, including that codec stats do not coalesce without BUNDLE. + const testNonBundledStats = async pc => { + // This is basically PC_*_TEST_*_STATS fleshed out, but uses + // sender/receiver.getStats instead of pc.getStats, since the codec stats + // code assumes at most one sender and at most one receiver. + await waitForSyncedRtcp(pc); + const senderPromises = pc.getSenders().map(obj => obj.getStats()); + const receiverPromises = pc.getReceivers().map(obj => obj.getStats()); + const senderStats = await Promise.all(senderPromises); + const receiverStats = await Promise.all(receiverPromises); + for (const stats of [...senderStats, ...receiverStats]) { + checkExpectedFields(stats); + pedanticChecks(stats); + } + for (const stats of senderStats) { + checkSenderStats(stats, 1); + } + }; + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [ + async function PC_LOCAL_TEST_LOCAL_NONBUNDLED_STATS(test) { + await testNonBundledStats(test.pcLocal._pc); + }, + ]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [ + async function PC_REMOTE_TEST_LOCAL_NONBUNDLED_STATS(test) { + await testNonBundledStats(test.pcRemote._pc); + }, + ]); + + const testNonCoalescedCodecStats = stats => { + const codecs = [...stats.values()] + .filter(({type}) => type == "codec"); + is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length, 16, + "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound"); + const codecTypes = new Set(codecs.map(({codecType}) => codecType)); + is(codecTypes.size, 1, + "Should have identical encode and decode configurations (and stats)"); + is(codecTypes[0], undefined, + "Should have identical encode and decode configurations (and stats)"); + const transportIds = new Set(codecs.map(({transportId}) => transportId)); + is(transportIds.size, 4, + "Should have registered four transports for two sendrecv streams"); + for (const transportId of transportIds) { + is(codecs.filter(c => c.transportId == transportId).length, 1, + "Should have registered one codec per transport without BUNDLE"); + } + for (const prefix of ["audio", "video"]) { + const prefixed = codecs.filter(c => c.mimeType.startsWith(prefix)); + is(prefixed.length, 2, `Should have registered two ${prefix} codecs`); + if (prefixed.length == 2) { + is(prefixed[0].payloadType, prefixed[1].payloadType, + "same payloadType"); + isnot(prefixed[0].transportId, prefixed[1].transportId, + "different transportIds"); + is(prefixed[0].mimeType, prefixed[1].mimeType, "same mimeType"); + is(prefixed[0].clockRate, prefixed[1].clockRate, "same clockRate"); + is(prefixed[0].channels, prefixed[1].channels, "same channels"); + is(prefixed[0].sdpFmtpLine, prefixed[1].sdpFmtpLine, + "same sdpFmtpLine"); + } + } + }; + + test.chain.append([ + async function PC_LOCAL_TEST_NON_COALESCED_CODEC_STATS() { + testNonCoalescedCodecStats(await test.pcLocal._pc.getStats()); + }, + async function PC_REMOTE_TEST_NON_COALESCED_CODEC_STATS() { + testNonCoalescedCodecStats(await test.pcRemote._pc.getStats()); + }, + ]); + + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html new file mode 100644 index 0000000000..0ab180cc55 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1091242", + title: "Multistream: Two video streams" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html new file mode 100644 index 0000000000..4eaf8b3f48 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1145407", + title: "Multistream: Two video tracks in offerer stream" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + test.chain.insertAfter("PC_REMOTE_GET_OFFER", [ + function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) { + test._local_offer.sdp = test._local_offer.sdp.replace( + /a=msid:[^\s]*/g, + "a=msid:foo"); + } + ]); + test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [ + function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) { + test._remote_answer.sdp = test._remote_answer.sdp.replace( + /a=msid:[^\s]*/g, + "a=msid:foo"); + } + ]); + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}, {video: true}]); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html new file mode 100644 index 0000000000..4f5397873d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1166832", + title: "Renegotiation: verify audio after renegotiation" + }); + + runNetworkTest(function (options) { + const test = new PeerConnectionTest(options); + const helper = new AudioStreamFlowingHelper(); + + test.chain.append([ + function CHECK_ASSUMPTIONS() { + is(test.pcLocal.localMediaElements.length, 1, + "pcLocal should have one media element"); + is(test.pcRemote.remoteMediaElements.length, 1, + "pcRemote should have one media element"); + is(test.pcLocal._pc.getLocalStreams().length, 1, + "pcLocal should have one stream"); + is(test.pcRemote._pc.getRemoteStreams().length, 1, + "pcRemote should have one stream"); + }, + function CHECK_AUDIO() { + return Promise.resolve() + .then(() => info("Checking local audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0])) + .then(() => info("Checking remote audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0])) + + .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false) + + .then(() => info("Checking local audio disabled")) + .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0])) + .then(() => info("Checking remote audio disabled")) + .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0])) + } + ]); + + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + test.setMediaConstraints([{audio: true}], + []); + return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]); + }, + ] + ); + + test.chain.append([ + function CHECK_ASSUMPTIONS2() { + is(test.pcLocal.localMediaElements.length, 2, + "pcLocal should have two media elements"); + is(test.pcRemote.remoteMediaElements.length, 2, + "pcRemote should have two media elements"); + is(test.pcLocal._pc.getLocalStreams().length, 2, + "pcLocal should have two streams"); + is(test.pcRemote._pc.getRemoteStreams().length, 2, + "pcRemote should have two streams"); + }, + function RE_CHECK_AUDIO() { + return Promise.resolve() + .then(() => info("Checking local audio enabled")) + .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0])) + .then(() => info("Checking remote audio enabled")) + .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0])) + + .then(() => info("Checking local2 audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[1])) + .then(() => info("Checking remote2 audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[1])) + + .then(() => test.pcLocal._pc.getLocalStreams()[1].getAudioTracks()[0].enabled = false) + .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = true) + + .then(() => info("Checking local2 audio disabled")) + .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[1])) + .then(() => info("Checking remote2 audio disabled")) + .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[1])) + + .then(() => info("Checking local audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0])) + .then(() => info("Checking remote audio enabled")) + .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0])) + } + ]); + + test.setMediaConstraints([{audio: true}], []); + return test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html new file mode 100644 index 0000000000..f685f7c99a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1264479", + title: "PeerConnection verify current and pending descriptions" + }); + + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed); + pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); + pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); + + + runNetworkTest(function() { + const v1 = createMediaElement('video', 'v1'); + const v2 = createMediaElement('video', 'v2'); + + return navigator.mediaDevices.getUserMedia({ video: true, audio: true }) + .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream))) + .then(() => pc1.createOffer({})) // check that createOffer accepts arg. + .then(offer => pc1.setLocalDescription(offer)) + .then(() => { + ok(!pc1.currentLocalDescription, "pc1 currentLocalDescription is empty"); + ok(pc1.pendingLocalDescription, "pc1 pendingLocalDescription is set"); + ok(pc1.localDescription, "pc1 localDescription is set"); + }) + .then(() => pc2.setRemoteDescription(pc1.localDescription)) + .then(() => { + ok(!pc2.currentRemoteDescription, "pc2 currentRemoteDescription is empty"); + ok(pc2.pendingRemoteDescription, "pc2 pendingRemoteDescription is set"); + ok(pc2.remoteDescription, "pc2 remoteDescription is set"); + }) + .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg. + .then(answer => pc2.setLocalDescription(answer)) + .then(() => { + ok(pc2.currentLocalDescription, "pc2 currentLocalDescription is set"); + ok(!pc2.pendingLocalDescription, "pc2 pendingLocalDescription is empty"); + ok(pc2.localDescription, "pc2 localDescription is set"); + }) + .then(() => pc1.setRemoteDescription(pc2.localDescription)) + .then(() => { + ok(pc1.currentRemoteDescription, "pc1 currentRemoteDescription is set"); + ok(!pc1.pendingRemoteDescription, "pc1 pendingRemoteDescription is empty"); + ok(pc1.remoteDescription, "pc1 remoteDescription is set"); + }); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html new file mode 100644 index 0000000000..8d4155ddff --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1166832", + title: "Renegotiation: verify video after renegotiation" + }); + +runNetworkTest(async () => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const test = new PeerConnectionTest(); + + const h1 = new CaptureStreamTestHelper2D(50, 50); + const canvas1 = h1.createAndAppendElement('canvas', 'source_canvas1'); + let stream1; + let vremote1; + + const h2 = new CaptureStreamTestHelper2D(50, 50); + let canvas2; + let stream2; + let vremote2; + + test.setMediaConstraints([{video: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function DRAW_INITIAL_LOCAL_GREEN(test) { + h1.drawColor(canvas1, h1.green); + }, + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + stream1 = canvas1.captureStream(0); + test.pcLocal.attachLocalStream(stream1); + let i = 0; + return setInterval(function() { + try { + info("draw " + i ? "green" : "red"); + h1.drawColor(canvas1, i ? h1.green : h1.red); + i = 1 - i; + stream1.requestFrame(); + if (stream2 != null) { + h2.drawColor(canvas2, i ? h2.green : h2.blue); + stream2.requestFrame(); + } + } catch (e) { + // ignore; stream might have shut down, and we don't bother clearing + // the setInterval. + } + }, 500); + } + ]); + + test.chain.append([ + function FIND_REMOTE_VIDEO() { + vremote1 = test.pcRemote.remoteMediaElements[0]; + ok(!!vremote1, "Should have remote video element for pcRemote"); + }, + function WAIT_FOR_REMOTE_GREEN() { + return h1.pixelMustBecome(vremote1, h1.green, { + threshold: 128, + infoString: "pcRemote's remote should become green", + }); + }, + function WAIT_FOR_REMOTE_RED() { + return h1.pixelMustBecome(vremote1, h1.red, { + threshold: 128, + infoString: "pcRemote's remote should become red", + }); + } + ]); + + addRenegotiation(test.chain, + [ + function PC_LOCAL_ADD_SECOND_STREAM(test) { + canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2'); + h2.drawColor(canvas2, h2.blue); + stream2 = canvas2.captureStream(0); + + // can't use test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]); + // because it doesn't let us substitute the capture stream + test.pcLocal.attachLocalStream(stream2); + } + ] + ); + + test.chain.append([ + function FIND_REMOTE2_VIDEO() { + vremote2 = test.pcRemote.remoteMediaElements[1]; + ok(!!vremote2, "Should have remote2 video element for pcRemote"); + }, + function WAIT_FOR_REMOTE2_BLUE() { + return h2.pixelMustBecome(vremote2, h2.blue, { + threshold: 128, + infoString: "pcRemote's remote2 should become blue", + }); + }, + function DRAW_NEW_LOCAL_GREEN(test) { + stream1.requestFrame(); + h1.drawColor(canvas1, h1.green); + }, + function WAIT_FOR_REMOTE1_GREEN() { + return h1.pixelMustBecome(vremote1, h1.green, { + threshold: 128, + infoString: "pcRemote's remote1 should become green", + }); + } + ]); + + await test.run(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html new file mode 100644 index 0000000000..7a245b5d8c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="stats.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1395853", + title: "Verify video content over WebRTC for every video codec", + }); + + async function testVideoCodec(options = {}, codec) { + const test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}], []); + + let payloadType; + test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [ + function PC_LOCAL_FILTER_OUT_CODECS() { + const otherCodec = codecs.find(c => c != codec); + const otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec.name, otherCodec.offset); + const otherRtpmapMatcher = new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi"); + + const id = sdputils.findCodecId(test.originalOffer.sdp, codec.name, codec.offset); + payloadType = Number(id); + if (codec.offset) { + isnot(id, sdputils.findCodecId(test.originalOffer.sdp, codec.name, 0), + "Different offsets should return different payload types"); + } + test.originalOffer.sdp = + sdputils.removeAllButPayloadType(test.originalOffer.sdp, id); + + ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")), + `Other codec ${otherId} should be removed after filtering`); + ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")), + `Tested codec ${id} should remain after filtering`); + + // We only set it now, or the framework would remove non-H264 codecs + // for us. + options.h264 = codec.name == "H264"; + }, + ]); + + test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", + [PC_LOCAL_TEST_LOCAL_STATS]); + + test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", + [PC_REMOTE_TEST_REMOTE_STATS]); + + test.chain.append([ + async function PC_LOCAL_TEST_CODEC() { + const stats = await test.pcLocal._pc.getStats(); + let codecCount = 0; + stats.forEach(stat => { + if (stat.type == "codec") { + is(codecCount++, 0, "expected only one encode codec stat"); + is(stat.payloadType, payloadType, "payloadType as expected"); + is(stat.mimeType, `video/${codec.name}`, "mimeType as expected"); + is(stat.codecType, "encode", "codecType as expected"); + } + }); + }, + async function PC_REMOTE_TEST_CODEC() { + const stats = await test.pcRemote._pc.getStats(); + let codecCount = 0; + stats.forEach(stat => { + if (stat.type == "codec") { + is(codecCount++, 0, "expected only one decode codec stat"); + is(stat.payloadType, payloadType, "payloadType as expected"); + is(stat.mimeType, `video/${codec.name}`, "mimeType as expected"); + is(stat.codecType, "decode", "codecType as expected"); + } + }); + }, + async function CHECK_VIDEO_FLOW() { + try { + const h = new VideoStreamHelper(); + await h.checkVideoPlaying( + test.pcRemote.remoteMediaElements[0], + 10, 10, 128); + ok(true, `Got video flow for codec ${codec.name}, offset ${codec.offset}`); + } catch(e) { + ok(false, `No video flow for codec ${codec.name}, offset ${codec.offset}: ${e}`); + } + }, + ]); + + await test.run(); + } + + // We match the name against the sdp to figure out the payload type, + // so all other present codecs can be removed. + // Use `offset` when there are multiple instances of a codec expected in an sdp. + const codecs = [ + { name: "VP8" }, + { name: "VP9" }, + { name: "H264" }, + { name: "H264", offset: 1 }, + ]; + + runNetworkTest(async (options) => { + // This test expects the video being captured will change color. Use fake + // video device as loopback does not currently change. + await pushPrefs( + ['media.video_loopback_dev', ''], + ['media.navigator.streams.fake', true]); + for (let codec of codecs) { + info(`Testing video for codec ${codec.name} offset ${codec.offset}`); + try { + let enc = SpecialPowers.getBoolPref('media.webrtc.platformencoder'); + let dec = SpecialPowers.getBoolPref('media.navigator.mediadatadecoder_h264_enabled'); + if (codec.name == "H264") { + await matchPlatformH264CodecPrefs(); + if (codec.offset == 1) { + // Force fake GMP codec for H.264 mode 0 because not all platforms + // support slice size control. Re-enable it after + // a. SW encoder fallback support (bug 1726617), and + // b. returning valid bitstream from fake GMP encoder (bug 1509012). + await pushPrefs( + ['media.webrtc.platformencoder', false], + ['media.navigator.mediadatadecoder_h264_enabled', false], + ); + } + } + await testVideoCodec(options, codec); + await pushPrefs( + ['media.webrtc.platformencoder', enc], + ['media.navigator.mediadatadecoder_h264_enabled', dec], + ); + } catch(e) { + ok(false, `Error in test for codec ${codec.name}: ${e}\n${e.stack}`); + } + info(`Tested video for codec ${codec.name}`); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html new file mode 100644 index 0000000000..b77633493d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> + <script type="application/javascript" src="sdpUtils.js"></script> + <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1213773", + title: "Renegotiation: answerer uses a=inactive for video" + }); + + runNetworkTest(async (options) => { + // [TODO] re-enable HW decoder after bug 1526207 is fixed. + if (navigator.userAgent.includes("Android")) { + await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false], + ["media.webrtc.hw.h264.enabled", false]); + } + + const emitter = new VideoFrameEmitter(); + const helper = new VideoStreamHelper(); + + const test = new PeerConnectionTest(options); + + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + test.pcLocal.attachLocalStream(emitter.stream()); + emitter.start(); + } + ]); + + var haveFirstUnmuteEvent; + + test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [ + function PC_REMOTE_SETUP_ONUNMUTE_1() { + haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute"); + } + ]); + + test.chain.append([ + function PC_REMOTE_CHECK_VIDEO_UNMUTED() { + return haveFirstUnmuteEvent; + }, + function PC_REMOTE_WAIT_FOR_FRAMES() { + var vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + return addFinallyToPromise(helper.checkVideoPlaying(vremote)) + .finally(() => emitter.stop()); + } + ]); + + addRenegotiation(test.chain, []); + + test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [ + function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) { + test._remote_answer.sdp = + sdputils.setAllMsectionsInactive(test._remote_answer.sdp); + } + ], false, 1); + + test.chain.append([ + function PC_REMOTE_ENSURE_NO_FRAMES() { + var vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + return addFinallyToPromise(helper.checkVideoPaused(vremote)) + .finally(() => emitter.stop()); + }, + ]); + + test.chain.remove("PC_REMOTE_CHECK_STATS", 1); + test.chain.remove("PC_LOCAL_CHECK_STATS", 1); + + addRenegotiation(test.chain, []); + + test.chain.append([ + function PC_REMOTE_WAIT_FOR_FRAMES_2() { + var vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + return addFinallyToPromise(helper.checkVideoPlaying(vremote)) + .finally(() => emitter.stop()); + } + ]); + + test.setMediaConstraints([{video: true}], []); + await test.run(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html new file mode 100644 index 0000000000..1d695ecbfa --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + bug: "1081819", + title: "WebAudio on both input and output side of peerconnection" +}); + +// This tests WebAudio (a 700Hz OscillatorNode) as input to a PeerConnection. +// It also tests that a PeerConnection works as input to WebAudio as the remote +// stream is connected to an AnalyserNode and compared to the source node. + +runNetworkTest(function() { + const test = new PeerConnectionTest(); + test.audioContext = new AudioContext(); + test.setMediaConstraints([{audio: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_WEBAUDIO_SOURCE(test) { + const oscillator = test.audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.value = 700; + oscillator.start(); + const dest = test.audioContext.createMediaStreamDestination(); + oscillator.connect(dest); + test.pcLocal.attachLocalStream(dest.stream); + } + ]); + test.chain.append([ + function CHECK_AUDIO_FLOW(test) { + return test.pcRemote.checkReceivingToneFrom(test.audioContext, test.pcLocal); + } + ]); + return test.run(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_selftest.html b/dom/media/webrtc/tests/mochitests/test_selftest.html new file mode 100644 index 0000000000..3f1ce1402d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_selftest.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + title: "Self-test of harness functions", + visible: true + }); + +function TEST(test) {} + +var catcher = func => { + try { + func(); + return null; + } catch (e) { + return e.message; + } +}; + +runNetworkTest(() => { + var test = new PeerConnectionTest(); + test.setMediaConstraints([{video: true}], [{video: true}]); + is(catcher(() => test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", TEST)), + null, "test.chain.replace works"); + is(catcher(() => test.chain.replace("FOO", TEST)), + "Unknown test: FOO", "test.chain.replace catches typos"); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId-echoCancellation.html b/dom/media/webrtc/tests/mochitests/test_setSinkId-echoCancellation.html new file mode 100644 index 0000000000..fa13b72bfb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId-echoCancellation.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> +<title></title> +<script src="mediaStreamPlayback.js"></script> +</head> +<script> +"use strict"; + +createHTML({ + title: "Test echoCancellation with setSinkId()", + bug: "1849108", + visible: true, +}); +/** +This test captures audio from a loopback device to test echo cancellation. + **/ +runTest(async () => { + await SpecialPowers.pushPrefEnv({set: [ + // skip selectAudioOutput/getUserMedia permission prompt + ["media.navigator.permission.disabled", true], + // enumerateDevices() without focus + ["media.devices.unfocused.enabled", true], + ]}); + + const ac = new AudioContext(); + const dest = new MediaStreamAudioDestinationNode(ac); + const gain = new GainNode(ac, {gain: 0.5}); + gain.connect(dest); + // Use a couple of triangle waves for audio with some bandwidth. + // Pick incommensurable frequencies so that the audio is aperiodic. + // Perhaps that might help the AEC determine the delay. + const osc1 = + new OscillatorNode(ac, {type: "triangle", frequency: 200}); + const osc2 = + new OscillatorNode(ac, {type: "triangle", frequency: Math.PI * 100}); + osc1.connect(gain); + osc2.connect(gain); + osc1.start(); + osc2.start(); + const audio = new Audio(); + audio.srcObject = dest.stream; + audio.controls = true; + document.body.appendChild(audio); + + // The loopback device is currenly only available on Linux. + let loopbackInputLabel = + SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!navigator.userAgent.includes("Linux")) { + todo_isnot(loopbackInputLabel, "", "audio_loopback_dev"); + return; + } + isnot(loopbackInputLabel, "", + "audio_loopback_dev. Use --use-test-media-devices."); + + const loopbackStream = await navigator.mediaDevices.getUserMedia({ audio: { + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false, + }}); + is(loopbackStream.getTracks()[0].label, loopbackInputLabel, + "loopback track label"); + + // Check that the loopback stream contains silence now. + const loopbackNode = ac.createMediaStreamSource(loopbackStream); + const processor1 = ac.createScriptProcessor(4096, 1, 0); + loopbackNode.connect(processor1); + const {inputBuffer} = await new Promise(r => processor1.onaudioprocess = r); + loopbackNode.disconnect(); + is(inputBuffer.getChannelData(0).find(value => value != 0.0), undefined, + "should have silence in loopback input"); + + // Find the loopback output device + const devices = await navigator.mediaDevices.enumerateDevices(); + let loopbackOutputLabel = + SpecialPowers.getCharPref("media.cubeb.output_device", ""); + const outputDeviceInfo = devices.find( + ({kind, label}) => kind == "audiooutput" && label == loopbackOutputLabel + ); + ok(outputDeviceInfo, `found "${loopbackOutputLabel}"`); + + await audio.setSinkId(outputDeviceInfo.deviceId); + await audio.play(); + + const analyser = new AudioStreamAnalyser(ac, loopbackStream); + const bin1 = analyser.binIndexForFrequency(osc1.frequency.value); + const bin2 = analyser.binIndexForFrequency(osc2.frequency.value); + try { + analyser.enableDebugCanvas(); + // Check for audio with AEC. + await analyser.waitForAnalysisSuccess(array => { + return array[bin1] > 200 && array[bin2] > 200; + }); + + // Check echo cancellation. + await loopbackStream.getTracks()[0].applyConstraints({ + echoCancellation: true, + autoGainControl: false, + noiseSuppression: false, + }); + await analyser.waitForAnalysisSuccess(array => { + return !array.find(bin => bin > 50); + }); + } finally { + await ac.close(); + loopbackStream.getTracks()[0].stop(); + } +}); +</script> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html b/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html new file mode 100644 index 0000000000..5b044b0bc8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId-stream-source.html @@ -0,0 +1,138 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test setSinkId() on an Audio element with MediaStream source</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +SimpleTest.requestFlakyTimeout("delays to trigger races"); + +function maybeTodoIs(a, b, msg) { + if (Object.is(a, b)) { + is(a, b, msg); + } else { + todo(false, msg, `got ${a}, wanted ${b}`); + } +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({set: [ + // skip selectAudioOutput/getUserMedia permission prompt + ["media.navigator.permission.disabled", true], + // enumerateDevices() without focus + ["media.devices.unfocused.enabled", true], + ]}); + + const audio = new Audio(); + const stream1 = new AudioContext().createMediaStreamDestination().stream; + audio.srcObject = stream1; + audio.controls = true; + document.body.appendChild(audio); + await audio.play(); + + // Expose an audio output device. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + const {deviceId, label: label1} = await navigator.mediaDevices.selectAudioOutput(); + isnot(deviceId, "", "deviceId from selectAudioOutput()"); + + // pre-fill devices cache to reduce delay until MediaStreamRenderer acts on + // setSinkId(). + await navigator.mediaDevices.enumerateDevices(); + + SpecialPowers.pushPrefEnv({set: [ + ["media.cubeb.slow_stream_init_ms", 200], + ]}); + + // When playback is stopped before setSinkId()'s parallel step "Switch the + // underlying audio output device for element to the audio device identified + // by sinkId" completes, then whether that step "failed" might be debatable. + // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid + // Gecko chooses to resolve the setSinkId() promise so that behavior does + // not depend on a race (assuming that switching would succeed if allowed to + // complete). + async function expectSetSinkIdResolutionWithSubsequentAction( + deviceId, action, actionLabel) { + let p = audio.setSinkId(deviceId); + // Wait long enough for MediaStreamRenderer to initiate a switch to the new + // device, but not so long as the new device's graph has started. + await new Promise(r => setTimeout(r, 100)); + action(); + const resolved = await p.then(() => true, () => false); + ok(resolved, `setSinkId before ${actionLabel}`); + } + + await expectSetSinkIdResolutionWithSubsequentAction( + deviceId, () => audio.pause(), "pause"); + + await audio.setSinkId(""); + await audio.play(); + await expectSetSinkIdResolutionWithSubsequentAction( + deviceId, () => audio.srcObject = null, "null srcObject"); + + await audio.setSinkId(""); + audio.srcObject = stream1; + await audio.play(); + await expectSetSinkIdResolutionWithSubsequentAction( + deviceId, () => stream1.getTracks()[0].stop(), "stop"); + + const stream2 = new AudioContext().createMediaStreamDestination().stream; + audio.srcObject = stream2; + await audio.play(); + + let loopbackInputLabel = + SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!navigator.userAgent.includes("Linux")) { + todo_isnot(loopbackInputLabel, "", "audio_loopback_dev"); + return; + } + isnot(loopbackInputLabel, "", + "audio_loopback_dev. Use --use-test-media-devices."); + + // Expose more output devices + SpecialPowers.pushPrefEnv({set: [ + ["media.audio_loopback_dev", ""], + ]}); + const inputStream = await navigator.mediaDevices.getUserMedia({audio: true}); + inputStream.getTracks()[0].stop(); + const devices = await navigator.mediaDevices.enumerateDevices(); + const {deviceId: otherDeviceId} = devices.find( + ({kind, label}) => kind == "audiooutput" && label != label1); + ok(otherDeviceId, "id2"); + isnot(otherDeviceId, deviceId, "other id is different"); + + // With multiple setSinkId() calls having `sinkId` parameters differing from + // the element's `sinkId` attribute, the order of each "switch the + // underlying audio output device" and each subsequent Promise settling is + // not clearly specified due to parallel steps for different calls not + // specifically running on the same task queue. + // https://w3c.github.io/mediacapture-output/#dom-htmlmediaelement-setsinkid + // Gecko aims to switch and settle in the same order as corresonding + // setSinkId() calls, but this does not necessarily happen - bug 1874629. + async function setSinkIdTwice(id1, id2, label) { + const p1 = audio.setSinkId(id1); + const p2 = audio.setSinkId(id2); + let p1Settled = false; + let p1SettledFirst; + const results = await Promise.allSettled([ + p1.finally(() => p1Settled = true), + p2.finally(() => p1SettledFirst = p1Settled), + ]); + maybeTodoIs(results[0].status, "fulfilled", `${label}: results[0]`); + maybeTodoIs(results[1].status, "fulfilled", `${label}: results[1]`); + maybeTodoIs(p1SettledFirst, true, + `${label}: first promise should settle first`); + } + + is(audio.sinkId, deviceId, "sinkId after stop"); + await setSinkIdTwice(otherDeviceId, "", "other then empty"); + + maybeTodoIs(audio.sinkId, "", "sinkId after empty"); + await setSinkIdTwice(deviceId, otherDeviceId, "both not empty"); + + stream2.getTracks()[0].stop() +}); +</script> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId.html b/dom/media/webrtc/tests/mochitests/test_setSinkId.html new file mode 100644 index 0000000000..0d85114a0e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> + +<script> + createHTML({ + title: "SetSinkId in HTMLMediaElement", + bug: "934425", + }); + + const memoryReportPath = 'explicit/media/media-manager-aggregates'; + + /** + * Run a test to verify set sink id in audio element. + */ + runTest(async () => { + await pushPrefs(["media.setsinkid.enabled", true]); + + if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) { + ok(false, "No loopback device set by framework. Try --use-test-media-devices"); + return; + } + + // Expose an audio output device. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await navigator.mediaDevices.selectAudioOutput(); + + const allDevices = await navigator.mediaDevices.enumerateDevices(); + const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput'); + is(audioDevices.length, 1, "Number of output devices found"); + + const audio = createMediaElement("audio", "audio"); + document.body.appendChild(audio); + + is(audio.sinkId, "", "Initial value is empty string"); + + const p = audio.setSinkId(audioDevices[0].deviceId); + is(audio.sinkId, "", "Value is unchanged upon function return"); + is(await p, undefined, "promise resolves with undefined"); + is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`); + + await audio.setSinkId(audioDevices[0].deviceId); + ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`); + + try { + await audio.setSinkId("dummy sink id"); + ok(false, "Never enter here, this must fail"); + } catch (error) { + ok(true, `Set sink id expected to fail: ${error}`); + is(error.name, "NotFoundError", "Verify correct error"); + } + + const {usage: usage1} = + await collectMemoryUsage(memoryReportPath); // Provided by head.js + + ok(usage1 > 0, "MediaManager memory usage should be non-zero to store \ +device ids after enumerateDevices"); + + const p2 = audio.setSinkId(""); + is(audio.sinkId, audioDevices[0].deviceId, + 'sinkId after setSinkId("") return'); + is(await p2, undefined, + "promise resolution value when sinkId parameter is empty"); + is(audio.sinkId, "", 'sinkId after setSinkId("") resolution'); + + await audio.setSinkId(audioDevices[0].deviceId); + + const {usage: usage2, reportCount} = + await collectMemoryUsage(memoryReportPath); + is(reportCount, 1, + 'Expect only one MediaManager to report in content processes.'); + is(usage2, usage1, "MediaManager memory usage should return to previous \ +value after promise resolution"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html new file mode 100644 index 0000000000..64db4cad7c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> + +<script> + createHTML({ + title: "HTMLMediaElement.setSinkId with default device and adding a track", + bug: "1661649", + }); + + /** + * Run a test to verify set sink id in audio element. + */ + runTest(async () => { + await pushPrefs(["media.setsinkid.enabled", true]); + + // Expose an audio output device. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await navigator.mediaDevices.selectAudioOutput(); + + const allDevices = await navigator.mediaDevices.enumerateDevices(); + const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput'); + info(`Found ${audioDevices.length} output devices`); + isnot(audioDevices.length, 0, "Found output devices"); + + const audio = createMediaElement("audio", "audio"); + document.body.appendChild(audio); + + audio.srcObject = await navigator.mediaDevices.getUserMedia({audio: true}); + audio.play(); + + await audio.setSinkId(audioDevices[0].deviceId); + await audio.setSinkId(""); + is(audio.sinkId, "", "sinkId restored to default"); + + audio.srcObject.addTrack((await navigator.mediaDevices.getUserMedia({audio: true})).getTracks()[0]); + + await wait(0); + + for (let t of audio.srcObject.getTracks()) { + t.stop(); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html new file mode 100644 index 0000000000..ceb4d82b50 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> +createHTML({ + title: "Test changing sink and muting before the MediaStream is set", + bug: "1651049", + visible: true +}); + +let getOutputDeviceId = async () => { + let devices = await navigator.mediaDevices.enumerateDevices(); + let audios = devices.filter(d => d.kind == "audiooutput"); + ok(audios.length, "One or more output devices found."); + return audios[0].deviceId; +} + +let verifyAudioTone = async (ac, stream, freq) => { + const toneAnalyser = new AudioStreamAnalyser(ac, stream); + return toneAnalyser.waitForAnalysisSuccess(array => { + const lowerFreq = freq / 2; + const upperFreq = freq + 1000; + const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)]; + const freqMag = array[toneAnalyser.binIndexForFrequency(freq)]; + const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)]; + info("Audio tone expected. " + + lowerFreq + ": " + lowerMag + ", " + + freq + ": " + freqMag + ", " + + upperFreq + ": " + upperMag); + return lowerMag < 50 && freqMag > 200 && upperMag < 50; + }); +} + +let verifyNoAudioTone = async (ac, stream, freq) => { + const toneAnalyser = new AudioStreamAnalyser(ac, stream); + // repeat check 100 times to make sure that it is muted. + let retryCnt = 0; + return toneAnalyser.waitForAnalysisSuccess(array => { + const lowerFreq = freq / 2; + const upperFreq = freq + 1000; + const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)]; + const freqMag = array[toneAnalyser.binIndexForFrequency(freq)]; + const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)]; + info("No audio tone expected. " + + lowerFreq + ": " + lowerMag + ", " + + freq + ": " + freqMag + ", " + + upperFreq + ": " + upperMag); + return lowerMag == 0 && freqMag == 0 && upperMag == 0 && ++retryCnt == 100; + }); +} + +runTest(async () => { + let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", ""); + if (!audioDevice) { + todo(false, "No loopback device set by framework. Try --use-test-media-devices"); + return; + } + + await pushPrefs(["media.setsinkid.enabled", true]); + + // Implicitly expose the loopback device sink by opening the source in the + // same group. + const verifyStream = await getUserMedia({audio: true}); + + let sinkId = await getOutputDeviceId(); + isnot(sinkId, "", "SinkId is not null"); + + let audioElement = createMediaElement('audio', 'audioElement'); + audioElement.muted = true; + await audioElement.setSinkId(sinkId); + isnot(audioElement.sinkId, "", "sinkId property of the element is not null"); + + // The test stream is a sine tone of 1000 Hz + let ac = new AudioContext(); + const frequency = 2000; + let stream = createOscillatorStream(ac, frequency); + await verifyAudioTone(ac, stream, frequency); + + audioElement.srcObject = stream; + audioElement.play(); + + // Verify the silent output using the loopback device. + await verifyNoAudioTone(ac, verifyStream, frequency); + info("output is muted"); + + // Clean up + audioElement.pause(); + audioElement.srcObject = null; + verifyStream.getTracks()[0].stop(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html new file mode 100644 index 0000000000..22df020f7c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="mediaStreamPlayback.js"></script> +</head> +<body> +<script> +"use strict"; + +createHTML({ + // This pref exists only for a partner testing framework without WebDriver + // switch-to-window nor SpecialPowers to set the active window. + // Prefer "focusmanager.testmode". + title: "Test media.devices.unfocused.enabled", + bug: "1740824" +}); + +const blank_url = "/tests/docshell/test/navigation/blank.html"; + +async function resolveOnEvent(target, name) { + return new Promise(r => target.addEventListener(name, r, {once: true})); +} + +runTest(async () => { + ok(document.hasFocus(), "This test expects initial focus on the document."); + // 'resizable' is requested for a separate OS window on relevant platforms + // so that this test tests OS focus changes rather than document visibility. + const other = window.open(blank_url, "", "resizable"); + SimpleTest.registerCleanupFunction(() => { + other.close(); + return SimpleTest.promiseFocus(window); + }); + await Promise.all([ + resolveOnEvent(window, 'blur'), + SimpleTest.promiseFocus(other), + pushPrefs(["media.devices.unfocused.enabled", true]), + ]); + ok(!document.hasFocus(), "!document.hasFocus()"); + await navigator.mediaDevices.enumerateDevices(); + ok(true, "enumerateDevices() completes without focus."); + // The focus requirement with media.devices.unfocused.enabled false + // (default) is tested in + // testing/web-platform/mozilla/tests/mediacapture-streams/enumerateDevices-without-focus.https.html +}); + +</script> +</body> +</html> diff --git a/dom/media/webrtc/tests/mochitests/turnConfig.js b/dom/media/webrtc/tests/mochitests/turnConfig.js new file mode 100644 index 0000000000..1267de4ec5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/turnConfig.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* An example of how to specify two TURN server configs: + * + * Note: If turn URL uses FQDN rather then an IP address the TURN relay + * verification step in checkStatsIceConnectionType might fail. + * + * var turnServers = { + * local: { iceServers: [{"username":"mozilla","credential":"mozilla","url":"turn:10.0.0.1"}] }, + * remote: { iceServers: [{"username":"firefox","credential":"firefox","url":"turn:10.0.0.2"}] } + * }; + */ + +var turnServers = {}; |