diff options
Diffstat (limited to 'dom/media/webrtc/tests/mochitests')
246 files changed, 21768 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..271a422881 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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..20018f4119 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js @@ -0,0 +1,30 @@ +/* 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 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, + Ci.nsICertOverrideService.ERROR_UNTRUSTED, + 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..bb16e4d78e --- /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..b6230e440f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/head.js @@ -0,0 +1,1283 @@ +/* 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(); + +/** + * Global flag to skip LoopbackTone + */ +let DISABLE_LOOPBACK_TONE = false; +/** + * Helper class to setup a sine tone of a given frequency. + */ +class LoopbackTone { + constructor(audioContext, frequency) { + if (!audioContext) { + throw new Error("You must provide a valid AudioContext"); + } + this.oscNode = audioContext.createOscillator(); + var gainNode = audioContext.createGain(); + gainNode.gain.value = 0.5; + this.oscNode.connect(gainNode); + gainNode.connect(audioContext.destination); + this.changeFrequency(frequency); + } + + // Method should be used when 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 ( + 1 + + 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 - 1) * 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) { + // Tests may have changed the values of prefs, so recheck + updateConfigFromFakeAndLoopbackPrefs(); + if ( + !WANT_FAKE_AUDIO && + !constraints.fake && + constraints.audio && + !DISABLE_LOOPBACK_TONE + ) { + // Loopback device is configured, start the default loopback tone + if (!DefaultLoopbackTone) { + TEST_AUDIO_FREQ = 440; + DefaultLoopbackTone = new LoopbackTone( + new AudioContext(), + TEST_AUDIO_FREQ + ); + DefaultLoopbackTone.start(); + } + // Disable input processing mode when it's not explicity enabled. + // This is to avoid distortion of the loopback tone + constraints.audio = Object.assign( + {}, + { autoGainControl: false }, + { echoCancellation: false }, + { noiseSuppression: false }, + constraints.audio + ); + } else { + // Fake device configured, ensure our test freq is correct. + TEST_AUDIO_FREQ = 1000; + } + info("Call getUserMedia for " + JSON.stringify(constraints)); + return navigator.mediaDevices + .getUserMedia(constraints) + .then(stream => (checkMediaStreamTracks(constraints, stream), stream)); +} + +// These are the promises we use to track that the prerequisites for the test +// are in place before running it. +var setTestOptions; +var testConfigured = new Promise(r => (setTestOptions = r)); + +function pushPrefs(...p) { + return SpecialPowers.pushPrefEnv({ set: p }); +} + +function setupEnvironment() { + var defaultMochitestPrefs = { + set: [ + ["media.peerconnection.enabled", true], + ["media.peerconnection.identity.enabled", true], + ["media.peerconnection.identity.timeout", 120000], + ["media.peerconnection.ice.stun_client_maximum_transmits", 14], + ["media.peerconnection.ice.trickle_grace_period", 30000], + ["media.peerconnection.rtpsourcesapi.enabled", true], + ["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], + ], + }; + + const isAndroid = !!navigator.userAgent.includes("Android"); + + if (isAndroid) { + defaultMochitestPrefs.set.push( + ["media.navigator.video.default_width", 320], + ["media.navigator.video.default_height", 240], + ["media.navigator.video.max_fr", 10], + ["media.autoplay.default", Ci.nsIAutoplay.ALLOWED] + ); + } else { + // For platforms other than Android, the tests use Fake H.264 GMP encoder. + // We can't use that with a real decoder until bug 1509012 is done. + // So force using the Fake H.264 GMP decoder for now. + defaultMochitestPrefs.set.push([ + "media.navigator.mediadatadecoder_h264_enabled", + false, + ]); + } + + // 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(); +} + +function runTestWhenReady(testFunc) { + setupEnvironment(); + return testConfigured + .then(options => testFunc(options)) + .catch(e => { + ok( + false, + "Error executing test: " + + e + + (typeof e.stack === "string" + ? " " + e.stack.split("\n").join(" ... ") + : "") + ); + 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), 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) + ); +} + +/** + * 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 AudioStreamHelper() { + this._context = new AudioContext(); +} + +AudioStreamHelper.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); + }, + + 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/identity/identityPcTest.js b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js new file mode 100644 index 0000000000..158eb14fce --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js @@ -0,0 +1,79 @@ +function identityPcTest(remoteOptions) { + var user = "someone"; + var domain1 = "test1.example.com"; + var domain2 = "test2.example.com"; + var id1 = user + "@" + domain1; + var id2 = user + "@" + domain2; + + 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), + ]); + }, + ]); + test.run(); +} 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..6a45bf044a --- /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..240b98bbda --- /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..240b98bbda --- /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..240b98bbda --- /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..240b98bbda --- /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..240b98bbda --- /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..d779f7fa5e --- /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..b6313297bc --- /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.ini b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini new file mode 100644 index 0000000000..0beb50561e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini @@ -0,0 +1,48 @@ +[DEFAULT] +subsuite = media +skip-if = (os == 'linux' && !debug && !e10s) +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_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_fingerprints.html] +scheme=https +[test_getIdentityAssertion.html] +[test_setIdentityProvider.html] +scheme=https +[test_setIdentityProviderWithErrors.html] +scheme=https +[test_peerConnection_peerIdentity.html] +scheme=https +skip-if = os == 'android' +[test_peerConnection_asymmetricIsolation.html] +scheme=https +skip-if = os == 'android' +[test_loginNeeded.html] +support-files = + /.well-known/idp-proxy/login.html + /.well-known/idp-proxy/idp.sjs 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..5a93465513 --- /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 {Cu} = SpecialPowers; + const rtcid = Cu.import('resource://gre/modules/media/IdpSandbox.jsm'); + const sandbox = new rtcid.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(); + networkTestFinished(); +}); +</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..0c16604b67 --- /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')); + } + ]); + 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..d18142ae56 --- /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 Cu = SpecialPowers.Cu; +var rtcid = Cu.import("resource://gre/modules/media/IdpSandbox.jsm"); +var IdpSandbox = rtcid.IdpSandbox; +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..c1dba507c6 --- /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'); + }); + } + ]); + 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..5ea0f5c779 --- /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. + 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..4cb8bfee20 --- /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"); + } + ]); + 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..86469d9e73 --- /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')) + ]); + } + ]); + + 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..5bf39cb2f6 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js @@ -0,0 +1,214 @@ +/* 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(), 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() {} + +var scriptsReady = Promise.all( + ["/tests/SimpleTest/SimpleTest.js", "head.js"].map(script => { + var 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)); +} + +// noGum - Helper to detect whether active guM tracks still exist. +// +// It relies on the fact that, by spec, device labels from enumerateDevices are +// only visible during active gum calls. They're also visible when persistent +// permissions are granted, so turn off media.navigator.permission.disabled +// (which is normally on otherwise in our tests). Lastly, we must turn on +// media.navigator.permission.fake otherwise fake devices don't count as active. + +var noGum = () => + pushPrefs( + ["media.navigator.permission.disabled", false], + ["media.navigator.permission.fake", true], + ["media.devices.insecure.enabled", true] + ) + .then(() => navigator.mediaDevices.enumerateDevices()) + .then( + ([device]) => + device && + is(device.label, "", "Test must leave no active gUM streams behind.") + ); + +var runTest = testFunction => + scriptsReady + .then(() => runTestWhenReady(testFunction)) + .then(() => noGum()) + .then(() => finish()); diff --git a/dom/media/webrtc/tests/mochitests/mochitest.ini b/dom/media/webrtc/tests/mochitests/mochitest.ini new file mode 100644 index 0000000000..b4a3b51a8f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/mochitest.ini @@ -0,0 +1,319 @@ +[DEFAULT] +tags = mtg webrtc +subsuite = media +scheme = https +support-files = + head.js + dataChannel.js + mediaStreamPlayback.js + network.js + nonTrickleIce.js + pc.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 + !/dom/canvas/test/captureStream_common.js + !/dom/canvas/test/webgl-mochitest/webgl-util.js + !/dom/media/test/manifest.js + !/dom/media/test/320x240.ogv + !/dom/media/test/r11025_s16_c1.wav + !/dom/media/test/bug461281.ogg + !/dom/media/test/seek.webm + !/dom/media/test/gizmo.mp4 +prefs = + privacy.partition.network_state=false + network.proxy.allow_hijacking_localhost=true + +[test_1488832.html] +[test_a_noOp.html] +scheme=http +[test_dataChannel_basicAudio.html] +[test_dataChannel_basicAudioVideo.html] +[test_dataChannel_basicAudioVideoNoBundle.html] +[test_dataChannel_basicAudioVideoCombined.html] +[test_dataChannel_basicDataOnly.html] +[test_dataChannel_hostnameObfuscation.html] +scheme=http +[test_dataChannel_stats.html] +skip-if = toolkit == 'android' # no hostname obfuscation on android +scheme=http +[test_dataChannel_basicVideo.html] +[test_dataChannel_bug1013809.html] +[test_dataChannel_dataOnlyBufferedAmountLow.html] +scheme=http +[test_dataChannel_dtlsVersions.html] +[test_dataChannel_noOffer.html] +scheme=http +[test_enumerateDevices.html] +[test_enumerateDevices_navigation.html] +skip-if = true # Disabled because it is a racy test and causes timeouts, see bug 1650932 +[test_groupId.html] +[test_setSinkId.html] +skip-if = os != 'linux' # the only platform with real devices +[test_setSinkId_default_addTrack.html] +skip-if = os != 'linux' # the only platform with real devices +[test_setSinkId_preMutedElement.html] +[test_ondevicechange.html] +run-sequentially = sets prefs that may disrupt other tests +[test_getUserMedia_active_autoplay.html] +[test_getUserMedia_audioCapture.html] +skip-if = toolkit == 'android' || (os == "win" && processor == "aarch64") # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333), aarch64 due to 1538359 +[test_getUserMedia_addTrackRemoveTrack.html] +[test_getUserMedia_addtrack_removetrack_events.html] +[test_getUserMedia_audioConstraints.html] +skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms +[test_getUserMedia_audioConstraints_concurrentIframes.html] +skip-if = os == 'mac' || os == 'win' || toolkit == 'android' || (os == 'linux' && debug) # Bug 1404995, no loopback devices on some platforms # Bug 1481101 +[test_getUserMedia_audioConstraints_concurrentStreams.html] +skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms +[test_getUserMedia_basicAudio_loopback.html] +skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms +[test_defaultAudioConstraints.html] +skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms +[test_getUserMedia_basicAudio.html] +[test_getUserMedia_basicVideo.html] +[test_getUserMedia_basicVideo_playAfterLoadedmetadata.html] +[test_getUserMedia_basicScreenshare.html] +skip-if = toolkit == 'android' || (os == 'win' && webrender) # no screenshare on android, see bug 1504162 for webrender on windows +[test_getUserMedia_basicTabshare.html] +skip-if = + toolkit == 'android' # no windowshare on android + os == 'win' && os_version == '10.0' && bits == 64 # Bug 1678060 +[test_getUserMedia_basicWindowshare.html] +skip-if = toolkit == 'android' # no windowshare on android +[test_getUserMedia_basicVideoAudio.html] +[test_getUserMedia_bug1223696.html] +[test_getUserMedia_constraints.html] +[test_getUserMedia_callbacks.html] +[test_getUserMedia_cubebDisabled.html] +[test_getUserMedia_cubebDisabledFakeStreams.html] +[test_getUserMedia_GC_MediaStream.html] +[test_getUserMedia_getTrackById.html] +skip-if = (verify && debug && (os == 'linux')) +[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_permission.html] +[test_getUserMedia_playAudioTwice.html] +[test_getUserMedia_playVideoAudioTwice.html] +[test_getUserMedia_playVideoTwice.html] +[test_getUserMedia_scarySources.html] +skip-if = toolkit == 'android' # no screenshare or windowshare on android +[test_getUserMedia_spinEventLoop.html] +[test_getUserMedia_trackCloneCleanup.html] +[test_getUserMedia_trackEnded.html] +[test_getUserMedia_peerIdentity.html] +[test_peerConnection_addtrack_removetrack_events.html] +[test_peerConnection_audioCodecs.html] +[test_peerConnection_basicAudio.html] +[test_peerConnection_basicAudio_forced_lower_rate.html] +[test_peerConnection_basicAudio_forced_higher_rate.html] +[test_peerConnection_audioSynchronizationSources.html] +[test_peerConnection_audioSynchronizationSourcesUnidirectional.html] +[test_peerConnection_audioContributingSources.html] +[test_peerConnection_checkPacketDumpHook.html] +[test_peerConnection_basicAudioNATSrflx.html] +skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217) +scheme=http +[test_peerConnection_basicAudioNATRelay.html] +skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217) +scheme=http +[test_peerConnection_basicAudioNATRelayTCP.html] +skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217) +scheme=http +[test_peerConnection_basicAudioNoisyUDPBlock.html] +skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217) +scheme=http +[test_peerConnection_basicAudioNATRelayTLS.html] +skip-if = true # need pyopenssl on builders, see bug 1323439 # toolkit == 'android' # websockets don't work on android (bug 1266217) +scheme=http +[test_peerConnection_basicAudioRequireEOC.html] +[test_peerConnection_basicAudioPcmaPcmuOnly.html] +[test_peerConnection_basicAudioDynamicPtMissingRtpmap.html] +[test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html] +[test_peerConnection_basicAudioVideo.html] +[test_peerConnection_basicAudioVideoCombined.html] +[test_peerConnection_basicAudioVideoVerifyExtmap.html] +[test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html] +[test_peerConnection_basicAudioVideoNoBundle.html] +[test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html] +[test_peerConnection_basicAudioVideoNoRtcpMux.html] +[test_peerConnection_basicAudioVideoTransceivers.html] +[test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html] +[test_peerConnection_basicVideo.html] +[test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html] +[test_peerConnection_basicScreenshare.html] +# frequent timeouts/crashes on e10s (bug 1048455) +skip-if = toolkit == 'android' # no screenshare on android +[test_peerConnection_basicWindowshare.html] +# frequent timeouts/crashes on e10s (bug 1048455) +skip-if = toolkit == 'android' # no screenshare on android +[test_peerConnection_basicH264Video.html] +skip-if = (toolkit == 'android' && is_emulator) # Bug 1355786, No h264 support on android emulator +[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 = (toolkit == 'android' && is_emulator) # Bug 1355786, No h264 support on android emulator +[test_peerConnection_capturedVideo.html] +tags=capturestream +skip-if = toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator), Bug 1264340 +[test_peerConnection_captureStream_canvas_2d.html] +scheme=http +[test_peerConnection_captureStream_canvas_2d_noSSRC.html] +scheme=http +[test_peerConnection_multiple_captureStream_canvas_2d.html] +scheme=http +[test_peerConnection_captureStream_canvas_webgl.html] +# Cross process WebGL doesn't seem to work in the emulator +skip-if = (toolkit == 'android' && e10s && is_emulator) +scheme=http +[test_peerConnection_certificates.html] +disabled=bug 1180968 +scheme=http +[test_peerConnection_close.html] +scheme=http +[test_peerConnection_closeDuringIce.html] +[test_peerConnection_constructedStream.html] +[test_peerConnection_disabledVideoPreNegotiation.html] +[test_peerConnection_errorCallbacks.html] +scheme=http +[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_forwarding_basicAudioVideoCombined.html] +skip-if = toolkit == 'android' # Bug 1189784 +[test_peerConnection_maxFsConstraint.html] +[test_peerConnection_noTrickleAnswer.html] +[test_peerConnection_noTrickleOffer.html] +[test_peerConnection_noTrickleOfferAnswer.html] +[test_peerConnection_offerRequiresReceiveAudio.html] +[test_peerConnection_offerRequiresReceiveVideo.html] +[test_peerConnection_offerRequiresReceiveVideoAudio.html] +[test_peerConnection_promiseSendOnly.html] +[test_peerConnection_recordReceiveTrack.html] +[test_peerConnection_renderAfterRenegotiation.html] +scheme=http +[test_peerConnection_restartIce.html] +[test_peerConnection_restartIceNoBundle.html] +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538440 +[test_peerConnection_restartIceNoBundleNoRtcpMux.html] +[test_peerConnection_restartIceNoRtcpMux.html] +[test_peerConnection_restartIceLocalRollback.html] +[test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html] +[test_peerConnection_restartIceLocalAndRemoteRollback.html] +[test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html] +[test_peerConnection_restartIceBadAnswer.html] +[test_peerConnection_scaleResolution.html] +[test_peerConnection_simulcastOffer.html] +skip-if = toolkit == 'android' # no simulcast support on android +[test_peerConnection_simulcastOffer_lowResFirst.html] +skip-if = toolkit == 'android' # no simulcast support on android +[test_peerConnection_simulcastAnswer.html] +skip-if = toolkit == 'android' # no simulcast support on android +[test_peerConnection_simulcastAnswer_lowResFirst.html] +skip-if = toolkit == 'android' # no simulcast support on android +[test_peerConnection_simulcastOddResolution.html] +skip-if = toolkit == 'android' # no simulcast support on android +[test_peerConnection_relayOnly.html] +disabled=bug 1612063 # test is racy +[test_peerConnection_callbacks.html] +[test_peerConnection_replaceTrack.html] +[test_peerConnection_replaceTrack_disabled.html] +skip-if = toolkit == 'android' # Bug 1614460 +[test_peerConnection_syncSetDescription.html] +[test_peerConnection_setLocalAnswerInHaveLocalOffer.html] +[test_peerConnection_setLocalAnswerInStable.html] +[test_peerConnection_setLocalOfferInHaveRemoteOffer.html] +[test_peerConnection_setParameters.html] +[test_peerConnection_setParameters_scaleResolutionDownBy.html] +skip-if = (os == 'win' && processor == 'aarch64') # aarch64 due to bug 1537567 +[test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html] +[test_peerConnection_setRemoteAnswerInStable.html] +[test_peerConnection_setRemoteOfferInHaveLocalOffer.html] +[test_peerConnection_throwInCallbacks.html] +[test_peerConnection_toJSON.html] +scheme=http +[test_peerConnection_trackDisabling_clones.html] +[test_peerConnection_trackDisabling.html] +skip-if = toolkit == 'android' # Bug 1614460 +[test_peerConnection_twoAudioStreams.html] +[test_peerConnection_twoAudioTracksInOneStream.html] +[test_peerConnection_twoAudioVideoStreams.html] +skip-if = (os == 'linux' && debug && e10s) # Bug 1171255 for Linux debug e10s +[test_peerConnection_twoAudioVideoStreamsCombined.html] +skip-if = (os == 'linux' && debug && e10s) || (toolkit == 'android') || (os == 'linux' && asan) # Bug 1127828 for Linux debug e10s, android(Bug 1189784), Bug 1480942 for Linux asan +[test_peerConnection_twoVideoStreams.html] +[test_peerConnection_twoVideoTracksInOneStream.html] +[test_peerConnection_addAudioTrackToExistingVideoStream.html] +[test_peerConnection_addSecondAudioStream.html] +[test_peerConnection_answererAddSecondAudioStream.html] +[test_peerConnection_removeAudioTrack.html] +[test_peerConnection_removeThenAddAudioTrack.html] +[test_peerConnection_addSecondVideoStream.html] +[test_peerConnection_restrictBandwidthTargetBitrate.html] +[test_peerConnection_restrictBandwidthWithTias.html] +[test_peerConnection_removeVideoTrack.html] +[test_peerConnection_removeThenAddVideoTrack.html] +[test_peerConnection_replaceVideoThenRenegotiate.html] +[test_peerConnection_addSecondAudioStreamNoBundle.html] +[test_peerConnection_removeThenAddAudioTrackNoBundle.html] +[test_peerConnection_addSecondVideoStreamNoBundle.html] +[test_peerConnection_removeThenAddVideoTrackNoBundle.html] +[test_peerConnection_addDataChannel.html] +[test_peerConnection_addDataChannelNoBundle.html] +[test_peerConnection_verifyAudioAfterRenegotiation.html] +[test_peerConnection_verifyVideoAfterRenegotiation.html] +[test_peerConnection_videoCodecs.html] +skip-if = toolkit == 'android' # android(Bug 1614460) +[test_peerConnection_audioRenegotiationInactiveAnswer.html] +[test_peerConnection_videoRenegotiationInactiveAnswer.html] +[test_peerConnection_webAudio.html] +tags = webaudio webrtc +scheme=http +[test_peerConnection_localRollback.html] +[test_peerConnection_localReofferRollback.html] +[test_peerConnection_remoteRollback.html] +[test_peerConnection_remoteReofferRollback.html] +[test_peerConnection_threeUnbundledConnections.html] +[test_selftest.html] +# Bug 1227781: Crash with bogus TURN server. +scheme=http +[test_peerConnection_bug1227781.html] +scheme=http +[test_peerConnection_stats.html] +[test_peerConnection_stats_jitter.html] +skip-if = tsan # Bug 1672590, TSan is just too slow to pass this test +[test_peerConnection_stats_relayProtocol.html] +skip-if = toolkit == 'android' || socketprocess_e10s # android(Bug 1189784, timeouts on 4.3 emulator, Bug 1373858, Bug 1521117) +scheme=http +[test_peerConnection_sender_and_receiver_stats.html] +[test_peerConnection_trackless_sender_stats.html] +[test_peerConnection_verifyDescriptions.html] +[test_fingerprinting_resistance.html] +[test_getUserMedia_nonDefaultRate.html] +[test_peerConnection_nonDefaultRate.html] +[test_forceSampleRate.html] +scheme=http +[test_peerConnection_bug1512281.html] +fail-if = 1 +[test_peerConnection_telephoneEventFirst.html] +[test_peerConnection_rtcp_rsize.html] diff --git a/dom/media/webrtc/tests/mochitests/network.js b/dom/media/webrtc/tests/mochitests/network.js new file mode 100644 index 0000000000..286ec5b71e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/network.js @@ -0,0 +1,20 @@ +/* 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 + * + */ +function startNetworkAndTest() { + return Promise.resolve(); +} + +/** + * A stub function to shutdown the network if needed + */ +function networkTestFinished() { + return Promise.resolve().then(() => finish()); +} diff --git a/dom/media/webrtc/tests/mochitests/nonTrickleIce.js b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js new file mode 100644 index 0000000000..b706fc3030 --- /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 mozRTCSessionDescription({ 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..31c5ee7f63 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/pc.js @@ -0,0 +1,2539 @@ +/* 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 = iceServersArray; + } + if (!options.turn_disabled_remote) { + 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("utf-8").encode(d).length; + } else { + ok(false); + } + }; + + 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 = function() { + /* We have to modify the chain here to allow tests which modify the default + * test chain instantiating a PeerConnectionTest() */ + this.updateChainSteps(); + var finished = () => { + if (window.SimpleTest) { + networkTestFinished(); + } else { + finish(); + } + }; + return this.chain + .execute() + .then(() => this.close()) + .catch(e => + ok( + false, + "Error in test execution: " + + e + + (typeof e.stack === "string" + ? " " + e.stack.split("\n").join(" ... ") + : "") + ) + ) + .then(() => finished()) + .catch(e => ok(false, "Error in finished()")); +}; + +/** + * 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; // replaceTrack on a null track, probably + } + info.track = withTrack; + this.addSendStream(stream); + this.ensureMediaElement(withTrack, "local"); + return sender.replaceTrack(withTrack); + }, + + async getUserMedia(constraints) { + 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; + } + return Promise.all(streams.map(stream => this.attachLocalStream(stream))); + }, + + async getAllUserMediaAndAddTransceivers(constraintsList) { + var streams = await this.getAllUserMedia(constraintsList); + if (!streams) { + return; + } + 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 > 0, "SDP mid not empty"); + ok( + anEvent.candidate.usernameFragment.length > 0, + "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) + ), + ]); + }, + + async waitForSyncedRtcp() { + // Ensures that RTCP is present + let ensureSyncedRtcp = async () => { + let report = await this._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; + }; + let attempts = 0; + // 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) { + 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" + ); + }, + + /** + * 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. + */ + getStats(selector) { + return this._pc.getStats(selector).then(stats => { + let dict = {}; + for (const [k, v] of stats.entries()) { + dict[k] = v; + } + info(this + ": Got stats: " + JSON.stringify(dict)); + this._last_stats = stats; + 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"); + ok(rem.packetsLost !== undefined, "Rtcp packetsLost"); + 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 + ); + } + } + ok(rem.jitter !== undefined, "Rtcp jitter"); + 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 (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. +var scriptsReady = loadScript("/tests/SimpleTest/SimpleTest.js").then(() => { + return loadScript( + "../../../test/manifest.js", + "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.Cu.import( + "resource://gre/modules/AppConstants.jsm", + {} + ); + let isNightly = AppConstants.NIGHTLY_BUILD; + let isAndroid = AppConstants.platform == "android"; + + await scriptsReady; + await runTestWhenReady(async options => { + await startNetworkAndTest(); + await setupIceServerConfig(fixtureOptions.useIceServer); + + // currently we set android hardware encoder default enabled in nightly. + // But before QA approves the quality, we want to ensure the legacy + // encoder is working fine. + if (isNightly && isAndroid) { + let value = Math.random() >= 0.5; + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.navigator.hardware.vp8_encode.acceleration_enabled", value], + [ + "media.navigator.hardware.vp8_encode.acceleration_remote_enabled", + value, + ], + ], + }); + } + await testFunction(options); + }); +} 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..87d7e1bbd5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js @@ -0,0 +1,43 @@ +// 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. +function test_peerconnection_audio_forced_sample_rate(forcedSampleRate) { + scriptsReady.then(function() { + pushPrefs(["media.cubeb.force_sample_rate", forcedSampleRate]).then( + function() { + runNetworkTest(function(options) { + let test = new PeerConnectionTest(options); + let ac = new AudioContext(); + test.setMediaConstraints( + [ + { + audio: true, + }, + ], + [] + ); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_WEBAUDIO_SOURCE(test) { + let oscillator = ac.createOscillator(); + oscillator.type = "sine"; + oscillator.frequency.value = 700; + oscillator.start(); + let 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); + }, + ]); + 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..f2307ea65f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/sdpUtils.js @@ -0,0 +1,388 @@ +/* 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]; + }, + + // 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 > 0, + "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/templates.js b/dom/media/webrtc/tests/mochitests/templates.js new file mode 100644 index 0000000000..dcb0cfab4b --- /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 > 0) { + 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 > 0, + 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_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..411c032ed9 --- /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}]); + 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..c012c9e3b6 --- /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}]); + 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..14e38e222c --- /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}]); + 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..dc8725f5ad --- /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}]); + 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..83aa6edc1b --- /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); + 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..becee67764 --- /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}]); + 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..b74f9decaa --- /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}]); + 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..a38742abc0 --- /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); + 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..3bd08f5315 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.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: "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}]); + + // This inlines test.run(), to allow for multiple tests to run. + test.updateChainSteps(); + await test.chain.execute(); + await test.close(); + } + + 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); + } + networkTestFinished(); + }); +</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..2523b18abf --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.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: "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"); + } + } + }); + }]); + + // This inlines test.run(), to allow for multiple tests to run. + test.updateChainSteps(); + await test.chain.execute(); + await test.close(); + } + + 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); + + networkTestFinished(); + }); + +</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..1b14910c1d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.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: "856319", + title: "Don't offer m=application unless createDataChannel is called first" + }); + + runNetworkTest(function () { + var pc = new RTCPeerConnection(); + + // necessary to circumvent bug 864109 + var options = { offerToReceiveAudio: true }; + + pc.createOffer(options).then(offer => { + ok(!offer.sdp.includes("m=application"), + "m=application is not contained in the SDP"); + + networkTestFinished(); + }) + .catch(generateErrorCallback()); + }); + +</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..63eba719dd --- /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'); + }); + 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..0114dca3ae --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html @@ -0,0 +1,132 @@ +<!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 mustSucceed(msg, f) { + try { + await f(); + 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."); + } + } +} + +var gUM = c => navigator.mediaDevices.getUserMedia(c); + +var validateDevice = ({kind, label, deviceId, groupId}) => { + ok(kind == "videoinput" || kind == "audioinput", "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. + + let devices = await navigator.mediaDevices.enumerateDevices(); + ok(devices.length > 0, "At least one device found"); + let 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 (let device of devices) { + validateDevice(device); + // Test deviceId constraint + let deviceId = device.deviceId; + let constraints = (device.kind == "videoinput") ? { video: { deviceId } } + : { audio: { deviceId } }; + for (let 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 mustSucceed("unknown plain deviceId on video", + () => gUM({ video: { deviceId: unknownId } })); + await mustSucceed("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); + + let haveDevicesMap = new Promise(resolve => { + let 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 => { + let iframe = document.createElement("iframe"); + iframe.src = origin + path; + iframe.allow = "camera;microphone"; + 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); + is(differentOriginDevices.length, devices.length); + [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d)); + + for (let device of sameOriginDevices) { + ok(devices.find(d => d.deviceId == device.deviceId), + "Same origin deviceId for " + device.label + " must match"); + } + for (let 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(); + ok(devices.length === 0, "No devices found"); +}); +</script> +</pre> +</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..a893ed3b95 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<body> +<pre id="test"> +<script type="application/javascript"> +/** + Runs inside iframe in test_enumerateDevices.html. +*/ + +var pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p}); +var gUM = c => navigator.mediaDevices.getUserMedia(c); + +(async () => { + await pushPrefs(["media.navigator.streams.fake", true]); + + let 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_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..5aa0e64947 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html @@ -0,0 +1,59 @@ +<!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 () => { + // We do not need LoopbackTone because it is not used + // and creates extra streams that affect the result + DISABLE_LOOPBACK_TONE = true; + + let gUMStream = await getUserMedia({video: true}); + info("Testing GC of 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..833653ebb2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html @@ -0,0 +1,110 @@ +<!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), 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..65410e404e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioCapture </title> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> +<script> + +createHTML({ + bug: "1156472", + title: "Test AudioCapture with regular HTMLMediaElement, AudioContext, and HTMLMediaElement playing a MediaStream", + visible: true +}); + +// Get an opus file containing a sine wave at maximum amplitude, of duration +// `lengthSeconds`, and of frequency `frequency`. +async function getSineWaveFile(frequency, lengthSeconds) { + const off = new OfflineAudioContext(1, lengthSeconds * 48000, 48000); + const osc = off.createOscillator(); + const rec = new MediaRecorder(off.destination, + {mimeType: "audio/ogg; codecs=opus"}); + osc.frequency.value = frequency; + osc.connect(off.destination); + osc.start(); + rec.start(); + off.startRendering(); + const {data} = await new Promise(r => rec.ondataavailable = r); + return data; +} + +(async function() { + await scriptsReady; + FAKE_ENABLED = false; + await runTestWhenReady(async function() { + /** + * 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 wavtone = createMediaElement("audio", "WaveTone"); + const acTone = createMediaElement("audio", "audioContextTone"); + const ac = new AudioContext(); + + const oscThroughMediaElement = ac.createOscillator(); + oscThroughMediaElement.frequency.value = 1000; + const oscThroughAudioDestinationNode = ac.createOscillator(); + oscThroughAudioDestinationNode.frequency.value = 5000; + const msDest = ac.createMediaStreamDestination(); + + oscThroughMediaElement.connect(msDest); + oscThroughAudioDestinationNode.connect(ac.destination); + + acTone.srcObject = msDest.stream; + + const blob = await getSineWaveFile(10000, 10); + wavtone.src = URL.createObjectURL(blob); + oscThroughMediaElement.start(); + oscThroughAudioDestinationNode.start(); + wavtone.loop = true; + wavtone.play(); + acTone.play(); + + const constraints = {audio: {mediaSource: "audioCapture"}}; + + const stream = await getUserMedia(constraints); + window.grip = stream; + const analyser = new AudioStreamAnalyser(ac, stream); + analyser.enableDebugCanvas(); + await analyser.waitForAnalysisSuccess(array => { + // We want to find three frequency components here, around 1000, 5000 + // and 10000Hz. Frequency are logarithmic. Also make sure we have low + // energy in between, not just a flat white noise. + return (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); + }); + finish(); + }); +})(); + + + +</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..d07dbc41f1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html @@ -0,0 +1,157 @@ +<!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)); + + // Start a tone at our top level page so the gUM calls will record something + // should we wish to verify their recording in future. + let tone = new LoopbackTone(new AudioContext, TEST_AUDIO_FREQ); + tone.start(); + + // 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); + } + + tone.stop(); + } + 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..2dd3bdb502 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> + +<script> + createHTML({ + title: "getUserMedia Basic Audio Test Loopback", + bug: "1406350", + visible: true + }); + /** + * Run a test to verify the use of LoopbackTone as audio input. + */ + scriptsReady.then(() => runTestWhenReady(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 Promise.resolve(); + } + + // At this point DefaultLoopbackTone has been instantiated + // automatically on frequency TEST_AUDIO_FREQ (440 Hz). Verify + // that a tone is detected on that frequency. + info("Capturing at default frequency"); + let stream = await getUserMedia({audio: true}); + + let audioContext = new AudioContext(); + let analyser = new AudioStreamAnalyser(audioContext, stream); + analyser.enableDebugCanvas(); + await analyser.waitForAnalysisSuccess( array => { + // High energy on 1000 Hz low energy around that + const freg_50Hz = array[analyser.binIndexForFrequency(50)]; + const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)]; + const freq_2000Hz = array[analyser.binIndexForFrequency(2000)]; + + info("Analysing audio frequency - low:target:high = " + + freg_50Hz + ':' + freq + ':' + freq_2000Hz); + return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50; + }) + + // Use the LoopbackTone API to change the frequency of the default tone. + // Verify that a tone is detected on the new frequency (800 Hz). + info("Change loopback tone frequency"); + DefaultLoopbackTone.changeFrequency(800); + await analyser.waitForAnalysisSuccess( array => { + const freg_50Hz = array[analyser.binIndexForFrequency(50)]; + const freq = array[analyser.binIndexForFrequency(800)]; + const freq_2000Hz = array[analyser.binIndexForFrequency(2000)]; + + info("Analysing audio frequency - low:target:high = " + + freg_50Hz + ':' + freq + ':' + freq_2000Hz); + return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50; + }) + + // Create a second tone at a different frequency. + // Verify that both tones are detected. + info("Multiple loopback tones"); + DefaultLoopbackTone.changeFrequency(TEST_AUDIO_FREQ); + let second_tone = new LoopbackTone(audioContext, 2000); + second_tone.start(); + await analyser.waitForAnalysisSuccess( array => { + const freg_50Hz = array[analyser.binIndexForFrequency(50)]; + const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)]; + const freq_2000Hz = array[analyser.binIndexForFrequency(2000)]; + const freq_4000Hz = array[analyser.binIndexForFrequency(4000)]; + + info("Analysing audio frequency - low:target1:target2:high = " + + freg_50Hz + ':' + freq + ':' + freq_2000Hz + ':' + freq_4000Hz); + return freg_50Hz < 50 && freq > 200 && freq_2000Hz > 200 && freq_4000Hz < 50; + }) + + // Stop all tones and verify that there is no audio on the given frequencies. + info("Stop all loopback tones"); + DefaultLoopbackTone.stop(); + second_tone.stop() + await analyser.waitForAnalysisSuccess( array => { + const freg_50Hz = array[analyser.binIndexForFrequency(50)]; + const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)]; + const freq_2000Hz = array[analyser.binIndexForFrequency(2000)]; + + info("Analysing audio frequency - low:target:high = " + + freg_50Hz + ':' + freq + ':' + freq_2000Hz); + return freg_50Hz < 50 && freq < 50 && freq_2000Hz < 50; + }) + })) + .then(() => finish()) +</script> +</pre> +</body> +</html> 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..00ffc74f88 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html @@ -0,0 +1,216 @@ +<!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, + }); + + let verifyScreenshare = async (video, helper, upleft, upright, downleft, downright) => { + if (video.readyState < video.HAVE_CURRENT_DATA) { + info("Waiting for data"); + await new Promise(r => video.addEventListener("loadeddata", r, {once: true})); + } + + // We assume video size will not change. Offsets help to account for a + // square fullscreen-canvas, while the screen is rectangular. + let offsetX = Math.max(0, video.videoWidth - video.videoHeight) / 2; + let offsetY = Math.max(0, video.videoHeight - video.videoWidth) / 2; + + let 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}, + ]; + for (let {dx, dy} of areaSamples) { + let x = offsetX + dx + internalX; + let y = offsetY + dy + internalY; + info("Checking screen coordinate (" + [x,y] + ") of total resolution " + + video.videoWidth + "x" + video.videoHeight + + " against " + color.name + "."); + await helper.waitForPixel(video, px => { + let result = helper.isPixel(px, color, 16); + info("Checking pixel against " + color.name + ". Got [" + + Array.from(px) + "] (" + (result ? "YES" : "NO") + ")"); + return result; + }, {offsetX: x, offsetY: y}); + } + }; + + let 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"], + ); + + let testVideo = createMediaElement('video', 'testVideo'); + + let canvas = document.createElement("canvas"); + canvas.width = canvas.height = 20; + document.getElementById("content").appendChild(canvas); + let draw = (upleft, upright, downleft, downright) => { + helper.drawColor(canvas, upleft, {offsetX: 0, offsetY: 0}); + helper.drawColor(canvas, upright, {offsetX: 10, offsetY: 0}); + helper.drawColor(canvas, downleft, {offsetX: 0, offsetY: 10}); + helper.drawColor(canvas, downright, {offsetX: 10, offsetY: 10}); + }; + let helper = new CaptureStreamTestHelper2D(1, 1); + + await new Promise((resolve, reject) => { + SpecialPowers.wrap(document).onfullscreenchange = resolve; + SpecialPowers.wrap(document).onfullscreenerror = () => reject(new Error("fullscreenerror")); + + // Note that going fullscreen requires the tab (and window) to be in the + // foreground and having focus. + SpecialPowers.wrap(canvas).requestFullscreen(); + }); + + info("Testing screenshare without constraints"); + 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)`); + draw(helper.red, helper.blue, + helper.green, helper.grey); + let playback = new MediaStreamPlayback(testVideo, stream); + playback.startMedia(); + await playback.verifyPlaying(); + 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"); + let screenWidth = testVideo.videoWidth; + let screenHeight = testVideo.videoHeight; + await verifyScreenshare(testVideo, helper, + helper.red, helper.blue, + helper.green, helper.grey); + for (let track of stream.getTracks()) { + track.stop(); + } + playback.detachFromMediaElement(); + + info("Testing screenshare with size and framerate constraints"); + 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)`); + draw(helper.green, helper.red, + helper.grey, helper.blue); + playback = new MediaStreamPlayback(testVideo, stream); + playback.startMedia(); + await playback.verifyPlaying(); + 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"); + await verifyScreenshare(testVideo, helper, + helper.green, helper.red, + helper.grey, helper.blue); + + info("Testing modifying screenshare with applyConstraints"); + let resize = haveEvent(testVideo, "resize", wait(5000, new Error("Timeout"))); + await testVideo.srcObject.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. + let 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"); + draw(helper.grey, helper.green, + helper.blue, helper.red); + await playback.verifyPlaying(); // still playing + await verifyScreenshare(testVideo, helper, + helper.grey, helper.green, + helper.blue, helper.red); + await playback.stopTracksForStreamInMediaPlayback(); + playback.detachFromMediaElement(); + + SpecialPowers.wrap(document).exitFullscreen(); + }); +</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..075b0ab5a7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html @@ -0,0 +1,66 @@ +<!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(() => getUserMedia({ + video: { mediaSource: "browser", + scrollWithPage: true }, + fake: false + })) + .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 + }, + fake: false + })) + .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..796db3af84 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html @@ -0,0 +1,37 @@ +<!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(function () { + var testVideo = createMediaElement('video', 'testVideo'); + var constraints = { + video: { + mozMediaSource: "window", + mediaSource: "window" + }, + fake: false + }; + + 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_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..f3bca0719e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html @@ -0,0 +1,162 @@ +<!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(() => 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..f8150cc4c1 --- /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); + return 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..3b9e00896c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html @@ -0,0 +1,116 @@ +<!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; +runTest(() => getUserMedia({audio: { echoCancellation: false }}) + .then(stream => { + gUMAudioElement = createMediaElement("audio", "gUMAudio"); + gUMAudioElement.srcObject = stream; + + audioContext = new AudioContext(); + 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 : "")))); + +</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..029ce77dd0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html @@ -0,0 +1,258 @@ +<!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), + 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..749ac353b3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.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 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). + */ + scriptsReady.then(() => runTestWhenReady(async () => { + // Since we do not examine the stream we do not need loopback. + DISABLE_LOOPBACK_TONE = true; + let stream = await getUserMedia({audio: true}); + const nonDefaultRate = 32000; + let ac = new AudioContext({sampleRate: nonDefaultRate}); + mustThrowWith("Connect stream with graph of different sample rate", "NotSupportedError", () => { + let source = ac.createMediaStreamSource(stream); + }); + })) + .then(() => finish()) +</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..c4dfb9acb8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html @@ -0,0 +1,51 @@ +<!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()); + }; + + // both without and with the constraint + await testPeerIdentityConstraint(false); + await testPeerIdentityConstraint(true); +} + +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..29f78fdb52 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html @@ -0,0 +1,96 @@ +<!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 ["https://example.com", "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, sandbox: "allow-scripts allow-same-origin " + origin }), + "success", "gUM works in regular 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" }), + "TypeError", "navigator.mediaDevices undefined in sandboxed data iframe"); + is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}), + "TypeError", "navigator.mediaDevices undefined 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..6fc56c561f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html @@ -0,0 +1,56 @@ +<!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.Cu.import('resource://gre/modules/Services.jsm'); + +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 => { + let [{ windowID, innerWindowID, callID }] = await Promise.race([ + getUserMedia(constraints), + observe("getUserMedia:request") + ]); + let window = Services.wm.getOuterWindowWithId(windowID); + let devices = await new Promise((resolve, reject) => { + resolve = SpecialPowers.wrapCallback(resolve); + reject = SpecialPowers.wrapCallback(reject); + window.navigator.mozGetUserMediaDevices({}, resolve, reject, + innerWindowID, callID); + }); + return devices.map(SpecialPowers.wrapCallback(d => d.QueryInterface(Ci.nsIMediaDevice))); +}; + +runTest(async () => { + await pushPrefs(["media.navigator.permission.disabled", true], + ["media.navigator.permission.fake", 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.filter(d => d.name.includes("MochiTest")); + ok(devices.length, + "Our own window is among the scary: " + devices.map(d => `"${d.name}"`)); + + 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..ae72b54a96 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.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"> + "use strict"; + + createHTML({ + title: "Stopping a MediaStreamTrack and its clones should deallocate the device", + bug: "1294605" + }); + + runTest(() => getUserMedia({audio: true, video: true}).then(stream => { + let 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..261896d74b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_groupId.html @@ -0,0 +1,46 @@ +<!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" }); + +let getDefaultDevices = async () => { + let 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")); + + let videos = devices.filter(d => d.kind == "videoinput"); + is(videos.length, 1, "One video device found."); + let 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", ""]); + + 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.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.groupId, video.groupId, "GroupIds should be the same"); +}); +</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..185ad1e278 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html @@ -0,0 +1,62 @@ +<!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" +}); + +const RESPONSE_WAIT_TIME_MS = 3000; + +async function maybeReceiveDevicechangeEvent() { + return Promise.race([ + new Promise(r => navigator.mediaDevices.ondevicechange = () => r(true)), + wait(RESPONSE_WAIT_TIME_MS).then(() => false) + ]); +} + +runTest(async () => { + SimpleTest.requestCompleteLog(); + + await pushPrefs( + // Ensure there are continuous fake devicechange events throughout this test + ["media.ondevicechange.fakeDeviceChangeEvent.enabled", true], + // Make fake devices count as real, permission-wise, or devicechange events + // won't be exposed + ["media.navigator.permission.fake", true], + // Ensure this precondition to the below tests + ["media.navigator.permission.disabled", true] + ); + + const stream = await getUserMedia({video: true, fake: true}); + const [track] = stream.getVideoTracks(); + await pushPrefs(["media.navigator.permission.disabled", false]); + try { + ok(await maybeReceiveDevicechangeEvent(), + "devicechange event is fired when gUM is in use without permanent " + + "permission granted"); + } finally { + track.stop(); + } + + ok(!await maybeReceiveDevicechangeEvent(), + "devicechange event is NOT fired when gUM is NOT in use and " + + "permanent permission is NOT granted"); + + await pushPrefs(["media.navigator.permission.disabled", true]); + ok(await maybeReceiveDevicechangeEvent(), + "devicechange event is fired when gUM is NOT in use and permanent "+ + "permission is granted"); +}); + +</script> +</pre> +</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..22ed0bc304 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.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: "1246310", + title: "Renegotiation: add audio track to existing video-only stream", + }); + + runNetworkTest(function (options) { + SimpleTest.requestCompleteLog(); + var 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(new AudioContext(), test.pcLocal); + } + ] + ); + + test.run(); + }); +</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..366e8fe413 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.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: "1017888", + title: "Renegotiation: add DataChannel" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + 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..782c8fddd3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html @@ -0,0 +1,45 @@ +<!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" + }); + + var test; + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + 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}]); + 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..ef5c909c64 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html @@ -0,0 +1,45 @@ +<!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"); + 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); + })); + }, + ] + ); + + test.setMediaConstraints([{audio: true}], [{audio: true}]); + 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..9d2c493cac --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.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: "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"); + 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); + })); + }, + ] + ); + + // TODO(bug 1093835): figure out how to verify if media flows through the new stream + test.setMediaConstraints([{audio: true}], [{audio: true}]); + 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..441f53ae2a --- /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}]); + 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..4ce06a9e06 --- /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}]); + 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..c39c9cbc57 --- /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}], []); + 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..2d138d23a8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.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: answerer adds second audio stream" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + test.run(); + }); +</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..a68f148322 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.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: "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" ]; + + 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(new AudioContext(), test.pcLocal); + ok(true, "input and output audio data matches"); + } catch(e) { + ok(false, `No audio flow: ${e}`); + } + }, + ]); + + // This inlines test.run(), to allow for multiple tests to run. + test.updateChainSteps(); + await test.chain.execute(); + await test.close(); + } + + runNetworkTest(async (options) => { + 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}`); + } + + networkTestFinished(); + }); +</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..4511e21861 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html @@ -0,0 +1,146 @@ +<!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 (${source0.timestamp})`); + 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 (${source1.timestamp})`); + 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(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; + SpecialPowers.pushPrefEnv( + { "set": [["privacy.reduceTimerPrecision", false]]}, function() { + 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..4900b46038 --- /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" + }); + + var test; + runNetworkTest(function (options) { + var helper = new AudioStreamHelper(); + + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}], []); + 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_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); + + addRenegotiation(test.chain, []); + + test.chain.append([ + function PC_REMOTE_CHECK_AUDIO_FLOWING_2() { + return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]); + } + ]); + + 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..e99a926a50 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html @@ -0,0 +1,92 @@ +<!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 > 0 && + receivers[1].getSynchronizationSources().length > 0) { + 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})`); + ok(window.performance.now() + window.performance.timeOrigin - + source.timestamp < 2500, // This large value is used because sometimes + // the testing hardware is _very_ slow + `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; + 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..a950a8274e --- /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 == 0) { + 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 > 0, + "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; + 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..9dfd11ea7c --- /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; + 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..6a5379d37b --- /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}]); + 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..ed50941ff3 --- /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}]); + 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..fdedb2faeb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.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: "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.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}]); + test.run(); + }, { 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..7ac638050c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html @@ -0,0 +1,40 @@ +<!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.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}]); + 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..9ac44d3f2d --- /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}]); + 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..2df88648cc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html @@ -0,0 +1,40 @@ +<!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.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}]); + 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..bd5e4c25dd --- /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}]); + test.run(); + }); +</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..e3e44cf1bf --- /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}]); + 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..d5c28eedf3 --- /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"); + } + ]); + + 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..0af2b207af --- /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}]); + 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..ae51e4a1c0 --- /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}]); + 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..c840804a6b --- /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}]); + 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..d2db0d8cf7 --- /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}]); + 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..da8dd2d559 --- /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}]); + 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..b2406758a0 --- /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); + } + ]); + + 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..873099cdde --- /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"); + } + ]); + + 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..16b1fda28b --- /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"); + } + ]); + + 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..5dbc398146 --- /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 + ); + + 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..13b49d2a39 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html @@ -0,0 +1,20 @@ +<!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..7e278a5408 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html @@ -0,0 +1,20 @@ +<!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..ea86875831 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.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: "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.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..9bbbd912bc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.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: "1039666", + title: "Basic screenshare-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + var constraints = { + video: { + mozMediaSource: "screen", + mediaSource: "screen" + }, + fake: false + }; + test.setMediaConstraints([constraints], []); + test.run(); + }); +</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..ce071a516f --- /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}]); + 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..503f85ad20 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html @@ -0,0 +1,67 @@ +<!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 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 packet = ParseRtpPacket(data); + 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`); + } + ]); + + 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..7348977dd5 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.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: "1038926", + title: "Basic windowshare-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + var constraints = { + video: { + mozMediaSource: "window", + mediaSource: "window" + }, + fake: false + }; + test.setMediaConstraints([constraints], []); + 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..69d36ee399 --- /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}]); + 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..d72bf9a720 --- /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"); + } + ]); + + 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..17a7c11069 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.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: "1227781", + title: "Test with invalid TURN server" + }); + + var turnConfig = { iceServers: [{"username":"mozilla","credential" + :"mozilla","url":"turn:test@10.0.0.1"}] }; + var test; + runNetworkTest(function (options) { + var exception = false; + + try { + pc = new RTCPeerConnection(turnConfig); + } catch (e) { + info(e); + exception = true; + } + is(exception, true, "Exception fired"); + ok("Success"); + SimpleTest.finish(); + }); +</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..f3d45c7f7e --- /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}]); + test.run(); +}); + +</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..b757ec7cd8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.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: "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; + networkTestFinished(); + }); +</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..b6b41f00ae --- /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, + 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); + + 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"); + }) + .then(networkTestFinished); +}); +</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..446bd29bb0 --- /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); + 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..40c25eecfc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.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: "834153", + title: "Queue CreateAnswer in PeerConnection.js" + }); + + runNetworkTest(function () { + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + + 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(); + networkTestFinished(); + }) + .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..a1f0bf365e --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html @@ -0,0 +1,88 @@ +<!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 + 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(); }) + .catch(reason => ok(false, "unexpected failure: " + reason)) + .then(networkTestFinished); +}); +</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..781ea41be5 --- /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 = 10; + 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", + }); + } + ]); + 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..20bf5ee140 --- /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 = 10; + 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", + }); + } + ]); + 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..9f20257480 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html @@ -0,0 +1,134 @@ +<!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 = 10; + canvas.style.display = 'none'; + document.getElementById('content').appendChild(canvas); + + var gl = WebGLUtil.getWebGL(canvas.id, false); + if (!gl) { + todo(false, "WebGL unavailable."); + networkTestFinished(); + return; + } + + var errorFunc = str => ok(false, 'Error: ' + str); + WebGLUtil.setErrorFunc(errorFunc); + WebGLUtil.setWarningFunc(errorFunc); + + 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", + }); + } + ]); + 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..25db23ef99 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + +createHTML({ + bug: "1081409", + title: "Captured video-only over peer connection", + visible: true +}).then(() => new Promise(resolve => { + // Run tests in sequence for log readability. + PARALLEL_TESTS = 1; + let manager = new MediaTestManager; + window.mediaTestManager = manager; + manager.runTests(getPlayableVideos(gLongerTests), startTest); + manager.onFinished = () => { + // Tear down before SimpleTest.finish. + if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) { + getNetworkUtils().tearDownNetwork(); + } + resolve(); + }; +})) +.catch(e => ok(false, "Unexpected " + e + ":\n" + e.stack)); + +function startTest(media, token) { + let manager = window.mediaTestManager; + manager.started(token); + info("Starting test for " + media.name); + var 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); + + var test; + new Promise((resolve, reject) => { + video.onloadedmetadata = resolve; + video.onerror = () => reject(video.error); + }) + .then(() => { + video.onerror = () => ok(false, media.name + " failed in playback (code=" + + video.error.code + "). Stream should be OK. " + + "Continuing test."); + return runNetworkTest(() => { + var stream = video.mozCaptureStream(); + test = new PeerConnectionTest({ config_local: { label_suffix: media.name }, + config_remote: { label_suffix: media.name } }); + test.setOfferOptions({ offerToReceiveVideo: false, + offerToReceiveAudio: false }); + var hasVideo = stream.getVideoTracks().length > 0; + var hasAudio = stream.getAudioTracks().length > 0; + 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(); + }, + ]); + return test.chain.execute(); + }); + }) + // Handle both MediaErrors (with the `code` attribute) and other errors. + .catch(e => ok(false, "Error (" + e + ")" + + (e.code ? " (code=" + e.code + ")" : "") + + " in test for " + media.name + + (e.stack ? ":\n" + e.stack : ""))) + .then(() => test && test.close()) + .then(() => { + removeNodeAndSource(video); + manager.finished(token); + }) + .catch(e => ok(false, "Error (" + e + ") during shutdown.")); +}; + +</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..6582446bfe --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html @@ -0,0 +1,180 @@ +<!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"), + + badCertificate({ + name: "ECDH", + namedCurve: "P-256" + }, "DataError", "otherwise valid ECDH config is rejected"), + + badCertificate({ + name: "not a valid algorithm" + }, "SyntaxError", "not a valid algorithm"), + + badCertificate("ECDSA", "SyntaxError", "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)); + } + }) + .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, 'InvalidParameterError', + '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..8001d23464 --- /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); + } + ]); + 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..36d7ac7aa4 --- /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"); + + Promise.all([finished, silence]).then(networkTestFinished); + }); +</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..37b2011664 --- /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]); + test.run(); +}); +</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..d975177ed4 --- /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); + }, + ]); + 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..1c1549edf7 --- /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)); + }); + test.run(); + }); +</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..9ae3b8d50f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html @@ -0,0 +1,57 @@ +<!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 () { + testCreateAnswerError() + .then(testSetLocalDescriptionError) + .then(testSetRemoteDescriptionError) + .catch(reason => ok(false, "unexpected error: " + reason)) + .then(networkTestFinished); + }); + +</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..b14a95b8bf --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.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: "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"); + gumTest.chain.execute() + .then(() => forwardingTest.chain.execute()) + .then(() => gumTest.close()) + .then(() => forwardingTest.close()) + .then(() => networkTestFinished()); +}); +</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..3f6728573f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.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: "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(() => { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['media.peerconnection.ice.stun_client_maximum_transmits', 3], + ['media.peerconnection.ice.trickle_grace_period', 3000], + ] + }, function() { + 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]); + 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..4094f8cf42 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.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: "1291715", + title: "Test insertDTMF on sender", + visible: true +}); + +function insertdtmftest(pc) { + ok(pc.getSenders().length > 0, "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 pushPrefs(['media.peerconnection.dtmf.enabled', true]) + .then(() => { test.run() }) + .catch(e => ok(false, "unexpected failure: " + e)); +}); + +</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..ff234b690b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html @@ -0,0 +1,45 @@ +<!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" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + 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..6e30743b82 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html @@ -0,0 +1,48 @@ +<!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" + }); + + var test; + runNetworkTest(function (options) { + 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); + }, + ]); + 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..c23cb50e34 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.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: "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"), 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/); + }; + + 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 { + is(v2.videoWidth, 160, "sink width should be 160 for " + codec); + is(v2.videoHeight, 120, "sink height should be 120 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()) { + 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"); + networkTestFinished(); + }); +</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..759c8dbfbd --- /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", + }), + ]) + }, + ]); + 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..88db25f39f --- /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}]); + 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..ac88277a26 --- /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}]); + 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..9f8668eb6a --- /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}]); + 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..b966accd1b --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html @@ -0,0 +1,198 @@ +<!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; + + let 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") + let audioTracks = localStream.getAudioTracks(); + + let servers = null; + + pc1 = new RTCPeerConnection(servers); + info('Created local peer connection object pc1'); + let iceComplete1 = new Promise( (resolve, reject) => { + pc1.onicecandidate = (e) => { + onIceCandidate(pc1, e, resolve); + }; + }); + + pc2 = new RTCPeerConnection(servers); + info('Created remote peer connection object pc2'); + let iceComplete2 = new Promise( (resolve, reject) => { + pc2.onicecandidate = (e) => { + onIceCandidate(pc2, e, resolve); + }; + }); + + pc1.oniceconnectionstatechange = (e) => { + onIceStateChange(pc1, e); + }; + pc2.oniceconnectionstatechange = (e) => { + onIceStateChange(pc2, e); + }; + + let 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; + let nonDefault_ctx = new AudioContext({sampleRate: nonDefaultRate}); + oscillator = nonDefault_ctx.createOscillator(); + let dest = nonDefault_ctx.createMediaStreamDestination(); + oscillator.connect(dest); + oscillator.start(); + + // Wait for remote stream + let 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 + let ac = new AudioContext; + let 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", () => { + let source_non_default_rate = 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..c050c6198d --- /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 }); + 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..9750916793 --- /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 }); + 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..ec80079716 --- /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 }); + test.run(); + }); +</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..6fb8add109 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.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: "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 > 0, "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"); + + 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.")) + .catch(reason => ok(false, "unexpected failure: " + reason)) + .then(networkTestFinished); + }); +</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..799a3476f4 --- /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"); + }, + ]); + 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..71a600e24a --- /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.")); +} + +var test; + +runNetworkTest(options => + pushPrefs(['media.peerconnection.ice.stun_client_maximum_transmits', 3], + ['media.peerconnection.ice.trickle_grace_period', 5000]).then(() => { + options = options || {}; + options.config_local = options.config_local || {}; + var 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"; + + 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"); + 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..929e6457b0 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.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 reoffer" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + 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..9d8111ab44 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html @@ -0,0 +1,52 @@ +<!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" + }); + + var test; + runNetworkTest(function (options) { + 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); + 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..ad8c2ddb53 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html @@ -0,0 +1,57 @@ +<!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); + 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( + new 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}]); + test.run(); + }); +</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..ad75441f16 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html @@ -0,0 +1,87 @@ +<!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); + 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( + new 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( + new 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}]); + test.run(); + }); +</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..c3c0709239 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.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: "1017888", + title: "Renegotiation: remove then add audio track" + }); + + runNetworkTest(function (options) { + options = options || { }; + options.bundle = false; + const test = new PeerConnectionTest(options); + 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}]); + test.run(); + }); +</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..32c9c3337b --- /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}]); + 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..bbb12164a6 --- /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}]); + 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..aa31c38fa3 --- /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}]); + 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..07443431d2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html @@ -0,0 +1,92 @@ +<!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 > 0, "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, + 10, 10); + emitter.start(); + emitter.stream().getTracks().forEach(t => pc1.addTrack(t, emitter.stream())); + let h = emitter.helper(); + + pc1.createOffer({}) + .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)) + + // re-negotiate to trigger the race condition in the jitter buffer + .then(() => pc1.createOffer({})) // check that createOffer accepts arg. + .then(offer => pc1.setLocalDescription(offer)) + .then(() => pc2.setRemoteDescription(pc1.localDescription)) + .then(() => pc2.createAnswer({})) + .then(answer => pc2.setLocalDescription(answer)) + .then(() => pc1.setRemoteDescription(pc2.localDescription)) + .then(() => delivered) + + // now verify that actually something gets rendered into the remote video + // element. + .then(() => 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 + .then(() => { + emitter.colors(h.red, h.green) + }) + .then(() => h.pixelMustBecome(v2, h.red, { + threshold: 128, + infoString: "pcRemote's video should become red", + })) + .catch(reason => ok(false, "unexpected failure: " + reason)) + .then(networkTestFinished); + }); +</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..ff2a94a8f1 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html @@ -0,0 +1,187 @@ +<!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 + // (440Hz for loopback devices, 1kHz for fake tracks). + 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"); + } + }); + } + ]); + test.run(); + }); +</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..dc482af87e --- /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(); + } + }, + ]); + test.run(); +}); +</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..66347f6bf1 --- /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); + } + ] + ); + + 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..009fe0ff03 --- /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}]); + 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..d501938c28 --- /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}], []); + 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..99e8c0bcd7 --- /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}]); + 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..7f22707582 --- /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}]); + 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..dd4b76d03d --- /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}]); + 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..471382f9da --- /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}]); + 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..ffb20e77d5 --- /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}]); + 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..77720b575a --- /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}]); + 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..f3591d0b46 --- /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}]); + 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..e1c5f6d185 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.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: "Bug 1404250", + title: "Extremely bitrate restricted video-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + 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)); + } + ]); + 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..72398897f2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.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: "1359854", + title: "500Kb restricted video-only peer connection" + }); + + var test; + runNetworkTest(function (options) { + 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 + 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..fc22e038d9 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html @@ -0,0 +1,80 @@ +<!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: "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 + + var test; + runNetworkTest(async function (options) { + test = new PeerConnectionTest(options); + + var 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) { + return Promise.all([test.pcLocal.waitForSyncedRtcp(), + test.pcRemote.waitForSyncedRtcp()]) + .then(async () => { + // 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}], []); + 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..9530748a06 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html @@ -0,0 +1,92 @@ +<!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 + }); + + 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); + + await mustRejectWith( + "Invalid scaleResolutionDownBy must reject", "RangeError", + () => sender.setParameters( + { encodings:[{ scaleResolutionDownBy: 0.5 } ] }) + ); + + 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"); + is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source"); + is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source"); + stream.getTracks().forEach(track => track.stop()); + v1.srcObject = v2.srcObject = null; + pc1.close() + pc2.close() + } + + runNetworkTest(async () => { + await pushPrefs(['media.peerconnection.video.lock_scaling', true]); + await testScale("VP8"); + if (!navigator.appVersion.includes("Android")) { + // No support for H.264 on Android in automation, see Bug 1355786 + await testScale("H264"); + } + networkTestFinished(); + }); +</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..d38e032894 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1355220", + title: "RTCRtpSender.getStats() and RTCRtpReceiver.getStats()", + visible: true + }); + + var checkStats = (sndReport, rcvReport, mediaType) => { + ok(sndReport instanceof window.RTCStatsReport, "sender stats are a RTCStatsReport"); + ok(rcvReport instanceof window.RTCStatsReport, "receiver stats are a RTCStatsReport"); + // Returns SSRCs and checks that the tracks are of the correct mediaType + let getSsrcs = (report, kind) => { + return [...report.values()] + .filter(stat => stat.type.endsWith("bound-rtp")).map(stat =>{ + is(stat.mediaType, kind, "mediaType of " + stat.id + + " is expected type " + kind); + return stat.ssrc; + }).sort().join("|"); + }; + let sndSsrcs = getSsrcs(sndReport, mediaType); + let rcvSsrcs = getSsrcs(rcvReport, mediaType); + 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. + + var test; + 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) { + return Promise.all([test.pcLocal.waitForSyncedRtcp(), + test.pcRemote.waitForSyncedRtcp()]) + .then(async () => { + 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}], []); + 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..191e664b27 --- /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 () { + var 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"); + }); + } + ]); + + 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..a3f37ecc64 --- /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 () { + var 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"); + }); + } + ]); + + 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..a10710542b --- /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 () { + var 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"); + } + ]); + + 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..8d78ca463d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.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: "1230184", + title: "Set parameters on sender", + visible: true +}); + +function parameterstest(pc) { + ok(pc.getSenders().length > 0, "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.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: [{ 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"], + [{}] + ].reduce((p, args) => p.then(() => testParameters.apply(this, args)), + Promise.resolve()); +} + +runNetworkTest(() => { + + 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() + .catch(e => ok(false, "unexpected failure: " + e)); +}); + +</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..12f588db60 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html @@ -0,0 +1,85 @@ +<!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" +}); + +let sender, localElem, remoteElem; +let originalWidth, originalAspectRatio, originalScale; + +async function checkScaleDownBy(scale) { + sender.setParameters({ encodings: [{ scaleResolutionDownBy: scale }] }); + await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout"))); + + // Find the expected resolution. Internally we pick the closest lower + // resolution with an identical aspect ratio. + let expectedWidth = Math.floor(originalWidth / scale); + while (expectedWidth / originalAspectRatio % 1 != 0) { + --expectedWidth; + } + is(remoteElem.videoWidth, expectedWidth, + `Width should have scaled down by ${scale}`); + is(remoteElem.videoHeight, expectedWidth / originalAspectRatio, + `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")) { + 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; + originalAspectRatio = originalWidth / localElem.videoHeight; + originalScale = remoteElem.videoWidth / originalWidth; + 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); + }, + ]); + 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..da1b2451e8 --- /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 () { + var 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")); + } + ]); + + 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..6c9546bf06 --- /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 () { + var 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")); + } + ]); + + 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..1877b8a084 --- /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 () { + var 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'); + } + ]); + + test.run(); +}); +</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..fdd490f4fd --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html @@ -0,0 +1,164 @@ +<!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: "1231507", + title: "Basic video-only peer connection with Simulcast answer", + visible: true + }); + + function addRIDExtension(pc, extensionId) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDExtension(extensionId); + } + + function selectRecvRID(pc, rid) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDFilter(rid); + } + + runNetworkTest(() => + 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]).then(() => { + let emitter, helper; + + test = new PeerConnectionTest({bundle: false}); + test.setMediaConstraints([{video: true}], [{video: true}]); + + test.chain.replace("PC_REMOTE_GUM", [ + function PC_REMOTE_CANVAS_CAPTURESTREAM(test) { + emitter = new VideoFrameEmitter(); + helper = new VideoStreamHelper(); + test.pcRemote.attachLocalStream(emitter.stream()); + emitter.start(); + } + ]); + + test.chain.insertAfter('PC_REMOTE_GET_OFFER', [ + function PC_REMOTE_SET_RIDS(test) { + const senders = test.pcRemote._pc.getSenders(); + is(senders.length, 1, "We have exactly one RTP sender"); + const sender = senders[0]; + ok(sender.track, "Sender has a track"); + + return sender.setParameters({ + encodings: [{ rid: "foo", maxBitrate: 40000 }, + { rid: "bar", maxBitrate: 40000, scaleResolutionDownBy: 2 }] + }); + }, + function PC_LOCAL_ADD_RIDS_TO_OFFER(test) { + // Create a dummy offer, and use it to set simulcast stuff on the + // offer we will actually be using. + return test.createOffer(test.pcRemote).then(offer => { + test._local_offer.sdp = sdputils.transferSimulcastProperties( + offer.sdp, test._local_offer.sdp); + info("Offer with RIDs: " + JSON.stringify(test._local_offer)); + ok(test._local_offer.sdp.match(/a=simulcast:/), "Modified offer has simulcast"); + ok(test._local_offer.sdp.match(/a=rid:foo/), "Modified offer has rid foo"); + ok(test._local_offer.sdp.match(/a=rid:bar/), "Modified offer has rid bar"); + ok(test._local_offer.sdp.match(/urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id/), "Modified offer has RID"); + }); + } + ]); + + test.chain.insertAfter('PC_LOCAL_GET_ANSWER',[ + function PC_LOCAL_REMOVE_SIMULCAST_ATTRS_FROM_ANSWER(test) { + test._remote_answer.sdp = + sdputils.removeSimulcastProperties(test._remote_answer.sdp); + } + ]); + + // do this after set remote description so the MediaPipeline + // has been created. + test.chain.insertAfter('PC_LOCAL_SET_REMOTE_DESCRIPTION',[ + function PC_LOCAL_SET_RTP_FIRST_RID(test) { + const extmap_id = test._local_offer.sdp.match( + "a=extmap:([0-9+])/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"); + ok(extmap_id, "Local offer has extmap id for simulcast: " + extmap_id[1]); + // Cause pcLocal to filter out everything but RID "bar", only + // allowing one of the simulcast streams through. + addRIDExtension(test.pcLocal, extmap_id[1]); + selectRecvRID(test.pcLocal, "bar"); + } + ]); + + test.chain.append([ + async function PC_LOCAL_WAIT_FOR_FRAMES() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_LOCAL_CHECK_SIZE_1() { + const vlocal = test.pcRemote.localMediaElements[0]; + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcRemote"); + ok(vremote, "Should have remote video element for pcLocal"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 24, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(vremote.videoHeight, 24, + "sink is 1/2 height of source, modulo our cropping algorithm"); + }, + function PC_LOCAL_SET_RTP_SECOND_RID(test) { + // Now, cause pcLocal to filter out everything but RID "foo", only + // allowing the other simulcast stream through. + selectRecvRID(test.pcLocal, "foo"); + }, + function PC_LOCAL_WAIT_FOR_SECOND_MEDIA_FLOW(test) { + return test.pcLocal.waitForMediaFlow(); + }, + async function PC_LOCAL_WAIT_FOR_FRAMES_2() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + // For some reason, even though we're getting a 24x24 stream, sometimes + // the resolution isn't updated on the video element on the first frame. + async function PC_LOCAL_WAIT_FOR_FRAMES_3() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_LOCAL_CHECK_SIZE_2() { + const vlocal = test.pcRemote.localMediaElements[0]; + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcRemote"); + ok(vremote, "Should have remote video element for pcLocal"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 48, + "sink is same width as source, modulo our cropping algorithm"); + is(vremote.videoHeight, 48, + "sink is same height as source, modulo our cropping algorithm"); + }, + ]); + + return test.run(); + }) + .catch(e => ok(false, "unexpected failure: " + e))); +</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..6c3c7b6acc --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html @@ -0,0 +1,164 @@ +<!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: "1231507", + title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution", + visible: true + }); + + function addRIDExtension(pc, extensionId) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDExtension(extensionId); + } + + function selectRecvRID(pc, rid) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDFilter(rid); + } + + runNetworkTest(() => + 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]).then(() => { + let emitter, helper; + + test = new PeerConnectionTest({bundle: false}); + test.setMediaConstraints([{video: true}], [{video: true}]); + + test.chain.replace("PC_REMOTE_GUM", [ + function PC_REMOTE_CANVAS_CAPTURESTREAM(test) { + emitter = new VideoFrameEmitter(); + helper = new VideoStreamHelper(); + test.pcRemote.attachLocalStream(emitter.stream()); + emitter.start(); + } + ]); + + test.chain.insertAfter('PC_REMOTE_GET_OFFER', [ + function PC_REMOTE_SET_RIDS(test) { + const senders = test.pcRemote._pc.getSenders(); + is(senders.length, 1, "We have exactly one RTP sender"); + const sender = senders[0]; + ok(sender.track, "Sender has a track"); + + return sender.setParameters({ + encodings: [{ rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: "bar", maxBitrate: 40000 }] + }); + }, + function PC_LOCAL_ADD_RIDS_TO_OFFER(test) { + // Create a dummy offer, and use it to set simulcast stuff on the + // offer we will actually be using. + return test.createOffer(test.pcRemote).then(offer => { + test._local_offer.sdp = sdputils.transferSimulcastProperties( + offer.sdp, test._local_offer.sdp); + info("Offer with RIDs: " + JSON.stringify(test._local_offer)); + ok(test._local_offer.sdp.match(/a=simulcast:/), "Modified offer has simulcast"); + ok(test._local_offer.sdp.match(/a=rid:foo/), "Modified offer has rid foo"); + ok(test._local_offer.sdp.match(/a=rid:bar/), "Modified offer has rid bar"); + ok(test._local_offer.sdp.match(/urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id/), "Modified offer has RID"); + }); + } + ]); + + test.chain.insertAfter('PC_LOCAL_GET_ANSWER',[ + function PC_LOCAL_REMOVE_SIMULCAST_ATTRS_FROM_ANSWER(test) { + test._remote_answer.sdp = + sdputils.removeSimulcastProperties(test._remote_answer.sdp); + } + ]); + + // do this after set remote description so the MediaPipeline + // has been created. + test.chain.insertAfter('PC_LOCAL_SET_REMOTE_DESCRIPTION',[ + function PC_LOCAL_SET_RTP_FIRST_RID(test) { + const extmap_id = test._local_offer.sdp.match( + "a=extmap:([0-9+])/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"); + ok(extmap_id, "Local offer has extmap id for simulcast: " + extmap_id[1]); + // Cause pcLocal to filter out everything but RID "bar", only + // allowing one of the simulcast streams through. + addRIDExtension(test.pcLocal, extmap_id[1]); + selectRecvRID(test.pcLocal, "bar"); + } + ]); + + test.chain.append([ + async function PC_LOCAL_WAIT_FOR_FRAMES() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_LOCAL_CHECK_SIZE_1() { + const vlocal = test.pcRemote.localMediaElements[0]; + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcRemote"); + ok(vremote, "Should have remote video element for pcLocal"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 48, + "sink is same width as source, modulo our cropping algorithm"); + is(vremote.videoHeight, 48, + "sink is same height as source, modulo our cropping algorithm"); + }, + function PC_LOCAL_SET_RTP_SECOND_RID(test) { + // Now, cause pcLocal to filter out everything but RID "foo", only + // allowing the other simulcast stream through. + selectRecvRID(test.pcLocal, "foo"); + }, + function PC_LOCAL_WAIT_FOR_SECOND_MEDIA_FLOW(test) { + return test.pcLocal.waitForMediaFlow(); + }, + async function PC_LOCAL_WAIT_FOR_FRAMES_2() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + // For some reason, even though we're getting a 24x24 stream, sometimes + // the resolution isn't updated on the video element on the first frame. + async function PC_LOCAL_WAIT_FOR_FRAMES_3() { + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcLocal"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_LOCAL_CHECK_SIZE_2() { + const vlocal = test.pcRemote.localMediaElements[0]; + const vremote = test.pcLocal.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcRemote"); + ok(vremote, "Should have remote video element for pcLocal"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 24, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(vremote.videoHeight, 24, + "sink is 1/2 height of source, modulo our cropping algorithm"); + }, + ]); + + return test.run(); + }) + .catch(e => ok(false, "unexpected failure: " + e))); +</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..b42683a3d7 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html @@ -0,0 +1,209 @@ +<!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: "1432793", + title: "Simulcast with odd resolution", + visible: true + }); + + const encodings = [{ rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 1.9 }, + { rid: "bar", maxBitrate: 40000, scaleResolutionDownBy: 3.5 }, + { rid: "baz", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }]; + + function addRIDExtension(pc, extensionId) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDExtension(extensionId); + } + + function selectRecvRID(pc, rid) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDFilter(rid); + } + + async function changeSourceResolution(test, emitter, width, height) { + info(`Changing source resolution to ${width}x${height}`); + emitter.size(width, height); + emitter.start(); + await Promise.all([ + haveEvent(test.pcRemote.localMediaElements[0], "resize"), + haveEvent(test.pcLocal.remoteMediaElements[0], "resize"), + ]); + emitter.stop(); + info("Source resolution changed"); + } + + async function setParameters(test, emitter, encodings) { + info(`Setting parameters to ${JSON.stringify(encodings)}`); + emitter.start(); + await test.pcRemote._pc.getSenders()[0].setParameters({encodings}); + await haveEvent(test.pcLocal.remoteMediaElements[0], "resize"); + emitter.stop(); + info("Parameters set"); + } + + async function checkResolution(test, emitter, rid) { + const vlocal = test.pcRemote.localMediaElements[0]; + const vremote = test.pcLocal.remoteMediaElements[0]; + + info(`Changing to rid ${rid}`); + selectRecvRID(test.pcLocal, rid); + emitter.start(); + await haveEvent(vremote, "resize"); + emitter.stop(); + + const srcWidth = vlocal.videoWidth; + const srcHeight = vlocal.videoHeight; + info(`Source resolution is ${srcWidth}x${srcHeight}`); + + const scaleDownBy = encodings.find(({rid: r}) => r == rid).scaleResolutionDownBy; + const expectedWidth = srcWidth / scaleDownBy; + const expectedHeight = srcHeight / scaleDownBy; + const margin = srcWidth * 0.1; + const width = vremote.videoWidth; + const height = vremote.videoHeight; + 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 checkResolutions(test, emitter) { + const vremote = test.pcLocal.remoteMediaElements[0]; + + // Start by making sure we're not on foo (first default) or + // baz (subsequent default), thus getting resize events. + selectRecvRID(test.pcLocal, "bar"); + emitter.start(); + await haveEvent(vremote, "resize"); + emitter.stop(); + + await checkResolution(test, emitter, "foo"); + await checkResolution(test, emitter, "bar"); + await checkResolution(test, emitter, "baz"); + } + + runNetworkTest(async () => { + await pushPrefs(['media.peerconnection.simulcast', true], + ['media.peerconnection.video.lock_scaling', true], + // 240Kbps was determined empirically + ['media.peerconnection.video.min_bitrate_estimate', 240*1000]); + + let emitter, helper; + + test = new PeerConnectionTest({bundle: false}); + test.setMediaConstraints([{video: true}], [{video: true}]); + + test.chain.replace("PC_REMOTE_GUM", [ + function PC_REMOTE_CANVAS_CAPTURESTREAM(test) { + helper = new VideoStreamHelper(); + emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528); + test.pcRemote.attachLocalStream(emitter.stream()); + // Don't start the emitter yet. Only do that when we're able to set rid filters. + } + ]); + + test.chain.insertAfter('PC_REMOTE_GET_OFFER', [ + function PC_REMOTE_SET_RIDS(test) { + const senders = test.pcRemote._pc.getSenders(); + is(senders.length, 1, "We have exactly one RTP sender"); + const sender = senders[0]; + ok(sender.track, "Sender has a track"); + + return sender.setParameters({encodings}); + }, + async function PC_LOCAL_ADD_RIDS_TO_OFFER(test) { + // Create a dummy offer, and use it to set simulcast stuff on the + // offer we will actually be using. + let offer = await test.createOffer(test.pcRemote); + test._local_offer.sdp = sdputils.transferSimulcastProperties( + offer.sdp, test._local_offer.sdp); + info(`Offer with RIDs: ${JSON.stringify(test._local_offer)}`); + ok(test._local_offer.sdp.match(/a=simulcast:/), "Modified offer has simulcast"); + ok(test._local_offer.sdp.match(/a=rid:foo/), "Modified offer has rid foo"); + ok(test._local_offer.sdp.match(/a=rid:bar/), "Modified offer has rid bar"); + ok(test._local_offer.sdp.match(/a=rid:baz/), "Modified offer has rid bar"); + ok(test._local_offer.sdp.match(/urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id/), "Modified offer has RID"); + } + ]); + + test.chain.insertAfter('PC_LOCAL_GET_ANSWER',[ + function PC_LOCAL_REMOVE_SIMULCAST_ATTRS_FROM_ANSWER(test) { + test._remote_answer.sdp = + sdputils.removeSimulcastProperties(test._remote_answer.sdp); + } + ]); + + // do this after set remote description so the MediaPipeline + // has been created. + test.chain.insertAfter('PC_LOCAL_SET_REMOTE_DESCRIPTION',[ + function PC_LOCAL_SET_RTP_FIRST_RID(test) { + info(`local offer: ${test._local_offer.sdp}`); + const extmap_id = test._local_offer.sdp.match( + "a=extmap:([0-9+])/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"); + ok(extmap_id, `Original answer has extmap id for simulcast: ${extmap_id[1]}`); + // Cause pcLocal to filter out everything but RID "foo", only + // allowing one of the simulcast streams through. + addRIDExtension(test.pcLocal, extmap_id[1]); + selectRecvRID(test.pcLocal, "foo"); + emitter.start(); + }, + function SETUP_RESIZE_LISTENERS(test) { + for(const elem of [...test.pcRemote.localMediaElements, + ...test.pcLocal.remoteMediaElements, + ]) { + elem.addEventListener("resize", + () => info(`element ${elem.id} resized to ${elem.videoWidth}x${elem.videoHeight}`)); + } + }, + ]); + + test.chain.append([ + async function PC_LOCAL_CHECK_INITIAL_SIZES() { + await checkResolutions(test, emitter); + }, + async function PC_LOCAL_CHANGE_SRC_1() { + await changeSourceResolution(test, emitter, 1280, 720); + await checkResolutions(test, emitter); + }, + async function PC_LOCAL_CHANGE_PARAMS_2() { + encodings.find(({rid}) => rid == "foo").scaleResolutionDownBy = 1; + encodings.find(({rid}) => rid == "bar").scaleResolutionDownBy = 2; + encodings.find(({rid}) => rid == "baz").scaleResolutionDownBy = 3; + await setParameters(test, emitter, encodings); + await checkResolutions(test, emitter); + }, + async function PC_LOCAL_CHANGE_PARAMS_3() { + encodings.find(({rid}) => rid == "foo").scaleResolutionDownBy = 6; + encodings.find(({rid}) => rid == "bar").scaleResolutionDownBy = 5; + encodings.find(({rid}) => rid == "baz").scaleResolutionDownBy = 4; + await setParameters(test, emitter, encodings); + await checkResolutions(test, emitter); + }, + async function PC_LOCAL_CHANGE_PARAMS_4() { + encodings.find(({rid}) => rid == "foo").scaleResolutionDownBy = 4; + encodings.find(({rid}) => rid == "bar").scaleResolutionDownBy = 1; + encodings.find(({rid}) => rid == "baz").scaleResolutionDownBy = 2; + await setParameters(test, emitter, encodings); + await checkResolutions(test, emitter); + }, + ]); + + await test.run(); + }); +</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..97a268d53c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html @@ -0,0 +1,185 @@ +<!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> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1231507", + title: "Basic video-only peer connection with Simulcast offer", + visible: true + }); + + function addRIDExtension(pc, extensionId) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDExtension(extensionId); + } + + function selectRecvRID(pc, rid) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDFilter(rid); + } + + runNetworkTest(() => + 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]).then(() => { + let emitter, helper; + + const test = new PeerConnectionTest({bundle: false}); + test.setMediaConstraints([{video: true}], []); + + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + emitter = new VideoFrameEmitter(); + helper = new VideoStreamHelper(); + test.pcLocal.attachLocalStream(emitter.stream()); + emitter.start(); + } + ]); + + test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [ + function PC_LOCAL_SET_RIDS(test) { + const senders = test.pcLocal._pc.getSenders(); + is(senders.length, 1, "We have exactly one RTP sender"); + const sender = senders[0]; + ok(sender.track, "Sender has a track"); + + return sender.setParameters({ + encodings: [{ rid: "foo", maxBitrate: 40000 }, + { rid: "bar", maxBitrate: 40000, scaleResolutionDownBy: 2 }] + }); + } + ]); + + test.chain.insertAfter('PC_LOCAL_GET_ANSWER', [ + function PC_LOCAL_ADD_RIDS_TO_ANSWER(test) { + test._remote_answer.sdp = sdputils.transferSimulcastProperties( + test.originalOffer.sdp, test._remote_answer.sdp); + info("Answer with RIDs: " + JSON.stringify(test._remote_answer)); + ok(test._remote_answer.sdp.match(/a=simulcast:/), "Modified answer has simulcast"); + ok(test._remote_answer.sdp.match(/a=rid:foo/), "Modified answer has rid foo"); + ok(test._remote_answer.sdp.match(/a=rid:bar/), "Modified answer has rid bar"); + ok(test._remote_answer.sdp.match(/urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id/), "Modified answer has RID"); + } + ]); + + // For storing the rid extension so it can be checked in the RTP + let ridExtensionId = 0; + + // do this after set local description so the MediaPipeline + // has been created. + test.chain.insertAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION',[ + function PC_REMOTE_SET_RTP_FIRST_RID(test) { + const extmap_id = test.originalOffer.sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"); + ok(extmap_id, "Original offer has extmap id for simulcast: " + extmap_id[1]); + ridExtensionId = extmap_id[1]; + // Cause pcRemote to filter out everything but RID "foo", only + // allowing one of the simulcast streams through. + addRIDExtension(test.pcRemote, extmap_id[1]); + selectRecvRID(test.pcRemote, "foo"); + } + ]); + + let getRtpPacket = (pc) => { + pc.mozEnablePacketDump(0, "rtp", false); + return new Promise((res, rej) => + pc.mozSetPacketCallback((...args) => { + res([...args]); + pc.mozSetPacketCallback(() => {}); + pc.mozDisablePacketDump(0, "rtp", false); + }) + ); + } + + test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [ + async function PC_REMOTE_CHECK_RID_IN_RTP() { + let pc = SpecialPowers.wrap(test.pcRemote._pc); + let [level, type, sending, data] = await getRtpPacket(pc); + let extensions = ParseRtpPacket(data).header.extensions; + ok(ridExtensionId, "RID extension ID has been extracted from SDP"); + let ridExt = extensions.find(e => e.id == ridExtensionId); + ok(ridExt, "RID is present in RTP."); + is(new TextDecoder('utf-8').decode(ridExt.data), "foo", + "RID is 'foo'."); + } + ]); + + test.chain.append([ + async function PC_REMOTE_WAIT_FOR_FRAMES() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_REMOTE_CHECK_SIZE_1() { + const vlocal = test.pcLocal.localMediaElements[0]; + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcLocal"); + ok(vremote, "Should have remote video element for pcRemote"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 48, + "sink is same width as source, modulo our cropping algorithm"); + is(vremote.videoHeight, 48, + "sink is same height as source, modulo our cropping algorithm"); + }, + function PC_REMOTE_SET_RTP_SECOND_RID(test) { + // Now, cause pcRemote to filter out everything but RID "bar", only + // allowing the other simulcast stream through. + selectRecvRID(test.pcRemote, "bar"); + }, + function PC_REMOTE_WAIT_FOR_SECOND_MEDIA_FLOW(test) { + return test.pcRemote.waitForMediaFlow(); + }, + async function PC_REMOTE_WAIT_FOR_FRAMES_2() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + // For some reason, even though we're getting a 25x25 stream, sometimes + // the resolution isn't updated on the video element on the first frame. + async function PC_REMOTE_WAIT_FOR_FRAMES_3() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_REMOTE_CHECK_SIZE_2() { + const vlocal = test.pcLocal.localMediaElements[0]; + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcLocal"); + ok(vremote, "Should have remote video element for pcRemote"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 24, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(vremote.videoHeight, 24, + "sink is 1/2 height of source, modulo our cropping algorithm"); + }, + ]); + + return test.run(); + }) + .catch(e => ok(false, "unexpected failure: " + e))); +</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..26d5f47817 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html @@ -0,0 +1,185 @@ +<!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> +</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 + }); + + function addRIDExtension(pc, extensionId) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDExtension(extensionId); + } + + function selectRecvRID(pc, rid) { + const receivers = pc._pc.getReceivers(); + is(receivers.length, 1, "We have exactly one RTP receiver"); + const receiver = receivers[0]; + + SpecialPowers.wrap(receiver).mozAddRIDFilter(rid); + } + + runNetworkTest(() => + 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]).then(() => { + let emitter, helper; + + const test = new PeerConnectionTest({bundle: false}); + test.setMediaConstraints([{video: true}], []); + + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_CANVAS_CAPTURESTREAM(test) { + emitter = new VideoFrameEmitter(); + helper = new VideoStreamHelper(); + test.pcLocal.attachLocalStream(emitter.stream()); + emitter.start(); + } + ]); + + test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [ + function PC_LOCAL_SET_RIDS(test) { + const senders = test.pcLocal._pc.getSenders(); + is(senders.length, 1, "We have exactly one RTP sender"); + const sender = senders[0]; + ok(sender.track, "Sender has a track"); + + return sender.setParameters({ + encodings: [{ rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 2 }, + { rid: "bar", maxBitrate: 40000 }] + }); + } + ]); + + test.chain.insertAfter('PC_LOCAL_GET_ANSWER', [ + function PC_LOCAL_ADD_RIDS_TO_ANSWER(test) { + test._remote_answer.sdp = sdputils.transferSimulcastProperties( + test.originalOffer.sdp, test._remote_answer.sdp); + info("Answer with RIDs: " + JSON.stringify(test._remote_answer)); + ok(test._remote_answer.sdp.match(/a=simulcast:/), "Modified answer has simulcast"); + ok(test._remote_answer.sdp.match(/a=rid:foo/), "Modified answer has rid foo"); + ok(test._remote_answer.sdp.match(/a=rid:bar/), "Modified answer has rid bar"); + ok(test._remote_answer.sdp.match(/urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id/), "Modified answer has RID"); + } + ]); + + // For storing the rid extension so it can be checked in the RTP + let ridExtensionId = 0; + + // do this after set local description so the MediaPipeline + // has been created. + test.chain.insertAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION',[ + function PC_REMOTE_SET_RTP_FIRST_RID(test) { + const extmap_id = test.originalOffer.sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"); + ok(extmap_id, "Original offer has extmap id for simulcast: " + extmap_id[1]); + ridExtensionId = extmap_id[1]; + // Cause pcRemote to filter out everything but RID "foo", only + // allowing one of the simulcast streams through. + addRIDExtension(test.pcRemote, extmap_id[1]); + selectRecvRID(test.pcRemote, "foo"); + } + ]); + + let getRtpPacket = (pc) => { + pc.mozEnablePacketDump(0, "rtp", false); + return new Promise((res, rej) => + pc.mozSetPacketCallback((...args) => { + res([...args]); + pc.mozSetPacketCallback(() => {}); + pc.mozDisablePacketDump(0, "rtp", false); + }) + ); + } + + test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [ + async function PC_REMOTE_CHECK_RID_IN_RTP() { + let pc = SpecialPowers.wrap(test.pcRemote._pc); + let [level, type, sending, data] = await getRtpPacket(pc); + let extensions = ParseRtpPacket(data).header.extensions; + ok(ridExtensionId, "RID extension ID has been extracted from SDP"); + let ridExt = extensions.find(e => e.id == ridExtensionId); + ok(ridExt, "RID is present in RTP."); + is(new TextDecoder('utf-8').decode(ridExt.data), "foo", + "RID is 'foo'."); + } + ]); + + test.chain.append([ + async function PC_REMOTE_WAIT_FOR_FRAMES() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_REMOTE_CHECK_SIZE_1() { + const vlocal = test.pcLocal.localMediaElements[0]; + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcLocal"); + ok(vremote, "Should have remote video element for pcRemote"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 24, + "sink is 1/2 width of source, modulo our cropping algorithm"); + is(vremote.videoHeight, 24, + "sink is 1/2 height of source, modulo our cropping algorithm"); + }, + function PC_REMOTE_SET_RTP_SECOND_RID(test) { + // Now, cause pcRemote to filter out everything but RID "bar", only + // allowing the other simulcast stream through. + selectRecvRID(test.pcRemote, "bar"); + }, + function PC_REMOTE_WAIT_FOR_SECOND_MEDIA_FLOW(test) { + return test.pcRemote.waitForMediaFlow(); + }, + async function PC_REMOTE_WAIT_FOR_FRAMES_2() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + // For some reason, even though we're getting a 25x25 stream, sometimes + // the resolution isn't updated on the video element on the first frame. + async function PC_REMOTE_WAIT_FOR_FRAMES_3() { + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vremote, "Should have remote video element for pcRemote"); + emitter.start(); + await helper.checkVideoPlaying(vremote); + emitter.stop(); + }, + function PC_REMOTE_CHECK_SIZE_2() { + const vlocal = test.pcLocal.localMediaElements[0]; + const vremote = test.pcRemote.remoteMediaElements[0]; + ok(vlocal, "Should have local video element for pcLocal"); + ok(vremote, "Should have remote video element for pcRemote"); + is(vlocal.videoWidth, 50, "correct source width"); + is(vlocal.videoHeight, 50, "correct source height"); + is(vremote.videoWidth, 48, + "sink is same width as source, modulo our cropping algorithm"); + is(vremote.videoHeight, 48, + "sink is same height as source, modulo our cropping algorithm"); + }, + ]); + + return test.run(); + }) + .catch(e => ok(false, "unexpected failure: " + e))); +</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..739675eab3 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html @@ -0,0 +1,838 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.js"></script> +</head> +<body> +<pre id="test"> +<script type="application/javascript"> + createHTML({ + bug: "1337525", + title: "webRtc Stats composition and sanity" + }); +const statsExpectedByType = { + "inbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "packetsReceived", + "packetsLost", + "bytesReceived", + "jitter", + ], + optional: [ + "remoteId", + "nackCount", + ], + localVideoOnly: [ + "discardedPackets", + "framerateStdDev", + "framerateMean", + "bitrateMean", + "bitrateStdDev", + "firCount", + "pliCount", + "framesDecoded", + ], + unimplemented: [ + "mediaTrackId", + "transportId", + "codecId", + "packetsDiscarded", + "associateStatsId", + "sliCount", + "packetsRepaired", + "fractionLost", + "burstPacketsLost", + "burstLossCount", + "burstDiscardCount", + "gapDiscardRate", + "gapLossRate", + "qpSum", // Not yet implemented for inbound media, see bug 1519590 + ], + deprecated: [ + "mozRtt", + "isRemote", + ], + }, + "outbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "packetsSent", + "bytesSent", + "remoteId", + ], + optional: [ + "nackCount", + ], + localVideoOnly: [ + "droppedFrames", + "bitrateMean", + "bitrateStdDev", + "framerateMean", + "framerateStdDev", + "framesEncoded", + "firCount", + "pliCount", + "qpSum", + ], + unimplemented: [ + "mediaTrackId", + "transportId", + "codecId", + "sliCount", + "targetBitrate", + ], + deprecated: [ + "isRemote", + ], + }, + "remote-inbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "packetsLost", + "jitter", + "localId", + ], + optional: [ + "roundTripTime", + "nackCount", + "packetsReceived", + "bytesReceived", + ], + unimplemented: [ + "mediaTrackId", + "transportId", + "codecId", + "packetsDiscarded", + "associateStatsId", + "sliCount", + "packetsRepaired", + "fractionLost", + "burstPacketsLost", + "burstLossCount", + "burstDiscardCount", + "gapDiscardRate", + "gapLossRate", + ], + deprecated: [ + "mozRtt", + "isRemote", + ], + }, + "remote-outbound-rtp": { + expected: [ + "id", + "timestamp", + "type", + "ssrc", + "mediaType", + "kind", + "packetsSent", + "bytesSent", + "localId", + "remoteTimestamp", + ], + optional: [ + "nackCount", + ], + unimplemented: [ + "mediaTrackId", + "transportId", + "codecId", + "sliCount", + "targetBitrate", + ], + deprecated: [ + "isRemote", + ], + }, + "csrc": { skip: true }, + "codec": { skip: true }, + "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 }, +}; + +["in", "out"].forEach(pre => { + let s = statsExpectedByType[pre + "bound-rtp"]; + s.optional = [...s.optional, ...s.localVideoOnly]; +}); + +// +// Checks that the fields in a report conform to the expectations in +// statExpectedByType +// +var 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."); + }); +}); + +var 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"); + + 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."); + } + + if (!isRemote && stat.inner.kind == "video") { + // firCount + ok(stat.firCount >= 0 && stat.firCount < 100, + stat.type + ".firCount is a sane number for a short test. value=" + + stat.firCount); + + // pliCount + ok(stat.pliCount >= 0 && stat.pliCount < 200, + stat.type + ".pliCount is a sane number for a short test. value=" + + stat.pliCount); + } + } + + if (stat.type == "inbound-rtp") { + // + // Required fields + // + + // packetsReceived + ok(stat.packetsReceived >= 0 + && stat.packetsReceived < 10 ** 5, + stat.type + ".packetsReceived is a sane number for a short test. value=" + + stat.packetsReceived); + + // 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 test. value=" + + stat.bytesReceived); + + // packetsLost + ok(stat.packetsLost < 100, + stat.type + ".packetsLost is a sane number for a short 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 local only test. value=" + + stat.jitter); + + // packetsDiscarded + // special exception for, TODO: Bug 1335967 + // if (!stat.inner.isRemote && stat.discardedPackets !== undefined) { + // ok(stat.packetsDiscarded < 100, stat.type + // + ".packetsDiscarded is a sane number for a short test. value=" + // + stat.packetsDiscarded); + // } + // if (stat.packetsDiscarded !== undefined) { + // ok(!stat.inner.isRemote, + // stat.type + ".packetsDiscarded is only set when isRemote is " + // + "false"); + // } + + // + // Optional fields + // + + // + // 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); + // framesDecoded + ok(stat.framesDecoded > 0 && stat.framesDecoded < 1000000, stat.type + + ".framesDecoded is a sane number for a short test. value=" + + stat.framesDecoded); + // bitrateMean + // special exception, TODO: Bug 1341533 + if (stat.bitrateMean !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.bitrateMean >= 0 && stat.bitrateMean < 2 ** 25, + // stat.type + ".bitrateMean is sane. value=" + // + stat.bitrateMean); + } + + // bitrateStdDev + // special exception, TODO Bug 1341533 + if (stat.bitrateStdDev !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.bitrateStdDev >= 0 && stat.bitrateStdDev < 2 ** 25, + // stat.type + ".bitrateStdDev is sane. value=" + // + stat.bitrateStdDev); + } + + // framerateMean + // special exception, TODO: Bug 1341533 + if (stat.framerateMean !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.framerateMean >= 0 && stat.framerateMean < 120, + // stat.type + ".framerateMean is sane. value=" + // + stat.framerateMean); + } + + // framerateStdDev + // special exception, TODO: Bug 1341533 + if (stat.framerateStdDev !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.framerateStdDev >= 0 && stat.framerateStdDev < 120, + // stat.type + ".framerateStdDev is sane. value=" + // + stat.framerateStdDev); + } + + } + } else if (stat.type == "remote-inbound-rtp") { + // roundTripTime + ok(stat.roundTripTime >= 0, stat.type + ".roundTripTime is sane with" + + "value of:" + stat.roundTripTime); + // + // Required fields + // + + // packetsLost + ok(stat.packetsLost < 100, + stat.type + ".packetsLost is a sane number for a short test. value=" + + stat.packetsLost); + + // jitter + ok(stat.jitter >= 0, + stat.type + ".jitter is sane number. 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 test. value=" + + stat.packetsReceived); + } + + // bytesReceived + if (stat.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 test. value=" + + stat.bytesReceived); + } + + } 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 test. value=" + + stat.packetsSent); + + // bytesSent + ok(stat.bytesSent, stat.type + ".bytesSent has a value." + + " Value not expected to be sane, bug 1339104. value=" + + stat.bytesSent); + + // + // Optional fields + // + + // + // 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"); + }); + + // bitrateMean + if (stat.bitrateMean !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.bitrateMean >= 0 && stat.bitrateMean < 2 ** 25, + // stat.type + ".bitrateMean is sane. value=" + // + stat.bitrateMean); + } + + // bitrateStdDev + if (stat.bitrateStdDev !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.bitrateStdDev >= 0 && stat.bitrateStdDev < 2 ** 25, + // stat.type + ".bitrateStdDev is sane. value=" + // + stat.bitrateStdDev); + } + + // framerateMean + if (stat.framerateMean !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.framerateMean >= 0 && stat.framerateMean < 120, + // stat.type + ".framerateMean is sane. value=" + // + stat.framerateMean); + } + + // framerateStdDev + if (stat.framerateStdDev !== undefined) { + // TODO: uncomment when Bug 1341533 lands + // ok(stat.framerateStdDev >= 0 && stat.framerateStdDev < 120, + // stat.type + ".framerateStdDev is sane. value=" + // + stat.framerateStdDev); + } + + // droppedFrames + ok(stat.droppedFrames >= 0 && stat.droppedFrames < 100000, + stat.type + ".droppedFrames is a sane number. value=" + + stat.droppedFrames); + + // framesEncoded + ok(stat.framesEncoded >= 0 && stat.framesEncoded < 100000, stat.type + + ".framesEncoded is a sane number for a short test. value=" + + stat.framesEncoded); + + // qpSum + // techinically optional but should be supported for all of our codecs (on the encode side, see bug 1519590) + ok(stat.qpSum >= 0, + `${stat.type} qpSum is a sane number. value=${stat.qpSum}`); + } + } 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 test. value=" + + stat.packetsSent); + + // bytesSent + ok(stat.bytesSent, stat.type + ".bytesSent has a value." + + " Value not expected to be sane, bug 1339104. value=" + + stat.bytesSent); + + ok(stat.remoteTimestamp !== undefined, `${stat.type}.remoteTimestamp ` + + ` is not undefined`); + + } 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); + + // localCandidateId + ok(stat.localCandidateId, + stat.type + ".localCandidateId has a value. value=" + + stat.localCandidateId); + + // remoteCandidateId + ok(stat.remoteCandidateId, + stat.type + ".remoteCandidateId has a value. value=" + + stat.remoteCandidateId); + + // priority + ok(stat.priority, + stat.type + ".priority has a value. value=" + + stat.priority); + + // readable + ok(stat.readable, + stat.type + ".readable is true. value=" + + stat.readable); + + // writable + ok(stat.writable, + stat.type + ".writable is true. value=" + + stat.writable); + + // 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); + + // bytesSent + ok(stat.bytesSent > 5000, + stat.type + ".bytesSent is a sane number (>5,000) for a short test. value=" + + stat.bytesSent); + + // bytesReceived + ok(stat.bytesReceived > 5000, + stat.type + ".bytesReceived is a sane number (>5,000) for a short test. value=" + + stat.bytesReceived); + + // lastPacketSentTimestamp + ok(stat.lastPacketSentTimestamp, + stat.type + ".lastPacketSentTimestamp has a value. value=" + + stat.lastPacketSentTimestamp); + + // lastPacketReceivedTimestamp + ok(stat.lastPacketReceivedTimestamp, + stat.type + ".lastPacketReceivedTimestamp has a value. value=" + + stat.lastPacketReceivedTimestamp); + + } else { + info("candidate-pair is _not_ both state == succeeded and selected"); + // nominated + ok(stat.nominated !== undefined, + stat.type + ".nominated exists. value=" + + stat.nominated); + ok(stat.bytesSent !== undefined, + stat.type + ".bytesSent exists. value=" + + stat.bytesSent); + ok(stat.bytesReceived !== undefined, + stat.type + ".bytesReceived exists. value=" + + stat.bytesReceived); + ok(stat.lastPacketSentTimestamp !== undefined, + stat.type + ".lastPacketSentTimestamp exists. value=" + + stat.lastPacketSentTimestamp); + ok(stat.lastPacketReceivedTimestamp !== undefined, + stat.type + ".lastPacketReceivedTimestamp exists. value=" + + stat.lastPacketReceivedTimestamp); + } + + + // + // 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); + + } 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}`); + + // protocol + ok(stat.protocol, `${stat.type} has protocol. value=${stat.protocol}`); + + // port + ok(stat.port >= 0, `${stat.type} has port >= 0. value=${stat.port}`); + ok(stat.port <= 65535, `${stat.type} has port <= 65535. value=${stat.port}`); + + // candidateType + ok(stat.candidateType, `${stat.type} has candidateType. value=${stat.candidateType}`); + + // priority + ok(stat.priority > 0 && stat.priority < (2**32 - 1), + `${stat.type} has priority between 1 and 2^32 - 1 inc. ` + + `value=${stat.priority}`); + + // relayProtocol + if (stat.type == "local-candidate" && stat.candidateType == "relay") { + ok(stat.relayProtocol, + `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol}`); + } else { + is(stat.relayProtocol, undefined, + `relayProtocol is undefined for candidates that are not relay and ` + + `local. value=${stat.relayProtocol}`); + } + + // proxied + if (stat.proxied) { + ok(stat.proxied == "proxied" || stat.proxied == "non-proxied", + `${stat.type} has proxied. value=${stat.proxied}`); + } + } + + // + // Ensure everything was tested + // + [...expectations.expected, ...expectations.optional].forEach(field => { + ok(Object.keys(tested).includes(field), stat.type + "." + field + + " was tested."); + }); + }); +} + +var PC_LOCAL_TEST_LOCAL_STATS = test => { + return test.pcLocal.waitForSyncedRtcp().then(stats => { + checkExpectedFields(stats); + pedanticChecks(stats); + }); +} + +var PC_REMOTE_TEST_REMOTE_STATS = test => { + return test.pcRemote.waitForSyncedRtcp().then(stats => { + checkExpectedFields(stats); + pedanticChecks(stats); + }); +} + +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_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}]); + 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..ff43844598 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="pc.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 test.pcLocal.waitForSyncedRtcp()); +} + +const PC_REMOTE_TEST_REMOTE_JITTER = async test => { + checkJitter(await test.pcRemote.waitForSyncedRtcp()); +} + +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}]); + 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..0d52cf5164 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html @@ -0,0 +1,60 @@ +<!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 }]); + test.run(); + }, { useIceServer: true }); +} + +var 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["srvflx-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"); + // TURN/TLS does not work, see https://bugzilla.mozilla.org/show_bug.cgi?id=1323439 + // With TURN/TLS working, we should have exactly five entries in haveRelayProtocol. + todo(haveRelayProtocol["relay-tls"], "Has TLS relay candidate. See https://bugzilla.mozilla.org/show_bug.cgi?id=1323439"); + is(Object.keys(haveRelayProtocol).length, 4, "All candidate types are accounted for"); + }); +} +</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..96b6fe77b8 --- /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(() => { + var 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"); + 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..e64258f549 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html @@ -0,0 +1,57 @@ +<!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 new Promise(resolve => setTimeout(resolve, 1000)); + networkTestFinished(); +}; + +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..d7638746fb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html @@ -0,0 +1,135 @@ +<!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"; + + var pc1 = new RTCPeerConnection(); + var pc2 = new RTCPeerConnection(); + var pc3 = new RTCPeerConnection(); + + var 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()); + + var ice1Finished, ice2Finished, ice3Finished; + var ice1Done = new Promise(r => ice1Finished = r); + var ice2Done = new Promise(r => ice2Finished = r); + var ice3Done = new Promise(r => ice3Finished = r); + + var icsc = (pc, str, resolve) => { + var 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."); + networkTestFinished(); + }); +</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..ce824688c8 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html @@ -0,0 +1,80 @@ +<!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 () { + function finish() { + window.onerror = oldOnError; + is(error_count, 7, "Seven expected errors verified."); + networkTestFinished(); + } + + 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"); + return false; + } + + 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"); + }); +}); + +</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..52a619c472 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html @@ -0,0 +1,40 @@ +<!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."); + } + networkTestFinished(); + }); +</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..e183721bc2 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html @@ -0,0 +1,108 @@ +<!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 () => { + 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 video to be somewhat green and + // audio to have a large 1000Hz component (or 440Hz if using fake devices). + ['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() { + var h = new CaptureStreamTestHelper2D(); + var localVideo = test.pcLocal.localMediaElements + .find(e => e instanceof HTMLVideoElement); + var remoteVideo = test.pcRemote.remoteMediaElements + .find(e => e instanceof HTMLVideoElement); + // We check a pixel somewhere away from the top left corner since + // MediaEngineDefault 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. + var checkVideoEnabled = video => h.waitForPixel(video, + px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)), + { offsetX, offsetY } + ); + var checkVideoDisabled = video => h.waitForPixel(video, + px => (px[3] = 255, 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() { + var ac = new AudioContext(); + var localAnalyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]); + var remoteAnalyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]); + + var checkAudio = (analyser, fun) => analyser.waitForAnalysisSuccess(fun); + + var freq = localAnalyser.binIndexForFrequency(TEST_AUDIO_FREQ); + var checkAudioEnabled = analyser => + checkAudio(analyser, array => array[freq] > 200); + var 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); + }, + ]); + 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..4c134dec60 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html @@ -0,0 +1,162 @@ +<!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 + // MediaEngineDefault 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, h.isPixelNot(px, h.black, threshold)), + { offsetX, offsetY } + ); + var checkVideoDisabled = video => h.waitForPixel(video, + px => (px[3] = 255, 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); + }, + ]); + 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..c6ef458e10 --- /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> +</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. + + var test; + runNetworkTest(function (options) { + 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) { + return Promise.all([test.pcLocal.waitForSyncedRtcp(), + test.pcRemote.waitForSyncedRtcp()]) + .then(async () => { + 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}], []); + 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..a886231636 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.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: "1091242", + title: "Multistream: Two audio streams" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {audio: true}], + [{audio: true}, {audio: true}]); + 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..cf025d023a --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.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: "1145407", + title: "Multistream: Two audio tracks in one stream" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + 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..6264e7cc86 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.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: "1091242", + title: "Multistream: Two audio streams, two video streams" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true}, {video: true}, {audio: true}, + {video: true}], + [{audio: true}, {video: true}, {audio: true}, + {video: true}]); + 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..61e0313b4f --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.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: "1091242", + title: "Multistream: Two audio/video streams" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{audio: true, video: true}, + {audio: true, video: true}], + [{audio: true, video: true}, + {audio: true, video: true}]); + 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..102c2f11ce --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.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: "1091242", + title: "Multistream: Two video streams" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: true}, {video: true}], + [{video: true}, {video: true}]); + 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..5aae44175d --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.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: "1145407", + title: "Multistream: Two video tracks in offerer stream" + }); + + var test; + runNetworkTest(function (options) { + 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}]); + 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..12b4404720 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html @@ -0,0 +1,100 @@ +<!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" + }); + + var test; + runNetworkTest(function (options) { + test = new PeerConnectionTest(options); + var helper = new AudioStreamHelper(); + + 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}], []); + 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..889bc4f1cb --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.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: "1264479", + title: "PeerConnection verify current and pending descriptions" + }); + + 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; + + runNetworkTest(function() { + v1 = createMediaElement('video', 'v1'); + v2 = createMediaElement('video', 'v2'); + + 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"); + }) + .catch(reason => ok(false, "unexpected failure: " + reason)) + .then(networkTestFinished); + }); +</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..2570d98716 --- /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]); + } + + var test = new PeerConnectionTest(); + + var h1 = new CaptureStreamTestHelper2D(50, 50); + var canvas1 = h1.createAndAppendElement('canvas', 'source_canvas1'); + var stream1; + var vremote1; + + var h2 = new CaptureStreamTestHelper2D(50, 50); + var canvas2; + var stream2; + var 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); + var 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", + }); + } + ]); + + 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..d7e5629649 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html @@ -0,0 +1,95 @@ +<!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: "1395853", + title: "Verify video content over WebRTC for every video codec", + }); + + async function testVideoCodec(options = {}, codec) { + let test = new PeerConnectionTest(options); + test.setMediaConstraints([{video: 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.name, otherCodec.offset); + let otherRtpmapMatcher = new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi"); + + let id = sdputils.findCodecId(test.originalOffer.sdp, codec.name, codec.offset); + 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.append([ + async function CHECK_VIDEO_FLOW() { + try { + let 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}`); + } + }, + ]); + + // This inlines test.run(), to allow for multiple tests to run. + test.updateChainSteps(); + await test.chain.execute(); + await test.close(); + } + + // 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}`); + try { + await testVideoCodec(options, codec); + } catch(e) { + ok(false, `Error in test for codec ${codec.name}: ${e}\n${e.stack}`); + } + info(`Tested video for codec ${codec.name}`); + } + + networkTestFinished(); + }); +</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..8aa0782953 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html @@ -0,0 +1,96 @@ +<!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" + }); + + 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]); + } + + const emitter = new VideoFrameEmitter(); + const helper = new VideoStreamHelper(); + + 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}], []); + 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..1873ab4a77 --- /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() { + var test = new PeerConnectionTest(); + test.audioContext = new AudioContext(); + test.setMediaConstraints([{audio: true}], []); + test.chain.replace("PC_LOCAL_GUM", [ + function PC_LOCAL_WEBAUDIO_SOURCE(test) { + var oscillator = test.audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.value = 700; + oscillator.start(); + var 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); + } + ]); + 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..831e7c2b96 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_selftest.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({ + 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"); + networkTestFinished(); +}); + +</script> +</pre> +</body> +</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..e307edaf13 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId.html @@ -0,0 +1,56 @@ +<!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", + }); + + /** + * 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; + } + + const allDevices = await navigator.mediaDevices.enumerateDevices(); + const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput'); + info(`Found ${audioDevices.length} output devices`); + ok(audioDevices.length > 0, "More than one output device 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"); + } + }); + +</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..715d658b5c --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html @@ -0,0 +1,53 @@ +<!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]); + + if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) { + ok(false, "No loopback device set by framework. Try --use-test-media-devices"); + return; + } + + 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..57cba7cf79 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html @@ -0,0 +1,106 @@ +<!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(); + if (devices.length > 3) { + // Temporary code to debug an intermitten failure. + let s = await navigator.mediaDevices.getUserMedia({audio: true}); + let d = await navigator.mediaDevices.enumerateDevices(); + d.forEach(e => info(e.kind + ": " + e.label)); + s.getTracks()[0].stop(); + } + is(devices.length, 3, "Three fake devices found (mic, video, speaker)."); + let audios = devices.filter(d => d.kind == "audiooutput"); + is(audios.length, 1, "One output device 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]); + + 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 stream by calling a gUM and using the loopack devices. + let verifyStream = await getUserMedia({audio: true}); + // We gonna test our tone, stop the auto created one. + DefaultLoopbackTone.stop(); + 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/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 = {}; |