summaryrefslogtreecommitdiffstats
path: root/dom/media/tests/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/tests/mochitest')
-rw-r--r--dom/media/tests/mochitest/NetworkPreparationChromeScript.js62
-rw-r--r--dom/media/tests/mochitest/blacksilence.js121
-rw-r--r--dom/media/tests/mochitest/dataChannel.js191
-rw-r--r--dom/media/tests/mochitest/head.js970
-rw-r--r--dom/media/tests/mochitest/identity/identityPcTest.js53
-rw-r--r--dom/media/tests/mochitest/identity/idp-bad.js1
-rw-r--r--dom/media/tests/mochitest/identity/idp-min.js24
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-http-trick.js3
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-http-trick.js^headers^2
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-http.js3
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-http.js^headers^2
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https-double.js3
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https-double.js^headers^2
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js3
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js^headers^2
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https.js3
-rw-r--r--dom/media/tests/mochitest/identity/idp-redirect-https.js^headers^2
-rw-r--r--dom/media/tests/mochitest/identity/idp.js110
-rw-r--r--dom/media/tests/mochitest/identity/idp.sjs18
-rw-r--r--dom/media/tests/mochitest/identity/login.html31
-rw-r--r--dom/media/tests/mochitest/identity/mochitest.ini42
-rw-r--r--dom/media/tests/mochitest/identity/test_fingerprints.html112
-rw-r--r--dom/media/tests/mochitest/identity/test_getIdentityAssertion.html96
-rw-r--r--dom/media/tests/mochitest/identity/test_idpproxy.html176
-rw-r--r--dom/media/tests/mochitest/identity/test_loginNeeded.html71
-rw-r--r--dom/media/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html31
-rw-r--r--dom/media/tests/mochitest/identity/test_peerConnection_peerIdentity.html21
-rw-r--r--dom/media/tests/mochitest/identity/test_setIdentityProvider.html63
-rw-r--r--dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html55
-rw-r--r--dom/media/tests/mochitest/mediaStreamPlayback.js252
-rw-r--r--dom/media/tests/mochitest/mochitest.ini272
-rw-r--r--dom/media/tests/mochitest/network.js20
-rw-r--r--dom/media/tests/mochitest/nonTrickleIce.js71
-rw-r--r--dom/media/tests/mochitest/pc.js1878
-rw-r--r--dom/media/tests/mochitest/sdpUtils.js148
-rw-r--r--dom/media/tests/mochitest/steeplechase.ini11
-rw-r--r--dom/media/tests/mochitest/steeplechase_long/long.js217
-rw-r--r--dom/media/tests/mochitest/steeplechase_long/steeplechase_long.ini12
-rw-r--r--dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudioVideoCombined_long.html38
-rw-r--r--dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudio_long.html38
-rw-r--r--dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicVideo_long.html38
-rw-r--r--dom/media/tests/mochitest/templates.js525
-rw-r--r--dom/media/tests/mochitest/test_a_noOp.html32
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicAudio.html25
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicAudioVideo.html26
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html26
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html27
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicDataOnly.html24
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_basicVideo.html25
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_bug1013809.html27
-rw-r--r--dom/media/tests/mochitest/test_dataChannel_noOffer.html32
-rw-r--r--dom/media/tests/mochitest/test_enumerateDevices.html134
-rw-r--r--dom/media/tests/mochitest/test_enumerateDevices_iframe.html23
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_active_autoplay.html61
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html169
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_addtrack_removetrack_events.html110
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_audioCapture.html109
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicAudio.html27
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html87
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html70
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicVideo.html30
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html30
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html42
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html42
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_bug1223696.html52
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_callbacks.html35
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_constraints.html153
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_getTrackById.html50
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html39
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_loadedmetadata.html39
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_audio.html117
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html185
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_video.html130
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html251
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaStreamConstructors.html171
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_mediaStreamTrackClone.html170
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html38
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html25
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html26
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html26
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_scarySources.html66
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_spinEventLoop.html28
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html27
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html35
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html28
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html38
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html28
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html35
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_trackCloneCleanup.html30
-rw-r--r--dom/media/tests/mochitest/test_getUserMedia_trackEnded.html60
-rw-r--r--dom/media/tests/mochitest/test_ondevicechange.html80
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addAudioTrackToExistingVideoStream.html54
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addDataChannel.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html45
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addIceCandidate.html103
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html42
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html42
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html80
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html59
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudio.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html36
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelay.html44
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelayTCP.html37
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioNATSrflx.html41
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioPcmaPcmuOnly.html39
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioRequireEOC.html35
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioVideo.html24
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html24
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html39
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoRtcpMux.html38
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicH264Video.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicScreenshare.html36
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicVideo.html23
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_basicWindowshare.html36
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug1013809.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug1042791.html36
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug1064223.html31
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug1227781.html33
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug822674.html27
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug825703.html162
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug827843.html70
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_bug834153.html37
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_callbacks.html92
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html60
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html114
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_capturedVideo.html87
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_certificates.html180
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_close.html147
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_closeDuringIce.html79
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_constructedStream.html70
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html59
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_forwarding_basicAudioVideoCombined.html42
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_iceFailure.html83
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_insertDTMF.html79
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html57
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_localRollback.html51
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_multiple_captureStream_canvas_2d.html99
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_noTrickleOffer.html25
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html26
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html23
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html23
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html23
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html63
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_relayOnly.html61
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html69
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_remoteRollback.html55
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html39
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html42
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html39
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html42
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html35
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_renderAfterRenegotiation.html85
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_replaceTrack.html177
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html46
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIce.html41
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html98
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIceLocalRollback.html74
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIceNoBundle.html43
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIceNoBundleNoRtcpMux.html44
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_restartIceNoRtcpMux.html43
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_scaleResolution.html85
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html33
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setParameters.html83
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html34
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html33
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html142
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_syncSetDescription.html53
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html79
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_toJSON.html40
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_trackDisabling.html98
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_trackDisabling_clones.html149
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoAudioStreams.html24
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html50
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreams.html27
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreamsCombined.html27
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoVideoStreams.html24
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html50
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html100
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html100
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html73
-rw-r--r--dom/media/tests/mochitest/test_peerConnection_webAudio.html43
-rw-r--r--dom/media/tests/mochitest/test_selftest.html38
-rw-r--r--dom/media/tests/mochitest/turnConfig.js16
192 files changed, 14519 insertions, 0 deletions
diff --git a/dom/media/tests/mochitest/NetworkPreparationChromeScript.js b/dom/media/tests/mochitest/NetworkPreparationChromeScript.js
new file mode 100644
index 000000000..1de778778
--- /dev/null
+++ b/dom/media/tests/mochitest/NetworkPreparationChromeScript.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const { Services } = Cu.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.
+function enableDataConnection() {
+ let setLock = browser.navigator.mozSettings.createLock();
+ setLock.set({
+ 'ril.data.enabled': true,
+ 'ril.data.apnSettings': [
+ [
+ {'carrier':'T-Mobile US',
+ 'apn':'epc.tmobile.com',
+ 'mmsc':'http://mms.msg.eng.t-mobile.com/mms/wapenc',
+ 'types':['default','supl','mms']}
+ ]
+ ]
+ });
+}
+
+// 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();
+ enableDataConnection();
+});
+
+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/tests/mochitest/blacksilence.js b/dom/media/tests/mochitest/blacksilence.js
new file mode 100644
index 000000000..cad3b5515
--- /dev/null
+++ b/dom/media/tests/mochitest/blacksilence.js
@@ -0,0 +1,121 @@
+(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/tests/mochitest/dataChannel.js b/dom/media/tests/mochitest/dataChannel.js
new file mode 100644
index 000000000..fc0251469
--- /dev/null
+++ b/dom/media/tests/mochitest/dataChannel.js
@@ -0,0 +1,191 @@
+/* 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(channel.readyState, "connecting", channel + " is in state: 'connecting'");
+
+ 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";
+
+ 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" });
+
+ 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 => {
+ var sourceChannel = result.local;
+ var targetChannel = result.remote;
+ is(sourceChannel.readyState, "open", sourceChannel + " is in state: 'open'");
+ is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
+
+ is(targetChannel.binaryType, "blob", targetChannel + " is of binary type 'blob'");
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the Omega";
+
+ 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]
+ };
+
+ 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]
+ };
+
+ 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(test) {
+ var options = {
+ negotiated:true,
+ id: 5,
+ protocol: "foo/bar",
+ ordered: false,
+ maxRetransmits: 500
+ };
+ return test.createDataChannel(options).then(result => {
+ var sourceChannel2 = result.local;
+ var targetChannel2 = result.remote;
+ is(sourceChannel2.readyState, "open", sourceChannel2 + " is in state: 'open'");
+ is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
+
+ is(targetChannel2.binaryType, "blob", targetChannel2 + " is of binary type 'blob'");
+
+ is(sourceChannel2.id, options.id, sourceChannel2 + " id is:" + sourceChannel2.id);
+ var reliable = !options.ordered ? false : (options.maxRetransmits || options.maxRetransmitTime);
+ is(sourceChannel2.protocol, options.protocol, sourceChannel2 + " protocol is:" + sourceChannel2.protocol);
+ is(sourceChannel2.reliable, reliable, sourceChannel2 + " reliable is:" + sourceChannel2.reliable);
+ /*
+ These aren't exposed by IDL yet
+ is(sourceChannel2.ordered, options.ordered, sourceChannel2 + " ordered is:" + sourceChannel2.ordered);
+ is(sourceChannel2.maxRetransmits, options.maxRetransmits, sourceChannel2 + " maxRetransmits is:" +
+ sourceChannel2.maxRetransmits);
+ is(sourceChannel2.maxRetransmitTime, options.maxRetransmitTime, sourceChannel2 + " maxRetransmitTime is:" +
+ sourceChannel2.maxRetransmitTime);
+ */
+
+ is(targetChannel2.id, options.id, targetChannel2 + " id is:" + targetChannel2.id);
+ is(targetChannel2.protocol, options.protocol, targetChannel2 + " protocol is:" + targetChannel2.protocol);
+ is(targetChannel2.reliable, reliable, targetChannel2 + " reliable is:" + targetChannel2.reliable);
+ /*
+ These aren't exposed by IDL yet
+ is(targetChannel2.ordered, options.ordered, targetChannel2 + " ordered is:" + targetChannel2.ordered);
+ is(targetChannel2.maxRetransmits, options.maxRetransmits, targetChannel2 + " maxRetransmits is:" +
+ targetChannel2.maxRetransmits);
+ is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
+ targetChannel2.maxRetransmitTime);
+ */
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the walrus; Goo goo g'joob";
+
+ 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 = 512*1024; // SCTP internal buffer is 256K, so we'll have ~256K queued
+ var buffer = new ArrayBuffer(size);
+ // note: type received is always blob for binary data
+ var options = {};
+ options.bufferedAmountLowThreshold = 64*1024;
+ 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(commandsCheckLargeXfer);
+ chain.append(commandsCheckDataChannel);
+}
diff --git a/dom/media/tests/mochitest/head.js b/dom/media/tests/mochitest/head.js
new file mode 100644
index 000000000..e4d86c750
--- /dev/null
+++ b/dom/media/tests/mochitest/head.js
@@ -0,0 +1,970 @@
+/* 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 whether we are using fake streams to run this automation
+var FAKE_ENABLED = true;
+var TEST_AUDIO_FREQ = 1000;
+try {
+ var audioDevice = SpecialPowers.getCharPref('media.audio_loopback_dev');
+ var videoDevice = SpecialPowers.getCharPref('media.video_loopback_dev');
+ dump('TEST DEVICES: Using media devices:\n');
+ dump('audio: ' + audioDevice + '\nvideo: ' + videoDevice + '\n');
+ FAKE_ENABLED = false;
+ TEST_AUDIO_FREQ = 440;
+} catch (e) {
+ dump('TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n');
+ FAKE_ENABLED = true;
+}
+
+/**
+ * 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: function() {
+ 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: function() {
+ 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: function() {
+ 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: function() {
+ 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).
+ *
+ * @param {function} analysisFunction
+ * A fonction that performs an analysis, and returns true if the
+ * analysis was a success (i.e. it found what it was looking for)
+ */
+ waitForAnalysisSuccess: function(analysisFunction) {
+ var self = this;
+ return new Promise((resolve, reject) => {
+ function analysisLoop() {
+ var success = analysisFunction(self.getByteFrequencyData());
+ if (success) {
+ resolve();
+ return;
+ }
+ // else, we need more time
+ requestAnimationFrame(analysisLoop);
+ }
+ // We need to give the Analyser some time to start gathering data.
+ wait(200).then(analysisLoop);
+ });
+ },
+
+ /**
+ * 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: function(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: function(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');
+ 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) {
+ 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 setupEnvironment() {
+ if (!window.SimpleTest) {
+ // Running under Steeplechase
+ return;
+ }
+
+ 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.navigator.permission.disabled', true],
+ ['media.navigator.streams.fake', FAKE_ENABLED],
+ ['media.getusermedia.screensharing.enabled', true],
+ ['media.getusermedia.screensharing.allowed_domains', "mochi.test"],
+ ['media.getusermedia.audiocapture.enabled', true],
+ ['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.enabled", true]
+ );
+ }
+
+ // 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();
+}
+
+// This is called by steeplechase; which provides the test configuration options
+// directly to the test through this function. If we're not on steeplechase,
+// the test is configured directly and immediately.
+function run_test(is_initiator,timeout) {
+ var options = { is_local: is_initiator,
+ is_remote: !is_initiator };
+
+ setTimeout(() => {
+ unexpectedEventArrived(new Error("PeerConnectionTest timed out after "+timeout+"s"));
+ }, timeout);
+
+ // Also load the steeplechase test code.
+ var s = document.createElement("script");
+ s.src = "/test.js";
+ s.onload = () => setTestOptions(options);
+ document.head.appendChild(s);
+}
+
+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 kind and id 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');
+ }
+ } 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");
+}
+
+/*** 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, false);
+ resolve(result);
+ }
+ }, false));
+};
+
+/* 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);
+ }
+};
+
+
+/*** 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 `cancelPromise` and resolve 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} cancelPromise
+ * 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, cancelPromise) {
+ var listener;
+ var counter = count || 1;
+ return Promise.race([
+ (cancelPromise || 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 `cancelPromise` and
+ * resolve 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} cancelPromise
+ * 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, cancelPromise) {
+ return haveEvents(target, name, 1, cancelPromise);
+};
+
+/**
+ * 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} cancelPromise
+ * 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, cancelPromise) {
+ return haveEvents(target, name, count, cancelPromise)
+ .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: function () {
+ 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: function(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: function(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: function(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: function(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 1, all, occurrence);
+ },
+
+ /**
+ * Inserts the new commands after every occurrence of the specified command
+ */
+ insertAfterEach: function(functionOrName, commands) {
+ this._insertHelper(functionOrName, commands, 1, true);
+ },
+
+ /**
+ * Inserts the new commands before the specified command.
+ */
+ insertBefore: function(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 0, all, occurrence);
+ },
+
+ _insertHelper: function(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: function(functionOrName, occurrence) {
+ return this.commands.splice(this.mustHaveIndexOf(functionOrName, occurrence), 1);
+ },
+
+ /**
+ * Removes all commands after the specified one, returns what was removed.
+ */
+ removeAfter: function(functionOrName, occurrence) {
+ return this.commands.splice(this.mustHaveIndexOf(functionOrName, occurrence) + 1);
+ },
+
+ /**
+ * Removes all commands before the specified one, returns what was removed.
+ */
+ removeBefore: function(functionOrName, occurrence) {
+ return this.commands.splice(0, this.mustHaveIndexOf(functionOrName, occurrence));
+ },
+
+ /**
+ * Replaces a single command, returns what was removed.
+ */
+ replace: function(functionOrName, commands) {
+ this.insertBefore(functionOrName, commands);
+ return this.remove(functionOrName);
+ },
+
+ /**
+ * Replaces all commands after the specified one, returns what was removed.
+ */
+ replaceAfter: function(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: function(functionOrName, commands) {
+ var oldCommands = this.removeBefore(functionOrName);
+ this.insertBefore(functionOrName, commands);
+ return oldCommands;
+ },
+
+ /**
+ * Remove all commands whose name match the specified regex.
+ */
+ filterOut: function (id_match) {
+ this.commands = this.commands.filter(c => !id_match.test(c.name));
+ },
+};
+
+function AudioStreamHelper() {
+ this._context = new AudioContext();
+}
+
+AudioStreamHelper.prototype = {
+ checkAudio: function(stream, analyser, fun) {
+ /*
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(fun)
+ .then(() => analyser.disableDebugCanvas());
+ */
+ return analyser.waitForAnalysisSuccess(fun);
+ },
+
+ checkAudioFlowing: function(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: function(stream) {
+ var analyser = new AudioStreamAnalyser(this._context, stream);
+ var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return this.checkAudio(stream, analyser, array => array[freq] < 50);
+ }
+}
+
+function VideoStreamHelper() {
+ this._helper = new CaptureStreamTestHelper2D(50,50);
+ this._canvas = this._helper.createAndAppendElement('canvas', 'source_canvas');
+ // Make sure this is initted
+ this._helper.drawColor(this._canvas, this._helper.green);
+ this._stream = this._canvas.captureStream(10);
+}
+
+VideoStreamHelper.prototype = {
+ stream: function() {
+ return this._stream;
+ },
+
+ startCapturingFrames: function() {
+ var i = 0;
+ var helper = this;
+ return setInterval(function() {
+ try {
+ helper._helper.drawColor(helper._canvas,
+ i ? helper._helper.green : helper._helper.red);
+ i = 1 - i;
+ helper._stream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ },
+
+ waitForFrames: function(canvas, timeout_value) {
+ var intervalId = this.startCapturingFrames();
+ timeout_value = timeout_value || 8000;
+
+ return addFinallyToPromise(timeout(
+ Promise.all([
+ this._helper.waitForPixelColor(canvas, this._helper.green, 128,
+ canvas.id + " should become green"),
+ this._helper.waitForPixelColor(canvas, this._helper.red, 128,
+ canvas.id + " should become red")
+ ]),
+ timeout_value,
+ "Timed out waiting for frames")).finally(() => clearInterval(intervalId));
+ },
+
+ verifyNoFrames: function(canvas) {
+ return this.waitForFrames(canvas).then(
+ () => ok(false, "Color should not change"),
+ () => ok(true, "Color should not change")
+ );
+ }
+}
+
+
+function IsMacOSX10_6orOlder() {
+ if (navigator.platform.indexOf("Mac") !== 0) {
+ return false;
+ }
+
+ var version = Cc["@mozilla.org/system-info;1"]
+ .getService(Ci.nsIPropertyBag2)
+ .getProperty("version");
+ // the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
+ // Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
+ // is the Darwin version.
+ return (parseFloat(version) < 11.0);
+}
+
+(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/tests/mochitest/identity/identityPcTest.js b/dom/media/tests/mochitest/identity/identityPcTest.js
new file mode 100644
index 000000000..e5fcbc5db
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/identityPcTest.js
@@ -0,0 +1,53 @@
+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', 'idp.js');
+ test.pcRemote.setIdentityProvider('test2.example.com', '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];
+ return Promise.all([
+ audioIsSilence(true, remoteStream),
+ videoIsBlack(true, remoteStream)
+ ]);
+ }
+ ]);
+ test.run();
+}
diff --git a/dom/media/tests/mochitest/identity/idp-bad.js b/dom/media/tests/mochitest/identity/idp-bad.js
new file mode 100644
index 000000000..86e1cb7a3
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-bad.js
@@ -0,0 +1 @@
+<This isn't valid JS>
diff --git a/dom/media/tests/mochitest/identity/idp-min.js b/dom/media/tests/mochitest/identity/idp-min.js
new file mode 100644
index 000000000..8e228b688
--- /dev/null
+++ b/dom/media/tests/mochitest/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: function(payload, origin, usernameHint) {
+ dump('idp: generateAssertion(' + payload + ')\n');
+ return Promise.resolve({
+ idp: { domain: 'example.com', protocol: 'idp.js' },
+ assertion: 'bogus'
+ });
+ },
+
+ validateAssertion: function(assertion, origin) {
+ dump('idp: validateAssertion(' + assertion + ')\n');
+ return Promise.resolve({
+ identity: 'user@example.com',
+ contents: 'bogus'
+ });
+ }
+ });
+}(this));
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-http-trick.js b/dom/media/tests/mochitest/identity/idp-redirect-http-trick.js
new file mode 100644
index 000000000..5d35ac0ca
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-redirect-http-trick.js
@@ -0,0 +1,3 @@
+(function() {
+ dump('ERROR\n');
+}());
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-http-trick.js^headers^ b/dom/media/tests/mochitest/identity/idp-redirect-http-trick.js^headers^
new file mode 100644
index 000000000..b3a2afd90
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/idp-redirect-http.js b/dom/media/tests/mochitest/identity/idp-redirect-http.js
new file mode 100644
index 000000000..5d35ac0ca
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-redirect-http.js
@@ -0,0 +1,3 @@
+(function() {
+ dump('ERROR\n');
+}());
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-http.js^headers^ b/dom/media/tests/mochitest/identity/idp-redirect-http.js^headers^
new file mode 100644
index 000000000..d2380984e
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/idp-redirect-https-double.js b/dom/media/tests/mochitest/identity/idp-redirect-https-double.js
new file mode 100644
index 000000000..5d35ac0ca
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-redirect-https-double.js
@@ -0,0 +1,3 @@
+(function() {
+ dump('ERROR\n');
+}());
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-https-double.js^headers^ b/dom/media/tests/mochitest/identity/idp-redirect-https-double.js^headers^
new file mode 100644
index 000000000..3fb8a35ae
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/idp-redirect-https-odd-path.js b/dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js
new file mode 100644
index 000000000..5d35ac0ca
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js
@@ -0,0 +1,3 @@
+(function() {
+ dump('ERROR\n');
+}());
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js^headers^ b/dom/media/tests/mochitest/identity/idp-redirect-https-odd-path.js^headers^
new file mode 100644
index 000000000..6e2931eda
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/idp-redirect-https.js b/dom/media/tests/mochitest/identity/idp-redirect-https.js
new file mode 100644
index 000000000..5d35ac0ca
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp-redirect-https.js
@@ -0,0 +1,3 @@
+(function() {
+ dump('ERROR\n');
+}());
diff --git a/dom/media/tests/mochitest/identity/idp-redirect-https.js^headers^ b/dom/media/tests/mochitest/identity/idp-redirect-https.js^headers^
new file mode 100644
index 000000000..77d56ac44
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/idp.js b/dom/media/tests/mochitest/identity/idp.js
new file mode 100644
index 000000000..6cc3a1706
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp.js
@@ -0,0 +1,110 @@
+(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: function() {
+ return fetch('https://example.com/.well-known/idp-proxy/idp.sjs?' + this.id)
+ .then(response => response.status === 200);
+ },
+ checkLogin: function(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: function(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: function(usernameHint) {
+ 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: function(payload, origin, usernameHint) {
+ dump('idp: generateAssertion(' + payload + ')\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(usernameHint),
+ contents: payload
+ })
+ });
+ },
+
+ validateAssertion: function(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/tests/mochitest/identity/idp.sjs b/dom/media/tests/mochitest/identity/idp.sjs
new file mode 100644
index 000000000..b6313297b
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/login.html b/dom/media/tests/mochitest/identity/login.html
new file mode 100644
index 000000000..eafba22f2
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/mochitest.ini b/dom/media/tests/mochitest/identity/mochitest.ini
new file mode 100644
index 000000000..85f01083d
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/mochitest.ini
@@ -0,0 +1,42 @@
+[DEFAULT]
+# Android 4.3 - bug 981881
+subsuite = media
+skip-if = android_version == '18' || (os == 'linux' && !debug && !e10s)
+support-files =
+ /.well-known/idp-proxy/idp.js
+ identityPcTest.js
+ !/dom/media/tests/mochitest/blacksilence.js
+ !/dom/media/tests/mochitest/dataChannel.js
+ !/dom/media/tests/mochitest/head.js
+ !/dom/media/tests/mochitest/network.js
+ !/dom/media/tests/mochitest/pc.js
+ !/dom/media/tests/mochitest/sdpUtils.js
+ !/dom/media/tests/mochitest/templates.js
+ !/dom/media/tests/mochitest/turnConfig.js
+tags = msg
+
+[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]
+[test_getIdentityAssertion.html]
+[test_setIdentityProvider.html]
+[test_setIdentityProviderWithErrors.html]
+[test_peerConnection_peerIdentity.html]
+[test_peerConnection_asymmetricIsolation.html]
+[test_loginNeeded.html]
+support-files =
+ /.well-known/idp-proxy/login.html
+ /.well-known/idp-proxy/idp.sjs
diff --git a/dom/media/tests/mochitest/identity/test_fingerprints.html b/dom/media/tests/mochitest/identity/test_fingerprints.html
new file mode 100644
index 000000000..0fd065af2
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_fingerprints.html
@@ -0,0 +1,112 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+ <script class="testbody" type="application/javascript">
+'use strict';
+
+// here we call the identity provider directly
+function getIdentityAssertion(fpArray) {
+ var Cu = SpecialPowers.Cu;
+ var rtcid = Cu.import('resource://gre/modules/media/IdpSandbox.jsm');
+ var sandbox = new rtcid.IdpSandbox('example.com', 'idp.js', window);
+ return sandbox.start()
+ .then(idp => SpecialPowers.wrap(idp)
+ .generateAssertion(JSON.stringify({ fingerprint: fpArray }),
+ 'https://example.com'))
+ .then(assertion => {
+ assertion = SpecialPowers.wrap(assertion);
+ var assertionString = btoa(JSON.stringify(assertion));
+ sandbox.stop();
+ return assertionString;
+ });
+}
+
+// This takes a real fingerprint and makes some extra bad ones.
+function makeFingerprints(algo, digest) {
+ var fingerprints = [];
+ fingerprints.push({ algorithm: algo, digest: digest });
+ for (var i = 0; i < 3; ++i) {
+ fingerprints.push({
+ algorithm: algo,
+ digest: digest.replace(/:./g, ':' + i.toString(16))
+ });
+ }
+ return fingerprints;
+}
+
+var fingerprintRegex = /^a=fingerprint:(\S+) (\S+)/m;
+var 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.
+function testMultipleFingerprints() {
+ // this one fails setRemoteDescription if the identity is not good
+ var pcStrict = new RTCPeerConnection({ peerIdentity: 'someone@example.com'});
+ // this one will be manually tweaked to have two fingerprints
+ var pcDouble = new RTCPeerConnection({});
+
+ var offer, match, fingerprints;
+
+ var fail = msg =>
+ (e => ok(false, 'error in ' + msg + ': ' +
+ (e.message ? (e.message + '\n' + e.stack) : e)));
+
+ navigator.mediaDevices.getUserMedia({ audio: true, fake: true })
+ .then(stream => {
+ ok(stream, 'Got fake stream');
+ pcDouble.addStream(stream);
+ return pcDouble.createOffer();
+ })
+ .then(o => {
+ offer = o;
+ ok(offer, 'Got offer');
+
+ match = offer.sdp.match(fingerprintRegex);
+ if (!match) {
+ throw new Error('No fingerprint in offer SDP');
+ }
+ fingerprints = makeFingerprints(match[1], match[2]);
+ return getIdentityAssertion(fingerprints);
+ })
+ .then(assertion => {
+ ok(assertion, 'Should have assertion');
+
+ var sdp = offer.sdp.slice(0, match.index) +
+ 'a=identity:' + assertion + '\n' +
+ fingerprintSdp(fingerprints.slice(1)) +
+ offer.sdp.slice(match.index);
+
+ var desc = new RTCSessionDescription({ type: 'offer', sdp: sdp });
+ return pcStrict.setRemoteDescription(desc);
+ })
+ .then(() => {
+ ok(true, 'Modified fingerprints were accepted');
+ }, error => {
+ var e = SpecialPowers.wrap(error);
+ ok(false, 'error in test: ' +
+ (e.message ? (e.message + '\n' + e.stack) : e));
+ })
+ .then(() => {
+ pcStrict.close();
+ pcDouble.close();
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({
+ set: [ [ 'media.peerconnection.identity.enabled', true ] ]
+}, testMultipleFingerprints);
+</script>
+ </body>
+</html>
diff --git a/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html b/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
new file mode 100644
index 000000000..662981613
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
@@ -0,0 +1,96 @@
+<!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) {
+ t.pcLocal.setIdentityProvider('example.com', 'idp.js' + instructions,
+ 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', 'idp-bad.js', '');
+ 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/tests/mochitest/identity/test_idpproxy.html b/dom/media/tests/mochitest/identity/test_idpproxy.html
new file mode 100644
index 000000000..c8860d1f4
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_idpproxy.html
@@ -0,0 +1,176 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="application/javascript" 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 : function() {
+ 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', '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/tests/mochitest/identity/test_loginNeeded.html b/dom/media/tests/mochitest/identity/test_loginNeeded.html
new file mode 100644
index 000000000..4c95a7978
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_loginNeeded.html
@@ -0,0 +1,71 @@
+<!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();
+ }, false);
+ });
+}
+
+function checkLogin(t, name, onLoginNeeded) {
+ t.pcLocal.setIdentityProvider('example.com', '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/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html b/dom/media/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html
new file mode 100644
index 000000000..5ea0f5c77
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/test_peerConnection_peerIdentity.html b/dom/media/tests/mochitest/identity/test_peerConnection_peerIdentity.html
new file mode 100644
index 000000000..a8116cc45
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/identity/test_setIdentityProvider.html b/dom/media/tests/mochitest/identity/test_setIdentityProvider.html
new file mode 100644
index 000000000..ee65d5af5
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_setIdentityProvider.html
@@ -0,0 +1,63 @@
+<!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", "idp.js", "someone");
+ test.pcRemote.setIdentityProvider("test2.example.com", "idp.js", "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/tests/mochitest/identity/test_setIdentityProviderWithErrors.html b/dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html
new file mode 100644
index 000000000..ce6276a2b
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html
@@ -0,0 +1,55 @@
+<!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', 'idp.js#bad-validate', '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/tests/mochitest/mediaStreamPlayback.js b/dom/media/tests/mochitest/mediaStreamPlayback.js
new file mode 100644
index 000000000..a72d65f3f
--- /dev/null
+++ b/dom/media/tests/mochitest/mediaStreamPlayback.js
@@ -0,0 +1,252 @@
+/* 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 : function(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 : function () {
+ 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 : function(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 : function(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 : function() {
+ var lastStreamTime = this.mediaStream.currentTime;
+ var lastElementTime = this.mediaElement.currentTime;
+
+ var mediaTimeProgressed = listenUntil(this.mediaElement, 'timeupdate',
+ () => this.mediaStream.currentTime > lastStreamTime &&
+ 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.loop, false, "Loop shall be false");
+ is(this.mediaElement.preload, "", "Preload should not exist");
+ 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 : function() {
+ this.mediaElement.pause();
+ this.mediaElement.srcObject = null;
+ }
+}
+
+
+/**
+ * This class is basically the same as MediaStreamPlayback except
+ * ensures that the instance provided startMedia is a MediaStream.
+ *
+ * @param {HTMLMediaElement} mediaElement the media element for playback
+ * @param {LocalMediaStream} mediaStream the media stream used in
+ * the mediaElement for playback
+ */
+function LocalMediaStreamPlayback(mediaElement, mediaStream) {
+ ok(mediaStream instanceof LocalMediaStream,
+ "Stream should be a LocalMediaStream");
+ MediaStreamPlayback.call(this, mediaElement, mediaStream);
+}
+
+LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, {
+
+ /**
+ * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
+ *
+ * Starts media with a media stream, runs it until a canplaythrough and
+ * timeupdate event fires, and calls stop() on the stream.
+ *
+ * @param {Boolean} isResume specifies if this media element is being resumed
+ * from a previous run
+ */
+ playMediaWithDeprecatedStreamStop : {
+ value: function(isResume) {
+ this.startMedia(isResume);
+ return this.verifyPlaying()
+ .then(() => this.deprecatedStopStreamInMediaPlayback())
+ .then(() => this.detachFromMediaElement());
+ }
+ },
+
+ /**
+ * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
+ *
+ * Stops the local media stream while it's currently in playback in
+ * a media element.
+ *
+ * Precondition: The media stream and element should both be actively
+ * being played.
+ *
+ */
+ deprecatedStopStreamInMediaPlayback : {
+ value: function () {
+ return new Promise((resolve, reject) => {
+ /**
+ * Callback fired when the ended event fires when stop() is called on the
+ * stream.
+ */
+ var endedCallback = () => {
+ this.mediaElement.removeEventListener('ended', endedCallback, false);
+ ok(true, "ended event successfully fired");
+ resolve();
+ };
+
+ this.mediaElement.addEventListener('ended', endedCallback, false);
+ this.mediaStream.stop();
+
+ // If ended doesn't fire in enough time, then we fail the test
+ setTimeout(() => {
+ reject(new Error("ended event never fired"));
+ }, ENDED_TIMEOUT_LENGTH);
+ });
+ }
+ }
+});
+
+// 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));
+}
+
+var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+// 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])
+ .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/tests/mochitest/mochitest.ini b/dom/media/tests/mochitest/mochitest.ini
new file mode 100644
index 000000000..22006ffa2
--- /dev/null
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -0,0 +1,272 @@
+[DEFAULT]
+tags = msg webrtc
+subsuite = media
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ templates.js
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.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
+
+[test_a_noOp.html]
+[test_dataChannel_basicAudio.html]
+skip-if = (android_version == '18') # Bug 962984 for debug, bug 963244 for opt
+[test_dataChannel_basicAudioVideo.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_dataChannel_basicAudioVideoNoBundle.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_dataChannel_basicAudioVideoCombined.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_dataChannel_basicDataOnly.html]
+[test_dataChannel_basicVideo.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_dataChannel_bug1013809.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_dataChannel_noOffer.html]
+[test_enumerateDevices.html]
+[test_enumerateDevices_iframe.html]
+skip-if = true # needed by test_enumerateDevices.html on builders
+[test_ondevicechange.html]
+skip-if = os == 'android'
+[test_getUserMedia_active_autoplay.html]
+[test_getUserMedia_audioCapture.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_getUserMedia_addTrackRemoveTrack.html]
+[test_getUserMedia_addtrack_removetrack_events.html]
+[test_getUserMedia_basicAudio.html]
+[test_getUserMedia_basicVideo.html]
+[test_getUserMedia_basicVideo_playAfterLoadedmetadata.html]
+[test_getUserMedia_basicScreenshare.html]
+skip-if = toolkit == 'android' # no screenshare on android
+[test_getUserMedia_basicTabshare.html]
+skip-if = toolkit == 'android' # no windowshare on android
+[test_getUserMedia_basicWindowshare.html]
+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_getTrackById.html]
+[test_getUserMedia_gumWithinGum.html]
+[test_getUserMedia_loadedmetadata.html]
+[test_getUserMedia_mediaElementCapture_audio.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_getUserMedia_mediaElementCapture_tracks.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_getUserMedia_mediaElementCapture_video.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_getUserMedia_mediaStreamClone.html]
+[test_getUserMedia_mediaStreamConstructors.html]
+[test_getUserMedia_mediaStreamTrackClone.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_stopAudioStream.html]
+[test_getUserMedia_stopAudioStreamWithFollowupAudio.html]
+[test_getUserMedia_stopVideoAudioStream.html]
+[test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html]
+[test_getUserMedia_stopVideoStream.html]
+[test_getUserMedia_stopVideoStreamWithFollowupVideo.html]
+[test_getUserMedia_trackCloneCleanup.html]
+[test_getUserMedia_trackEnded.html]
+[test_getUserMedia_peerIdentity.html]
+[test_peerConnection_addIceCandidate.html]
+[test_peerConnection_addtrack_removetrack_events.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudio.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioNATSrflx.html]
+skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
+[test_peerConnection_basicAudioNATRelay.html]
+skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
+[test_peerConnection_basicAudioNATRelayTCP.html]
+skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
+[test_peerConnection_basicAudioRequireEOC.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioPcmaPcmuOnly.html]
+skip-if = android_version == '18'
+[test_peerConnection_basicAudioDynamicPtMissingRtpmap.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioVideo.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioVideoCombined.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioVideoNoBundle.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicAudioVideoNoRtcpMux.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_basicVideo.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[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 = os == 'android' # bug 1043403
+[test_peerConnection_bug822674.html]
+[test_peerConnection_bug825703.html]
+[test_peerConnection_bug827843.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_bug834153.html]
+[test_peerConnection_bug1013809.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_bug1042791.html]
+skip-if = os == 'android' # bug 1043403
+[test_peerConnection_bug1064223.html]
+[test_peerConnection_capturedVideo.html]
+tags=capturestream
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_captureStream_canvas_2d.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_multiple_captureStream_canvas_2d.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_captureStream_canvas_webgl.html]
+# [test_peerConnection_certificates.html] # bug 1180968
+[test_peerConnection_close.html]
+[test_peerConnection_closeDuringIce.html]
+[test_peerConnection_constructedStream.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_errorCallbacks.html]
+[test_peerConnection_iceFailure.html]
+skip-if = os == 'linux' || os == 'mac' || os == 'win' || android_version == '18' # (Bug 1180388 for win, mac and linux), android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_insertDTMF.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_forwarding_basicAudioVideoCombined.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_noTrickleAnswer.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_noTrickleOffer.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_noTrickleOfferAnswer.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_offerRequiresReceiveAudio.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_offerRequiresReceiveVideo.html]
+[test_peerConnection_offerRequiresReceiveVideoAudio.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_promiseSendOnly.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_renderAfterRenegotiation.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_restartIce.html]
+skip-if = android_version
+[test_peerConnection_restartIceNoBundle.html]
+skip-if = android_version
+[test_peerConnection_restartIceNoBundleNoRtcpMux.html]
+skip-if = android_version
+[test_peerConnection_restartIceNoRtcpMux.html]
+skip-if = android_version
+[test_peerConnection_restartIceLocalRollback.html]
+skip-if = android_version
+[test_peerConnection_restartIceLocalAndRemoteRollback.html]
+skip-if = android_version
+[test_peerConnection_scaleResolution.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_simulcastOffer.html]
+skip-if = android_version # no simulcast support on android
+#[test_peerConnection_relayOnly.html]
+[test_peerConnection_callbacks.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_replaceTrack.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_syncSetDescription.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
+[test_peerConnection_setLocalAnswerInStable.html]
+[test_peerConnection_setLocalOfferInHaveRemoteOffer.html]
+[test_peerConnection_setParameters.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html]
+[test_peerConnection_setRemoteAnswerInStable.html]
+[test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
+[test_peerConnection_throwInCallbacks.html]
+[test_peerConnection_toJSON.html]
+[test_peerConnection_trackDisabling_clones.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_trackDisabling.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoAudioStreams.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoAudioTracksInOneStream.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoAudioVideoStreams.html]
+skip-if = (os == 'linux' && debug && e10s) || android_version == '18' # Bug 1171255 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoAudioVideoStreamsCombined.html]
+skip-if = (os == 'linux' && debug && e10s) || android_version == '18' # Bug 1127828 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoVideoStreams.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_twoVideoTracksInOneStream.html]
+skip-if = os == "android" # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addAudioTrackToExistingVideoStream.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addSecondAudioStream.html]
+skip-if = (android_version == '18') # emulator is too slow to finish a renegotiation test in under 5 minutes
+[test_peerConnection_answererAddSecondAudioStream.html]
+skip-if = (android_version == '18') # emulator is too slow to finish a renegotiation test in under 5 minutes
+[test_peerConnection_removeAudioTrack.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_removeThenAddAudioTrack.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addSecondVideoStream.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_removeVideoTrack.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_removeThenAddVideoTrack.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_replaceVideoThenRenegotiate.html]
+skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addSecondAudioStreamNoBundle.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_removeThenAddAudioTrackNoBundle.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addSecondVideoStreamNoBundle.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_removeThenAddVideoTrackNoBundle.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_addDataChannel.html]
+skip-if = (android_version == '18') # android(bug 1240256, intermittent ICE failures starting w/bug 1232082, possibly from timeout)
+[test_peerConnection_addDataChannelNoBundle.html]
+skip-if = (android_version == '18') # android(bug 1240256, intermittent ICE failures starting w/bug 1232082, possibly from timeout) android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_verifyAudioAfterRenegotiation.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_verifyVideoAfterRenegotiation.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_audioRenegotiationInactiveAnswer.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_videoRenegotiationInactiveAnswer.html]
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_webAudio.html]
+tags = webaudio webrtc
+skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_localRollback.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_localReofferRollback.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_remoteRollback.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_remoteReofferRollback.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_selftest.html]
+# Bug 1227781: Crash with bogus TURN server.
+[test_peerConnection_bug1227781.html]
diff --git a/dom/media/tests/mochitest/network.js b/dom/media/tests/mochitest/network.js
new file mode 100644
index 000000000..286ec5b71
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/nonTrickleIce.js b/dom/media/tests/mochitest/nonTrickleIce.js
new file mode 100644
index 000000000..2733b6494
--- /dev/null
+++ b/dom/media/tests/mochitest/nonTrickleIce.js
@@ -0,0 +1,71 @@
+/* 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: 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/tests/mochitest/pc.js b/dom/media/tests/mochitest/pc.js
new file mode 100644
index 000000000..a9383358f
--- /dev/null
+++ b/dom/media/tests/mochitest/pc.js
@@ -0,0 +1,1878 @@
+/* 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;
+
+ if (iceServersArray.length) {
+ if (!options.turn_disabled_local) {
+ options.config_local = options.config_local || {}
+ options.config_local.iceServers = iceServersArray;
+ }
+ if (!options.turn_disabled_remote) {
+ options.config_remote = options.config_remote || {}
+ options.config_remote.iceServers = iceServersArray;
+ }
+ }
+ else if (typeof turnServers !== "undefined") {
+ if ((!options.turn_disabled_local) && (turnServers.local)) {
+ if (!options.hasOwnProperty("config_local")) {
+ options.config_local = {};
+ }
+ if (!options.config_local.hasOwnProperty("iceServers")) {
+ options.config_local.iceServers = turnServers.local.iceServers;
+ }
+ }
+ if ((!options.turn_disabled_remote) && (turnServers.remote)) {
+ if (!options.hasOwnProperty("config_remote")) {
+ options.config_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;
+ }
+
+ options.steeplechase = !options.is_local || !options.is_remote;
+
+ // 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([
+ new Promise(resolve => {
+ pc.onsignalingstatechange = e => {
+ is(e.target.signalingState, "closed", "signalingState is closed");
+ resolve();
+ };
+ }),
+ 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("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 = function(data, options) {
+ options = options || { };
+ var source = options.sourceChannel ||
+ this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
+ var target = options.targetChannel ||
+ this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
+ var bufferedamount = options.bufferedAmountLowThreshold || 0;
+ var bufferlow_fired = true; // to make testing later easier
+ if (bufferedamount != 0) {
+ source.bufferedAmountLowThreshold = bufferedamount;
+ bufferlow_fired = false;
+ source.onbufferedamountlow = function() {
+ bufferlow_fired = true;
+ };
+ }
+
+ return new Promise(resolve => {
+ // Register event handler for the target channel
+ target.onmessage = e => {
+ ok(bufferlow_fired, "bufferedamountlow event fired");
+ resolve({ channel: target, data: e.data });
+ };
+
+ source.send(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();
+ 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 {RTCSessionDescription} 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(() => {
+ if (this.testOptions.steeplechase) {
+ send_message({"type": "end_of_trickle_ice"});
+ }
+ 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 {RTCSessionDescription} 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.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;
+ },
+
+ // ordered, maxRetransmits and maxRetransmitTime not exposed yet
+
+ /**
+ * Returns the readyState bit of the data channel
+ *
+ * @returns {String} The state of the channel
+ */
+ get readyState() {
+ return this._channel.readyState;
+ },
+
+ /**
+ * 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 : function () {
+ 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: function(data) {
+ info(this + ": Sending data '" + data + "'");
+ this._channel.send(data);
+ },
+
+ /**
+ * Returns the string representation of the class
+ *
+ * @returns {String} The string representation
+ */
+ toString: function() {
+ 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.whenCreated = Date.now();
+
+ 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.expectedLocalTrackInfoById = {};
+ this.expectedRemoteTrackInfoById = {};
+ this.observedRemoteTrackInfoById = {};
+
+ 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();
+ }
+ };
+
+ 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 local description.
+ *
+ * @returns {object} The local description
+ */
+ get localDescription() {
+ return this._pc.localDescription;
+ },
+
+ /**
+ * Sets the local description.
+ *
+ * @param {object} desc
+ * The new local description
+ */
+ set localDescription(desc) {
+ this._pc.localDescription = desc;
+ },
+
+ /**
+ * Returns the remote description.
+ *
+ * @returns {object} The remote description
+ */
+ get remoteDescription() {
+ return this._pc.remoteDescription;
+ },
+
+ /**
+ * Sets the remote description.
+ *
+ * @param {object} desc
+ * The new remote description
+ */
+ set remoteDescription(desc) {
+ this._pc.remoteDescription = desc;
+ },
+
+ /**
+ * 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: function(provider, protocol, identity) {
+ this._pc.setIdentityProvider(provider, protocol, identity);
+ },
+
+ ensureMediaElement : function(track, direction) {
+ const idPrefix = [this.label, direction].join('_');
+ var element = getMediaElementForTrack(track, idPrefix);
+
+ if (!element) {
+ element = createMediaElementForTrack(track, idPrefix);
+ 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();
+ },
+
+ /**
+ * 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 : function(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");
+
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ ok(stream.id, "stream has id");
+ this.expectedLocalTrackInfoById[track.id] = {
+ type: track.kind,
+ streamId: stream.id,
+ };
+
+ // 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 : function(stream) {
+ info("Got local media stream: (" + stream.id + ")");
+
+ this.expectNegotiationNeeded();
+ // In order to test both the addStream and addTrack APIs, we do half one
+ // way, half the other, at random.
+ 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");
+ });
+ }
+
+ stream.getTracks().forEach(track => {
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ this.expectedLocalTrackInfoById[track.id] = {
+ type: track.kind,
+ streamId: stream.id
+ };
+ this.ensureMediaElement(track, "local");
+ });
+ },
+
+ removeSender : function(index) {
+ var sender = this._pc.getSenders()[index];
+ delete this.expectedLocalTrackInfoById[sender.track.id];
+ this.expectNegotiationNeeded();
+ this._pc.removeTrack(sender);
+ return this.observedNegotiationNeeded;
+ },
+
+ senderReplaceTrack : function(index, withTrack, withStreamId) {
+ var sender = this._pc.getSenders()[index];
+ delete this.expectedLocalTrackInfoById[sender.track.id];
+ this.expectedLocalTrackInfoById[withTrack.id] = {
+ type: withTrack.kind,
+ streamId: withStreamId
+ };
+ return sender.replaceTrack(withTrack);
+ },
+
+ /**
+ * Requests all the media streams as specified in the constrains property.
+ *
+ * @param {array} constraintsList
+ * Array of constraints for GUM calls
+ */
+ getAllUserMedia : function(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 => {
+ return getUserMedia(constraints).then(stream => {
+ if (constraints.audio) {
+ stream.getAudioTracks().map(track => {
+ info(this + " gUM local stream " + stream.id +
+ " with audio track " + track.id);
+ });
+ }
+ if (constraints.video) {
+ stream.getVideoTracks().map(track => {
+ info(this + " gUM local stream " + stream.id +
+ " with video track " + track.id);
+ });
+ }
+ return this.attachLocalStream(stream);
+ });
+ }));
+ },
+
+ /**
+ * Create a new data channel instance. Also creates a promise called
+ * `this.nextDataChannel` that resolves when the next data channel arrives.
+ */
+ expectDataChannel: function(message) {
+ this.nextDataChannel = new Promise(resolve => {
+ this.ondatachannel = e => {
+ ok(e.channel, message);
+ 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 : function(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);
+ var wrapper = new DataChannelWrapper(channel, this);
+ this.dataChannels.push(wrapper);
+ return wrapper;
+ },
+
+ /**
+ * Creates an offer and automatically handles the failure case.
+ */
+ createOffer : function() {
+ 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 : function() {
+ 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
+ * RTCSessionDescription for the local description request
+ */
+ setLocalDescription : function(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
+ * RTCSessionDescription for the local description request
+ * @returns {Promise}
+ * A promise that resolves to the expected error
+ */
+ setLocalDescriptionAndFail : function(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
+ * RTCSessionDescription for the remote description request
+ */
+ setRemoteDescription : function(desc) {
+ this.observedNegotiationNeeded = undefined;
+ return this._pc.setRemoteDescription(desc).then(() => {
+ info(this + ": Successfully set remote description");
+ if (desc.type == "rollback") {
+ this.holdIceCandidates = new Promise(r => this.releaseIceCandidates = r);
+
+ } else {
+ 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
+ * RTCSessionDescription for the remote description request
+ * @returns {Promise}
+ * a promise that resolve to the returned error
+ */
+ setRemoteDescriptionAndFail : function(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: function() {
+ 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).indexOf(oldstate) >= 0) {
+ ok(signalingStateTransitions[oldstate].indexOf(newstate) >= 0, 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);
+ });
+ },
+
+ /**
+ * Checks whether a given track is expected, has not been observed yet, and
+ * is of the correct type. Then, moves the track from
+ * |expectedTrackInfoById| to |observedTrackInfoById|.
+ */
+ checkTrackIsExpected : function(track,
+ expectedTrackInfoById,
+ observedTrackInfoById) {
+ ok(expectedTrackInfoById[track.id], "track id " + track.id + " was expected");
+ ok(!observedTrackInfoById[track.id], "track id " + track.id + " was not yet observed");
+ var observedKind = track.kind;
+ var expectedKind = expectedTrackInfoById[track.id].type;
+ is(observedKind, expectedKind,
+ "track id " + track.id + " was of kind " +
+ observedKind + ", which matches " + expectedKind);
+ observedTrackInfoById[track.id] = expectedTrackInfoById[track.id];
+ },
+
+ isTrackOnPC: function(track) {
+ return this._pc.getRemoteStreams().some(s => !!s.getTrackById(track.id));
+ },
+
+ allExpectedTracksAreObserved: function(expected, observed) {
+ return Object.keys(expected).every(trackId => observed[trackId]);
+ },
+
+ setupTrackEventHandler: function() {
+ this._pc.addEventListener('track', event => {
+ info(this + ": 'ontrack' event fired for " + JSON.stringify(event.track));
+
+ this.checkTrackIsExpected(event.track,
+ this.expectedRemoteTrackInfoById,
+ this.observedRemoteTrackInfoById);
+ ok(this.isTrackOnPC(event.track), "Found track " + event.track.id);
+
+ this.ensureMediaElement(event.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 : function(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: function() {
+ 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).indexOf(oldstate) != -1) {
+ 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].indexOf(newstate) != -1, 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 : function() {
+ 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 : function() {
+ return this.iceConnected;
+ },
+
+ /**
+ * Setup a onicecandidate handler
+ *
+ * @param {object} test
+ * A PeerConnectionTest object to which the ice candidates gets
+ * forwarded.
+ */
+ setupIceCandidateHandler : function(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");
+ /* Bug 1193731. Accroding to WebRTC spec 4.3.1 the ICE Agent first sets
+ * the gathering state to completed (step 3.) before sending out the
+ * null newCandidate in step 4. */
+ todo(this._pc.iceGatheringState === 'completed',
+ "ICE gathering state has reached completed");
+ resolveEndOfTrickle(this.label);
+ return;
+ }
+
+ info(this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate));
+ ok(anEvent.candidate.candidate.length > 0, "ICE candidate contains candidate");
+ ok(anEvent.candidate.sdpMid.length > 0, "SDP mid not empty");
+
+ // only check the m-section for the updated default addr that corresponds
+ // with this candidate.
+ var mSections = this.localDescription.sdp.split("\r\nm=");
+ sdputils.checkSdpCLineNotDefault(
+ mSections[anEvent.candidate.sdpMLineIndex+1], this.label
+ );
+
+ 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 : function() {
+ var observed = {};
+ info(this + " Checking local tracks " + JSON.stringify(this.expectedLocalTrackInfoById));
+ this._pc.getSenders().forEach(sender => {
+ this.checkTrackIsExpected(sender.track, this.expectedLocalTrackInfoById, observed);
+ });
+
+ Object.keys(this.expectedLocalTrackInfoById).forEach(
+ id => ok(observed[id], this + " local id " + id + " was observed"));
+ },
+
+ /**
+ * Checks that we are getting the media tracks we expect.
+ */
+ checkMediaTracks : function() {
+ this.checkLocalMediaTracks();
+
+ info(this + " Checking remote tracks " +
+ JSON.stringify(this.expectedRemoteTrackInfoById));
+
+ ok(this.allExpectedTracksAreObserved(this.expectedRemoteTrackInfoById,
+ this.observedRemoteTrackInfoById),
+ "All expected tracks have been observed"
+ + "\nexpected: " + JSON.stringify(this.expectedRemoteTrackInfoById)
+ + "\nobserved: " + JSON.stringify(this.observedRemoteTrackInfoById));
+ },
+
+ checkMsids: function() {
+ var checkSdpForMsids = (desc, expectedTrackInfo, side) => {
+ Object.keys(expectedTrackInfo).forEach(trackId => {
+ var streamId = expectedTrackInfo[trackId].streamId;
+ ok(desc.sdp.match(new RegExp("a=msid:" + streamId + " " + trackId)),
+ this + ": " + side + " SDP contains stream " + streamId +
+ " and track " + trackId );
+ });
+ };
+
+ checkSdpForMsids(this.localDescription, this.expectedLocalTrackInfoById,
+ "local");
+ checkSdpForMsids(this.remoteDescription, this.expectedRemoteTrackInfoById,
+ "remote");
+ },
+
+ markRemoteTracksAsNegotiated: function() {
+ Object.values(this.observedRemoteTrackInfoById).forEach(
+ trackInfo => trackInfo.negotiated = true);
+ },
+
+ rollbackRemoteTracksIfNotNegotiated: function() {
+ Object.keys(this.observedRemoteTrackInfoById).forEach(
+ id => {
+ if (!this.observedRemoteTrackInfoById[id].negotiated) {
+ delete this.observedRemoteTrackInfoById[id];
+ }
+ });
+ },
+
+ /**
+ * 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 : function(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}
+ * A promise that resolves when media is flowing.
+ */
+ waitForRtpFlow(track) {
+ var hasFlow = stats => {
+ var rtp = stats.get([...stats.keys()].find(key =>
+ !stats.get(key).isRemote && stats.get(key).type.endsWith("boundrtp")));
+ ok(rtp, "Should have RTP stats for track " + track.id);
+ if (!rtp) {
+ return false;
+ }
+ var nrPackets = rtp[rtp.type == "outboundrtp" ? "packetsSent"
+ : "packetsReceived"];
+ info("Track " + track.id + " has " + nrPackets + " " +
+ rtp.type + " RTP packets.");
+ return nrPackets > 0;
+ };
+
+ info("Checking RTP packet flow for track " + track.id);
+
+ var retry = (delay) => this._pc.getStats(track)
+ .then(stats => hasFlow(stats)? ok(true, "RTP flowing for track " + track.id) :
+ wait(delay).then(retry(1000)));
+ return retry(200);
+ },
+
+ /**
+ * 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 : function() {
+ return Promise.all([].concat(
+ this.localMediaElements.map(element => this.waitForMediaElementFlow(element)),
+ Object.keys(this.expectedRemoteTrackInfoById)
+ .map(id => this.remoteMediaElements
+ .find(e => e.srcObject.getTracks().some(t => t.id == id)))
+ .map(e => this.waitForMediaElementFlow(e)),
+ this._pc.getSenders().map(sender => this.waitForRtpFlow(sender.track)),
+ this._pc.getReceivers().map(receiver => this.waitForRtpFlow(receiver.track))));
+ },
+
+ /**
+ * Check that correct audio (typically a flat tone) is flowing to this
+ * PeerConnection. 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 from |from|.
+ */
+ checkReceivingToneFrom : function(audiocontext, from) {
+ var inputElem = from.localMediaElements[0];
+
+ // As input we use the stream of |from|'s first available audio sender.
+ var inputSenderTracks = from._pc.getSenders().map(sn => sn.track);
+ var inputAudioStream = from._pc.getLocalStreams()
+ .find(s => inputSenderTracks.some(t => t.kind == "audio" && s.getTrackById(t.id)));
+ var inputAnalyser = new AudioStreamAnalyser(audiocontext, inputAudioStream);
+
+ // It would have been nice to have a working getReceivers() here, but until
+ // we do, let's use what remote streams we have.
+ var outputAudioStream = this._pc.getRemoteStreams()
+ .find(s => s.getAudioTracks().length > 0);
+ var outputAnalyser = new AudioStreamAnalyser(audiocontext, outputAudioStream);
+
+ var maxWithIndex = (a, b, i) => (b >= a.value) ? { value: b, index: i } : a;
+ var initial = { value: -1, index: -1 };
+
+ return new Promise((resolve, reject) => inputElem.ontimeupdate = () => {
+ var inputData = inputAnalyser.getByteFrequencyData();
+ var outputData = outputAnalyser.getByteFrequencyData();
+
+ var inputMax = inputData.reduce(maxWithIndex, initial);
+ var outputMax = outputData.reduce(maxWithIndex, initial);
+ info("Comparing maxima; input[" + inputMax.index + "] = " + inputMax.value +
+ ", output[" + outputMax.index + "] = " + outputMax.value);
+ if (!inputMax.value || !outputMax.value) {
+ return;
+ }
+
+ // When the input and output maxima are within reasonable distance
+ // from each other, we can be sure that the input tone has made it
+ // through the peer connection.
+ if (Math.abs(inputMax.index - outputMax.index) < 10) {
+ ok(true, "input and output audio data matches");
+ inputElem.ontimeupdate = null;
+ resolve();
+ }
+ });
+ },
+
+ /**
+ * Check that stats are present by checking for known stats.
+ */
+ getStats : function(selector) {
+ return this._pc.getStats(selector).then(stats => {
+ info(this + ": Got stats: " + JSON.stringify(stats));
+ 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 : function(stats, twoMachines) {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+
+ // Use spec way of enumerating stats
+ var counters = {};
+ for (let [key, res] of stats) {
+ // validate stats
+ ok(res.id == key, "Coherent stats id");
+ var nowish = Date.now() + 1000; // TODO: clock drift observed
+ var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649)
+ if (isWinXP) {
+ todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)");
+ } else if (!twoMachines) {
+ // Bug 1225729: On android, sometimes the first RTCP of the first
+ // test run gets this value, likely because no RTP has been sent yet.
+ if (res.timestamp != 2085978496000) {
+ ok(res.timestamp >= minimum,
+ "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
+ res.timestamp + " >= " + minimum + " (" +
+ (res.timestamp - minimum) + " ms)");
+ ok(res.timestamp <= nowish,
+ "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
+ res.timestamp + " <= " + nowish + " (" +
+ (res.timestamp - nowish) + " ms)");
+ } else {
+ info("Bug 1225729: Uninitialized timestamp (" + res.timestamp +
+ "), should be >=" + minimum + " and <= " + nowish);
+ }
+ }
+ if (res.isRemote) {
+ continue;
+ }
+ counters[res.type] = (counters[res.type] || 0) + 1;
+
+ switch (res.type) {
+ case "inboundrtp":
+ case "outboundrtp": {
+ // ssrc is a 32 bit number returned as a string by spec
+ ok(res.ssrc.length > 0, "Ssrc has length");
+ ok(res.ssrc.length < 11, "Ssrc not lengthy");
+ ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric");
+ ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits");
+
+ if (res.type == "outboundrtp") {
+ 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[res.remoteId];
+ ok(rem.isRemote, "Remote is rtcp");
+ ok(rem.remoteId == res.id, "Remote backlink match");
+ if(res.type == "outboundrtp") {
+ ok(rem.type == "inboundrtp", "Rtcp is inbound");
+ ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived");
+ ok(rem.packetsLost !== undefined, "Rtcp packetsLost");
+ ok(rem.bytesReceived >= rem.packetsReceived, "Rtcp bytesReceived");
+ if (!this.disableRtpCountChecking) {
+ ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
+ ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
+ }
+ ok(rem.jitter !== undefined, "Rtcp jitter");
+ ok(rem.mozRtt !== undefined, "Rtcp rtt");
+ ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0");
+ ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min");
+ } else {
+ ok(rem.type == "outboundrtp", "Rtcp is outbound");
+ ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
+ // We may have received more than outdated Rtcp packetsSent
+ ok(rem.bytesSent >= rem.packetsSent, "Rtcp bytesSent");
+ }
+ ok(rem.ssrc == res.ssrc, "Remote ssrc match");
+ } else {
+ info("No rtcp info received yet");
+ }
+ }
+ break;
+ }
+ }
+
+ // Use legacy way of enumerating stats
+ var counters2 = {};
+ for (let key in stats) {
+ if (!stats.hasOwnProperty(key)) {
+ continue;
+ }
+ var res = stats[key];
+ if (!res.isRemote) {
+ counters2[res.type] = (counters2[res.type] || 0) + 1;
+ }
+ }
+ is(JSON.stringify(counters), JSON.stringify(counters2),
+ "Spec and legacy variant of RTCStatsReport enumeration agree");
+ var nin = Object.keys(this.expectedRemoteTrackInfoById).length;
+ var nout = Object.keys(this.expectedLocalTrackInfoById).length;
+ var ndata = this.dataChannels.length;
+
+ // TODO(Bug 957145): Restore stronger inboundrtp test once Bug 948249 is fixed
+ //is((counters["inboundrtp"] || 0), nin, "Have " + nin + " inboundrtp stat(s)");
+ ok((counters.inboundrtp || 0) >= nin, "Have at least " + nin + " inboundrtp stat(s) *");
+
+ is(counters.outboundrtp || 0, nout, "Have " + nout + " outboundrtp stat(s)");
+
+ var numLocalCandidates = counters.localcandidate || 0;
+ var numRemoteCandidates = counters.remotecandidate || 0;
+ // If there are no tracks, there will be no stats either.
+ if (nin + nout + ndata > 0) {
+ ok(numLocalCandidates, "Have localcandidate stat(s)");
+ ok(numRemoteCandidates, "Have remotecandidate stat(s)");
+ } else {
+ is(numLocalCandidates, 0, "Have no localcandidate stats");
+ is(numRemoteCandidates, 0, "Have no remotecandidate 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 : function(stats, expectedLocalCandidateType) {
+ let lId;
+ let rId;
+ for (let stat of stats.values()) {
+ if (stat.type == "candidatepair" && 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.mozLocalTransport === "tcp") && (candidateType === "relayed")) {
+ candidateType = "relayed-tcp";
+ }
+
+ if ((expectedLocalCandidateType === "serverreflexive") &&
+ (candidateType === "peerreflexive")) {
+ // Be forgiving of prflx when expecting srflx, since that can happen due
+ // to timing.
+ candidateType = "serverreflexive";
+ }
+
+ 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} counters
+ * The counters for media and data tracks based on constraints
+ * @param {object} testOptions
+ * The test options object from the PeerConnectionTest
+ */
+ checkStatsIceConnections : function(stats,
+ offerConstraintsList, offerOptions, testOptions) {
+ var numIceConnections = 0;
+ Object.keys(stats).forEach(key => {
+ if ((stats[key].type === "candidatepair") && stats[key].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 {
+ // This code assumes that no media sections have been rejected due to
+ // codec mismatch or other unrecoverable negotiation failures.
+ var numAudioTracks =
+ sdputils.countTracksInConstraint('audio', offerConstraintsList) ||
+ ((offerOptions && offerOptions.offerToReceiveAudio) ? 1 : 0);
+
+ var numVideoTracks =
+ sdputils.countTracksInConstraint('video', offerConstraintsList) ||
+ ((offerOptions && offerOptions.offerToReceiveVideo) ? 1 : 0);
+
+ var numExpectedTransports = numAudioTracks + numVideoTracks;
+ 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 : function() {
+ 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 : function(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 : function() {
+ 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 : function() {
+ return "PeerConnectionWrapper (" + this.label + ")";
+ }
+};
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+var scriptsReady = Promise.all([
+ "/tests/SimpleTest/SimpleTest.js",
+ "head.js",
+ "templates.js",
+ "turnConfig.js",
+ "dataChannel.js",
+ "network.js",
+ "sdpUtils.js"
+].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; });
+}));
+
+function createHTML(options) {
+ return scriptsReady.then(() => realCreateHTML(options));
+}
+
+var iceServerWebsocket;
+var iceServersArray = [];
+
+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 => new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ {'set': [['media.peerconnection.disable_http_proxy', !enable]]},
+ resolve);
+ });
+
+ 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); });
+};
+
+function runNetworkTest(testFunction, fixtureOptions) {
+ fixtureOptions = fixtureOptions || {}
+ return scriptsReady.then(() =>
+ runTestWhenReady(options =>
+ startNetworkAndTest()
+ .then(() => setupIceServerConfig(fixtureOptions.useIceServer))
+ .then(() => testFunction(options))
+ )
+ );
+}
diff --git a/dom/media/tests/mochitest/sdpUtils.js b/dom/media/tests/mochitest/sdpUtils.js
new file mode 100644
index 000000000..ba8dad562
--- /dev/null
+++ b/dom/media/tests/mochitest/sdpUtils.js
@@ -0,0 +1,148 @@
+/* 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 = {
+
+checkSdpAfterEndOfTrickle: function(sdp, testOptions, label) {
+ info("EOC-SDP: " + JSON.stringify(sdp));
+
+ ok(sdp.sdp.includes("a=end-of-candidates"), label + ": SDP contains end-of-candidates");
+ sdputils.checkSdpCLineNotDefault(sdp.sdp, label);
+
+ if (testOptions.rtcpmux) {
+ ok(sdp.sdp.includes("a=rtcp-mux"), label + ": SDP contains rtcp-mux");
+ } else {
+ ok(sdp.sdp.includes("a=rtcp:"), label + ": SDP contains rtcp port");
+ }
+},
+
+// takes sdp in string form (or possibly a fragment, say an m-section), and
+// verifies that the default 0.0.0.0 addr is not present.
+checkSdpCLineNotDefault: function(sdpStr, label) {
+ info("CLINE-NO-DEFAULT-ADDR-SDP: " + JSON.stringify(sdpStr));
+ ok(!sdpStr.includes("c=IN IP4 0.0.0.0"), label + ": SDP contains non-zero IP c line");
+},
+
+// Note, we don't bother removing the fmtp lines, which makes a good test
+// for some SDP parsing issues. It would be good to have a "removeAllButCodec(sdp, codec)" too.
+removeCodec: function(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;
+},
+
+removeRtcpMux: function(sdp) {
+ return sdp.replace(/a=rtcp-mux\r\n/g,"");
+},
+
+removeBundle: function(sdp) {
+ return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
+},
+
+reduceAudioMLineToPcmuPcma: function(sdp) {
+ return sdp.replace(/m=audio .*\r\n/g, "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n");
+},
+
+setAllMsectionsInactive: function(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: function(sdp) {
+ return sdp.replace(/a=rtpmap:.*\r\n/g, "");
+},
+
+reduceAudioMLineToDynamicPtAndOpus: function(sdp) {
+ return sdp.replace(/m=audio .*\r\n/g, "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n");
+},
+
+transferSimulcastProperties: function(offer_sdp, answer_sdp) {
+ if (!offer_sdp.includes("a=simulcast:")) {
+ return answer_sdp;
+ }
+ var o_simul = offer_sdp.match(/simulcast: send rid=(.*)([\n$])*/i);
+ var o_rids = offer_sdp.match(/a=rid:(.*)/ig);
+ var new_answer_sdp = answer_sdp + "a=simulcast: recv rid=" + o_simul[1] + "\r\n";
+ o_rids.forEach((o_rid) => {
+ new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n";
+ });
+ return new_answer_sdp;
+},
+
+verifySdp: function(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"), "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");
+ }
+
+ return requiresTrickleIce;
+},
+
+/**
+ * Counts the amount of audio tracks in a given media constraint.
+ *
+ * @param constraints
+ * The contraint to be examined.
+ */
+countTracksInConstraint: function(type, constraints) {
+ if (!Array.isArray(constraints)) {
+ return 0;
+ }
+ return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
+},
+
+};
diff --git a/dom/media/tests/mochitest/steeplechase.ini b/dom/media/tests/mochitest/steeplechase.ini
new file mode 100644
index 000000000..686049a43
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ head.js
+ mediaStreamPlayback.js
+ network.js
+ pc.js
+ sdpUtils.js
+ templates.js
+ turnConfig.js
+
+[test_peerConnection_basicAudio.html]
diff --git a/dom/media/tests/mochitest/steeplechase_long/long.js b/dom/media/tests/mochitest/steeplechase_long/long.js
new file mode 100644
index 000000000..8e8030946
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase_long/long.js
@@ -0,0 +1,217 @@
+/* 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 true if res is a local rtp
+ *
+ * @param {Object} statObject
+ * One of the objects comprising the report received from getStats()
+ * @returns {boolean}
+ * True if object is a local rtp
+ */
+function isLocalRtp(statObject) {
+ return (typeof statObject === 'object' &&
+ statObject.isRemote === false);
+}
+
+
+/**
+ * Dumps the local, dynamic parts of the stats object as a formatted block
+ * Used for capturing and monitoring test status during execution
+ *
+ * @param {Object} stats
+ * Stats object to use for output
+ * @param {string} label
+ * Used in the header of the output
+ */
+function outputPcStats(stats, label) {
+ var outputStr = '\n\n';
+ function appendOutput(line) {
+ outputStr += line.toString() + '\n';
+ }
+
+ var firstRtp = true;
+ for (var prop in stats) {
+ if (isLocalRtp(stats[prop])) {
+ var rtp = stats[prop];
+ if (firstRtp) {
+ appendOutput(label.toUpperCase() + ' STATS ' +
+ '(' + new Date(rtp.timestamp).toISOString() + '):');
+ firstRtp = false;
+ }
+ appendOutput(' ' + rtp.id + ':');
+ if (rtp.type === 'inboundrtp') {
+ appendOutput(' bytesReceived: ' + rtp.bytesReceived);
+ appendOutput(' jitter: ' + rtp.jitter);
+ appendOutput(' packetsLost: ' + rtp.packetsLost);
+ appendOutput(' packetsReceived: ' + rtp.packetsReceived);
+ } else {
+ appendOutput(' bytesSent: ' + rtp.bytesSent);
+ appendOutput(' packetsSent: ' + rtp.packetsSent);
+ }
+ }
+ }
+ outputStr += '\n\n';
+ dump(outputStr);
+}
+
+
+var _lastStats = {};
+
+const MAX_ERROR_CYCLES = 5;
+var _errorCount = {};
+
+/**
+ * Verifies the peer connection stats interval over interval
+ *
+ * @param {Object} stats
+ * Stats object to use for verification
+ * @param {string} label
+ * Identifies the peer connection. Differentiates stats for
+ * interval-over-interval verification in cases where more than one set
+ * is being verified
+ */
+function verifyPcStats(stats, label) {
+ const INCREASING_INBOUND_STAT_NAMES = [
+ 'bytesReceived',
+ 'packetsReceived'
+ ];
+
+ const INCREASING_OUTBOUND_STAT_NAMES = [
+ 'bytesSent',
+ 'packetsSent'
+ ];
+
+ if (_lastStats[label] !== undefined) {
+ var errorsInCycle = false;
+
+ function verifyIncrease(rtpName, statNames) {
+ var timestamp = new Date(stats[rtpName].timestamp).toISOString();
+
+ statNames.forEach(function (statName) {
+ var passed = stats[rtpName][statName] >
+ _lastStats[label][rtpName][statName];
+ if (!passed) {
+ errorsInCycle = true;
+ }
+ ok(passed,
+ timestamp + '.' + label + '.' + rtpName + '.' + statName,
+ label + '.' + rtpName + '.' + statName + ' increased (value=' +
+ stats[rtpName][statName] + ')');
+ });
+ }
+
+ for (var prop in stats) {
+ if (isLocalRtp(stats[prop])) {
+ if (stats[prop].type === 'inboundrtp') {
+ verifyIncrease(prop, INCREASING_INBOUND_STAT_NAMES);
+ } else {
+ verifyIncrease(prop, INCREASING_OUTBOUND_STAT_NAMES);
+ }
+ }
+ }
+
+ if (errorsInCycle) {
+ _errorCount[label] += 1;
+ info(label +": increased error counter to " + _errorCount[label]);
+ } else {
+ // looks like we recovered from a temp glitch
+ if (_errorCount[label] > 0) {
+ info(label + ": reseting error counter to zero");
+ }
+ _errorCount[label] = 0;
+ }
+ } else {
+ _errorCount[label] = 0;
+ }
+
+ _lastStats[label] = stats;
+}
+
+
+/**
+ * Retrieves and performs a series of operations on PeerConnection stats
+ *
+ * @param {PeerConnectionWrapper} pc
+ * PeerConnectionWrapper from which to get stats
+ * @param {string} label
+ * Label for the peer connection, passed to each stats callback
+ * @param {Array} operations
+ * Array of stats callbacks, each as function (stats, label)
+ */
+function processPcStats(pc, label, operations) {
+ pc.getStats(null, function (stats) {
+ operations.forEach(function (operation) {
+ operation(stats, label);
+ });
+ });
+}
+
+
+/**
+ * Outputs and verifies the status for local and/or remote PeerConnection as
+ * appropriate
+ *
+ * @param {Object} test
+ * Test containing the peer connection(s) for verification
+ */
+function verifyConnectionStatus(test) {
+ const OPERATIONS = [outputPcStats, verifyPcStats];
+
+ if (test.pcLocal) {
+ processPcStats(test.pcLocal, 'LOCAL', OPERATIONS);
+ }
+
+ if (test.pcRemote) {
+ processPcStats(test.pcRemote, 'REMOTE', OPERATIONS);
+ }
+}
+
+
+/**
+ * Generates a setInterval wrapper command link for use in pc.js command chains
+ *
+ * This function returns a promise that will resolve once the link repeatedly
+ * calls the given callback function every interval ms
+ * until duration ms have passed, then it will continue the test.
+ *
+ * @param {function} callback
+ * Function to be called on each interval
+ * @param {number} [interval=1000]
+ * Frequency in milliseconds with which callback will be called
+ * @param {number} [duration=3 hours]
+ * Length of time in milliseconds for which callback will be called
+
+
+ */
+function generateIntervalCommand(callback, interval, duration) {
+ interval = interval || 1000;
+ duration = duration || 1000 * 3600 * 3;
+
+ return function INTERVAL_COMMAND(test) {
+ return new Promise (resolve=>{
+ var startTime = Date.now();
+ var intervalId = setInterval(function () {
+ if (callback) {
+ callback(test);
+ }
+
+ var failed = false;
+ Object.keys(_errorCount).forEach(function (label) {
+ if (_errorCount[label] > MAX_ERROR_CYCLES) {
+ ok(false, "Encountered more then " + MAX_ERROR_CYCLES + " cycles" +
+ " with errors on " + label);
+ failed = true;
+ }
+ });
+ var timeElapsed = Date.now() - startTime;
+ if ((timeElapsed >= duration) || failed) {
+ clearInterval(intervalId);
+ resolve();
+ }
+ }, interval);
+ });
+ };
+}
diff --git a/dom/media/tests/mochitest/steeplechase_long/steeplechase_long.ini b/dom/media/tests/mochitest/steeplechase_long/steeplechase_long.ini
new file mode 100644
index 000000000..5832c4a24
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase_long/steeplechase_long.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+ long.js
+ ../head.js
+ ../mediaStreamPlayback.js
+ ../pc.js
+ ../templates.js
+ ../turnConfig.js
+ ../network.js
+
+[test_peerConnection_basicAudioVideoCombined_long.html]
+
diff --git a/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudioVideoCombined_long.html b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudioVideoCombined_long.html
new file mode 100644
index 000000000..d2f03cd60
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudioVideoCombined_long.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+
+<!-- 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/. -->
+
+<html>
+<head>
+ <script type="application/javascript" src="long.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1014328",
+ title: "Basic audio/video (combined) peer connection, long running",
+ visible: true
+ });
+
+ var STEEPLECHASE_TIMEOUT = 1000 * 3600 * 3;
+ var test;
+ runNetworkTest(function (options) {
+ options = options || {};
+ options.commands = makeDefaultCommands();
+ options.commands.push(generateIntervalCommand(verifyConnectionStatus,
+ 1000 * 10,
+ STEEPLECHASE_TIMEOUT));
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true, fake: false}],
+ [{audio: true, video: true, fake: false}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudio_long.html b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudio_long.html
new file mode 100644
index 000000000..ef92c0c95
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicAudio_long.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+
+<!-- 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/. -->
+
+<html>
+<head>
+ <script type="application/javascript" src="long.js"></script>
+ <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",
+ visible: true
+ });
+
+ var STEEPLECHASE_TIMEOUT = 1000 * 3600 * 3;
+ var test;
+ runNetworkTest(function (options) {
+ options = options || {};
+ options.commands = makeDefaultCommands();
+ options.commands.push(generateIntervalCommand(verifyConnectionStatus,
+ 1000 * 10,
+ STEEPLECHASE_TIMEOUT));
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, fake: false}],
+ [{audio: true, fake: false}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicVideo_long.html b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicVideo_long.html
new file mode 100644
index 000000000..64e4ef8f6
--- /dev/null
+++ b/dom/media/tests/mochitest/steeplechase_long/test_peerConnection_basicVideo_long.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+
+<!-- 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/. -->
+
+<html>
+<head>
+ <script type="application/javascript" src="long.js"></script>
+ <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",
+ visible: true
+ });
+
+ var STEEPLECHASE_TIMEOUT = 1000 * 3600 * 3;
+ var test;
+ runNetworkTest(function (options) {
+ options = options || {};
+ options.commands = makeDefaultCommands();
+ options.commands.push(generateIntervalCommand(verifyConnectionStatus,
+ 1000 * 10,
+ STEEPLECHASE_TIMEOUT));
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true, fake: false}],
+ [{video: true, fake: false}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/templates.js b/dom/media/tests/mochitest/templates.js
new file mode 100644
index 000000000..3a8e0dd49
--- /dev/null
+++ b/dom/media/tests/mochitest/templates.js
@@ -0,0 +1,525 @@
+/**
+ * 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'");
+ });
+}
+
+function checkTrackStats(pc, rtpSenderOrReceiver, outbound) {
+ var track = rtpSenderOrReceiver.track;
+ var audio = (track.kind == "audio");
+ var msg = pc + " stats " + (outbound ? "outbound " : "inbound ") +
+ (audio ? "audio" : "video") + " rtp track id " + track.id;
+ return pc.getStats(track).then(stats => {
+ ok(pc.hasStat(stats, {
+ type: outbound ? "outboundrtp" : "inboundrtp",
+ isRemote: false,
+ mediaType: audio ? "audio" : "video"
+ }), msg + " - found expected stats");
+ ok(!pc.hasStat(stats, {
+ type: outbound ? "inboundrtp" : "outboundrtp",
+ isRemote: false
+ }), msg + " - did not find extra stats with wrong direction");
+ ok(!pc.hasStat(stats, {
+ mediaType: audio ? "video" : "audio"
+ }), msg + " - did not find extra stats with wrong media type");
+ });
+}
+
+var checkAllTrackStats = pc => {
+ return Promise.all([].concat(
+ pc._pc.getSenders().map(sender => checkTrackStats(pc, sender, true)),
+ pc._pc.getReceivers().map(receiver => checkTrackStats(pc, receiver, false))));
+}
+
+// Commands run once at the beginning of each test, even when performing a
+// renegotiation test.
+var commandsPeerConnectionInitial = [
+ function PC_SETUP_SIGNALING_CLIENT(test) {
+ if (test.testOptions.steeplechase) {
+ test.setupSignalingClient();
+ test.registerSignalingCallback("ice_candidate", function (message) {
+ var pc = test.pcRemote ? test.pcRemote : test.pcLocal;
+ pc.storeOrAddIceCandidate(new RTCIceCandidate(message.ice_candidate));
+ });
+ test.registerSignalingCallback("end_of_trickle_ice", function (message) {
+ test.signalingMessagesFinished();
+ });
+ }
+ },
+
+ 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.getAllUserMedia(test.pcLocal.constraints);
+ },
+
+ function PC_REMOTE_GUM(test) {
+ return test.pcRemote.getAllUserMedia(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_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
+ if (test.testOptions.steeplechase) {
+ send_message({"type": "local_expected_tracks",
+ "expected_tracks": test.pcLocal.expectedLocalTrackInfoById});
+ }
+ },
+
+ function PC_REMOTE_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
+ if (test.testOptions.steeplechase) {
+ send_message({"type": "remote_expected_tracks",
+ "expected_tracks": test.pcRemote.expectedLocalTrackInfoById});
+ }
+ },
+
+ function PC_LOCAL_GET_EXPECTED_REMOTE_TRACKS(test) {
+ if (test.testOptions.steeplechase) {
+ return test.getSignalingMessage("remote_expected_tracks").then(
+ message => {
+ test.pcLocal.expectedRemoteTrackInfoById = message.expected_tracks;
+ });
+ }
+
+ // Deep copy, as similar to steeplechase as possible
+ test.pcLocal.expectedRemoteTrackInfoById =
+ JSON.parse(JSON.stringify(test.pcRemote.expectedLocalTrackInfoById));
+ },
+
+ function PC_REMOTE_GET_EXPECTED_REMOTE_TRACKS(test) {
+ if (test.testOptions.steeplechase) {
+ return test.getSignalingMessage("local_expected_tracks").then(
+ message => {
+ test.pcRemote.expectedRemoteTrackInfoById = message.expected_tracks;
+ });
+ }
+
+ // Deep copy, as similar to steeplechase as possible
+ test.pcRemote.expectedRemoteTrackInfoById =
+ JSON.parse(JSON.stringify(test.pcLocal.expectedLocalTrackInfoById));
+ },
+
+ 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_STEEPLECHASE_SIGNAL_OFFER(test) {
+ if (test.testOptions.steeplechase) {
+ send_message({"type": "offer",
+ "offer": test.originalOffer,
+ "offer_constraints": test.pcLocal.constraints,
+ "offer_options": test.pcLocal.offerOptions});
+ test._local_offer = test.originalOffer;
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ }
+ },
+
+ 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) {
+ if (!test.testOptions.steeplechase) {
+ test._local_offer = test.originalOffer;
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ return Promise.resolve();
+ }
+ return test.getSignalingMessage("offer")
+ .then(message => {
+ ok("offer" in message, "Got an offer message");
+ test._local_offer = new RTCSessionDescription(message.offer);
+ test._offer_constraints = message.offer_constraints;
+ test._offer_options = message.offer_options;
+ });
+ },
+
+ 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");
+ if (test.testOptions.steeplechase) {
+ send_message({"type": "answer",
+ "answer": test.originalAnswer,
+ "answer_constraints": test.pcRemote.constraints});
+ test._remote_answer = test.pcRemote._last_answer;
+ test._answer_constraints = test.pcRemote.constraints;
+ }
+ });
+ },
+
+ 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'");
+ })
+ .then(() => test.pcRemote.markRemoteTracksAsNegotiated());
+ },
+
+ function PC_LOCAL_GET_ANSWER(test) {
+ if (!test.testOptions.steeplechase) {
+ test._remote_answer = test.originalAnswer;
+ test._answer_constraints = test.pcRemote.constraints;
+ return Promise.resolve();
+ }
+
+ return test.getSignalingMessage("answer").then(message => {
+ ok("answer" in message, "Got an answer message");
+ test._remote_answer = new RTCSessionDescription(message.answer);
+ test._answer_constraints = message.answer_constraints;
+ });
+ },
+
+ 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'");
+ })
+ .then(() => test.pcLocal.markRemoteTracksAsNegotiated());
+ },
+
+ 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, test.testOptions.steeplechase);
+ });
+ },
+
+ function PC_REMOTE_CHECK_STATS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStats(stats, test.testOptions.steeplechase);
+ });
+ },
+
+ 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._offer_constraints,
+ test._offer_options,
+ test.testOptions);
+ });
+ },
+
+ function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStatsIceConnections(stats,
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions);
+ });
+ },
+
+ function PC_LOCAL_CHECK_MSID(test) {
+ return test.pcLocal.checkMsids();
+ },
+ function PC_REMOTE_CHECK_MSID(test) {
+ return test.pcRemote.checkMsids();
+ },
+
+ 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));
+};
+
+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/tests/mochitest/test_a_noOp.html b/dom/media/tests/mochitest/test_a_noOp.html
new file mode 100644
index 000000000..aa6cfa90a
--- /dev/null
+++ b/dom/media/tests/mochitest/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 type="text/javascript" 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/tests/mochitest/test_dataChannel_basicAudio.html b/dom/media/tests/mochitest/test_dataChannel_basicAudio.html
new file mode 100644
index 000000000..411c032ed
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_basicAudioVideo.html b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideo.html
new file mode 100644
index 000000000..c012c9e3b
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html
new file mode 100644
index 000000000..14e38e222
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html
new file mode 100644
index 000000000..dc8725f5a
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_basicDataOnly.html b/dom/media/tests/mochitest/test_dataChannel_basicDataOnly.html
new file mode 100644
index 000000000..83aa6edc1
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_basicVideo.html b/dom/media/tests/mochitest/test_dataChannel_basicVideo.html
new file mode 100644
index 000000000..becee6776
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_bug1013809.html b/dom/media/tests/mochitest/test_dataChannel_bug1013809.html
new file mode 100644
index 000000000..b74f9deca
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_dataChannel_noOffer.html b/dom/media/tests/mochitest/test_dataChannel_noOffer.html
new file mode 100644
index 000000000..1b14910c1
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_enumerateDevices.html b/dom/media/tests/mochitest/test_enumerateDevices.html
new file mode 100644
index 000000000..dd4db59d7
--- /dev/null
+++ b/dom/media/tests/mochitest/test_enumerateDevices.html
@@ -0,0 +1,134 @@
+<!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 pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
+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);
+
+ // TODO: s/todo_// once Bug 1213453 is fixed.
+ todo_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/tests/mochitest/test_enumerateDevices_iframe.html";
+ const origins = ["http://mochi.test:8888", "http://test1.mochi.test:8888"];
+ 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;
+ 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/tests/mochitest/test_enumerateDevices_iframe.html b/dom/media/tests/mochitest/test_enumerateDevices_iframe.html
new file mode 100644
index 000000000..6a9054677
--- /dev/null
+++ b/dom/media/tests/mochitest/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), "http://mochi.test:8888");
+
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_active_autoplay.html b/dom/media/tests/mochitest/test_getUserMedia_active_autoplay.html
new file mode 100644
index 000000000..c1a39cdd4
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html b/dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html
new file mode 100644
index 000000000..2159fc917
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(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 LocalMediaStreamPlayback(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 LocalMediaStreamPlayback(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 LocalMediaStreamPlayback(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/tests/mochitest/test_getUserMedia_addtrack_removetrack_events.html b/dom/media/tests/mochitest/test_getUserMedia_addtrack_removetrack_events.html
new file mode 100644
index 000000000..833653ebb
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_audioCapture.html b/dom/media/tests/mochitest/test_getUserMedia_audioCapture.html
new file mode 100644
index 000000000..1b5d6f144
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_audioCapture.html
@@ -0,0 +1,109 @@
+<!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
+});
+
+scriptsReady
+.then(() => FAKE_ENABLED = false)
+.then(() => {
+ runTestWhenReady(function() {
+ // Get an opus file containing a sine wave at maximum amplitude, of duration
+ // `lengthSeconds`, and of frequency `frequency`.
+ function getSineWaveFile(frequency, lengthSeconds, callback) {
+ var chunks = [];
+ var off = new OfflineAudioContext(1, lengthSeconds * 48000, 48000);
+ var osc = off.createOscillator();
+ var rec = new MediaRecorder(osc);
+ rec.ondataavailable = function(e) {
+ chunks.push(e.data);
+ };
+ rec.onstop = function(e) {
+ var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
+ callback(blob);
+ }
+ osc.frequency.value = frequency;
+ osc.start();
+ rec.start();
+ off.startRendering().then(function(buffer) {
+ rec.stop();
+ });
+ }
+ /**
+ * 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.
+ */
+ var wavtone = createMediaElement("audio", "WaveTone");
+ var acTone = createMediaElement("audio", "audioContextTone");
+ var ac = new AudioContext();
+
+ var oscThroughMediaElement = ac.createOscillator();
+ oscThroughMediaElement.frequency.value = 1000;
+ var oscThroughAudioDestinationNode = ac.createOscillator();
+ oscThroughAudioDestinationNode.frequency.value = 5000;
+ var msDest = ac.createMediaStreamDestination();
+
+ oscThroughMediaElement.connect(msDest);
+ oscThroughAudioDestinationNode.connect(ac.destination);
+
+ acTone.mozSrcObject = msDest.stream;
+
+ getSineWaveFile(10000, 10, function(blob) {
+ wavtone.src = URL.createObjectURL(blob);
+ oscThroughMediaElement.start();
+ oscThroughAudioDestinationNode.start();
+ wavtone.loop = true;
+ wavtone.play();
+ acTone.play();
+ });
+
+ var constraints = {audio: {mediaSource: "audioCapture"}};
+
+ return getUserMedia(constraints).then((stream) => {
+ window.grip = stream;
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(function(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);
+ }).then(finish);
+ }).catch(finish);
+ });
+});
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html b/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
new file mode 100644
index 000000000..c69f5d418
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStream on an audio HTMLMediaElement.
+ */
+ runTest(function () {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var constraints = {audio: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new LocalMediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html b/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
new file mode 100644
index 000000000..90a553566
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
@@ -0,0 +1,87 @@
+<!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 Screenshare Test",
+ bug: "1211656"
+ });
+
+ var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a screenshare LocalMediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ return;
+ }
+ var testVideo = createMediaElement('video', 'testVideo');
+
+ var constraints = {
+ video: {
+ mozMediaSource: "screen",
+ mediaSource: "screen"
+ },
+ fake: false
+ };
+ var videoConstraints = [
+ {
+ mediaSource: 'screen',
+ width: {
+ min: '10',
+ max: '100'
+ },
+ height: {
+ min: '10',
+ max: '100'
+ },
+ frameRate: {
+ min: '10',
+ max: '15'
+ }
+ },
+ {
+ mediaSource: 'screen',
+ width: 200,
+ height: 200,
+ frameRate: {
+ min: '5',
+ max: '10'
+ }
+ }
+ ];
+ return Promise.resolve()
+ // Screensharing must work even without "mochi.test," in allowed_domains
+ .then(() => pushPrefs(["media.getusermedia.screensharing.allowed_domains",
+ "mozilla.github.io,*.bugzilla.mozilla.org"]))
+ .then(() => getUserMedia(constraints).then(stream => {
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+ return playback.playMediaWithDeprecatedStreamStop(false);
+ }))
+ .then(() => getUserMedia({video: videoConstraints[0], fake: false}))
+ .then(stream => {
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+ playback.startMedia(false);
+ return playback.verifyPlaying()
+ .then(() => Promise.all([
+ () => testVideo.srcObject.getVideoTracks()[0].applyConstraints(videoConstraints[1]),
+ () => listenUntil(testVideo, "resize", () => true)
+ ]))
+ .then(() => playback.verifyPlaying()) // still playing
+ .then(() => playback.deprecatedStopStreamInMediaPlayback())
+ .then(() => playback.detachFromMediaElement());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html b/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
new file mode 100644
index 000000000..c61b5c9b1
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
@@ -0,0 +1,70 @@
+<!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 LocalMediaStream on a video HTMLMediaElement.
+ *
+ * Additionally, exercise applyConstraints code for tabshare viewport offset.
+ */
+ runTest(function () {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ return;
+ }
+ var testVideo = createMediaElement('video', 'testVideo');
+
+ return Promise.resolve()
+ .then(() => getUserMedia({
+ video: { mediaSource: "browser",
+ scrollWithPage: true },
+ fake: false
+ }))
+ .then(stream => {
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+ return playback.playMediaWithDeprecatedStreamStop(false);
+ })
+ .then(() => getUserMedia({
+ video: {
+ mediaSource: "browser",
+ viewportOffsetX: 0,
+ viewportOffsetY: 0,
+ viewportWidth: 100,
+ viewportHeight: 100
+ },
+ fake: false
+ }))
+ .then(stream => {
+ var playback = new LocalMediaStreamPlayback(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.deprecatedStopStreamInMediaPlayback())
+ .then(() => playback.detachFromMediaElement());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html b/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
new file mode 100644
index 000000000..e0bce3530
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {video: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html b/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
new file mode 100644
index 000000000..d8cc9c533
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStream 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 LocalMediaStreamPlayback(testVideoAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html b/dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
new file mode 100644
index 000000000..27cfe6aff
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(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/tests/mochitest/test_getUserMedia_basicWindowshare.html b/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
new file mode 100644
index 000000000..204e51f78
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.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 Windowshare Test",
+ bug: "1038926"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an screenshare LocalMediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ return;
+ }
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {
+ video: {
+ mozMediaSource: "window",
+ mediaSource: "window"
+ },
+ fake: false
+ };
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+ return playback.playMediaWithDeprecatedStreamStop(false);
+ });
+
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_bug1223696.html b/dom/media/tests/mochitest/test_getUserMedia_bug1223696.html
new file mode 100644
index 000000000..975815be8
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_bug1223696.html
@@ -0,0 +1,52 @@
+<!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(() => Promise.resolve()
+ .then(() => getUserMedia({audio:true, video: true})).then(stream => {
+ info("Test addTrack()ing a video track to an audio-only gUM stream");
+
+ var video = createMediaElement("video", "test_video_track");
+ video.srcObject = stream;
+ video.play();
+
+ var h = new CaptureStreamTestHelper2D();
+ var removedTrack = stream.getVideoTracks()[0];
+ stream.removeTrack(removedTrack);
+ video.onloadeddata = () => {
+ info("loadeddata");
+ var canvas = document.createElement("canvas");
+ canvas.getContext("2d");
+ var canvasStream = canvas.captureStream();
+ setInterval(() => h.drawColor(canvas, h.grey), 1000);
+
+ stream.addTrack(canvasStream.getVideoTracks()[0]);
+
+ checkMediaStreamContains(stream, [stream.getAudioTracks()[0],
+ canvasStream.getVideoTracks()[0]]);
+ };
+
+ return listenUntil(video, "loadeddata", () => true)
+ .then(() => h.waitForPixelColor(video, h.grey, 5,
+ "The canvas track should be rendered by the media element"))
+ .then(() => {
+ [removedTrack, ...stream.getAudioTracks()].forEach(t => t.stop());
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_callbacks.html b/dom/media/tests/mochitest/test_getUserMedia_callbacks.html
new file mode 100644
index 000000000..5c1f6a6a8
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false)
+ .then(() => resolve(), generateErrorCallback());
+ }, generateErrorCallback())
+ );
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_constraints.html b/dom/media/tests/mochitest/test_getUserMedia_constraints.html
new file mode 100644
index 000000000..d6aa88980
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_constraints.html
@@ -0,0 +1,153 @@
+<!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 requires permission",
+ constraints: { video: { mediaSource: 'application' } },
+ error: "NotAllowedError" },
+ { 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: "NotSupportedError" },
+ { message: "Triggering mock failure in default video device fails",
+ constraints: { video: { deviceId: 'bad device' }, fake: true },
+ error: "NotReadableError" },
+ { message: "Triggering mock failure in default audio device fails",
+ constraints: { audio: { deviceId: 'bad device' }, fake: true },
+ 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',
+ // Yet to add:
+ // 'aspectRatio', 'frameRate', 'volume', 'sampleRate', 'sampleSize',
+ // 'latency', 'groupId'
+
+ // 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',
+
+ 'echoCancellation', 'mozNoiseSuppression', 'mozAutoGainControl'
+];
+
+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(() => Promise.resolve()
+ .then(() => {
+ // Check supported constraints first.
+ var dict = navigator.mediaDevices.getSupportedConstraints();
+ var supported = Object.keys(dict);
+
+ mustSupport.forEach(key => ok(supported.indexOf(key) != -1 && dict[key],
+ "Supports " + key));
+
+ var unexpected = supported.filter(key => mustSupport.indexOf(key) == -1);
+ 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" } } }))));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_getTrackById.html b/dom/media/tests/mochitest/test_getUserMedia_getTrackById.html
new file mode 100644
index 000000000..161bf631e
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_gumWithinGum.html b/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
new file mode 100644
index 000000000..f8cecd9d9
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.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 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 LocalMediaStreamPlayback(testVideo,
+ videoStream);
+
+ return videoPlayback.playMedia(false)
+ .then(() => getUserMedia({audio: true}))
+ .then(audioStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var audioPlayback = new LocalMediaStreamPlayback(testAudio,
+ audioStream);
+
+ return audioPlayback.playMedia(false)
+ .then(() => audioStream.stop());
+ })
+ .then(() => videoStream.stop());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_loadedmetadata.html b/dom/media/tests/mochitest/test_getUserMedia_loadedmetadata.html
new file mode 100644
index 000000000..d6efac465
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_mediaElementCapture_audio.html b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_audio.html
new file mode 100644
index 000000000..6d99f2e11
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_audio.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="head.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: true})
+ .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/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html
new file mode 100644
index 000000000..646cc15db
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream track output on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+var audioElement;
+var audioCaptureStream;
+var videoElement;
+var videoCaptureStream;
+var untilEndedElement;
+var streamUntilEnded;
+var tracks = [];
+runTest(() => getUserMedia({audio: true, video: true})
+ .then(stream => {
+ // We need to test with multiple tracks. We add an extra of each kind.
+ stream.getTracks().forEach(t => stream.addTrack(t.clone()));
+
+ audioElement = createMediaElement("audio", "gUMAudio");
+ audioElement.srcObject = stream;
+
+ return haveEvent(audioElement, "loadedmetadata", wait(50000, new Error("Timeout")));
+ })
+ .then(() => {
+ 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");
+
+ return haveNoEvent(audioCaptureStream, "addtrack");
+ })
+ .then(() => {
+ 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");
+
+ return haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 3, wait(50000, new Error("No event")));
+ })
+ .then(() => {
+ 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());
+ return haveEventsButNoMore(
+ audioCaptureStream, "addtrack", 1, wait(50000, new Error("No event")));
+ })
+ .then(() => {
+ 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());
+ return haveNoEvent(audioCaptureStream, "addtrack");
+ })
+ .then(() => {
+ 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());
+ return haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 1, wait(50000, new Error("Timeout")));
+ })
+ .then(() => {
+ 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());
+ return haveNoEvent(videoCaptureStream, "addtrack");
+ })
+ .then(() => {
+ 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());
+ videoElement.srcObject.getVideoTracks().reverse().forEach(t =>
+ videoElement.srcObject.removeTrack(t));
+ is(videoCaptureStream.getVideoTracks()
+ .filter(t => t.readyState == "live").length, 1,
+ "Captured video should have still have one video track captured.");
+
+ return haveEvent(videoCaptureStream.getVideoTracks()[0], "ended",
+ wait(50000, new Error("Timeout")));
+ })
+ .then(() => {
+ is(videoCaptureStream.getVideoTracks()
+ .filter(t => t.readyState == "live").length, 0,
+ "Captured video stream should have no video tracks captured after removal.");
+
+ info("Testing source reset.");
+ })
+ .then(() => getUserMedia({audio: true, video: true}))
+ .then(stream => {
+ videoElement.srcObject = stream;
+ return Promise.all(videoCaptureStream.getTracks()
+ .filter(t => t.readyState == "live")
+ .map(t => haveEvent(t, "ended", wait(50000, new Error("Timeout")))));
+ })
+ .then(() => haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 2, wait(50000, new Error("Timeout"))))
+ .then(() => {
+ is(videoCaptureStream.getAudioTracks()
+ .filter(t => t.readyState == "ended").length, 3,
+ "Captured video stream should have three ended audio tracks");
+ is(videoCaptureStream.getAudioTracks()
+ .filter(t => t.readyState == "live").length, 1,
+ "Captured video stream should have one live audio track");
+
+ is(videoCaptureStream.getVideoTracks()
+ .filter(t => t.readyState == "ended").length, 1,
+ "Captured video stream should have one ended video tracks");
+ is(videoCaptureStream.getVideoTracks()
+ .filter(t => t.readyState == "live").length, 1,
+ "Captured video stream should have one live video track");
+
+ info("Testing CaptureStreamUntilEnded");
+ untilEndedElement =
+ createMediaElement("video", "gUMVideoUntilEnded");
+ untilEndedElement.srcObject = audioElement.srcObject;
+
+ return haveEvent(untilEndedElement, "loadedmetadata",
+ wait(50000, new Error("Timeout")));
+ })
+ .then(() => {
+ 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");
+
+ untilEndedElement.srcObject.getTracks().forEach(t => t.stop());
+ // TODO(1208316) We stop the stream to make the media element end.
+ untilEndedElement.srcObject.stop();
+
+ return Promise.all([
+ haveEvent(untilEndedElement, "ended", wait(50000, new Error("Timeout"))),
+ ...streamUntilEnded.getTracks()
+ .map(t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
+ ]);
+ })
+ .then(() => {
+ info("Element and tracks ended. Ensuring that new tracks aren't created.");
+ untilEndedElement.srcObject = videoElement.srcObject;
+ return haveEventsButNoMore(
+ untilEndedElement, "loadedmetadata", 1, wait(50000, new Error("Timeout")));
+ })
+ .then(() => is(streamUntilEnded.getTracks().length, 4,
+ "Should still have 4 tracks"))
+ .catch(e => ok(false, "Test failed: " + e + (e && e.stack ? "\n" + e.stack : "")))
+ .then(() => [...tracks,
+ ...videoElement.srcObject.getTracks()].forEach(t => t.stop())));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_video.html b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_video.html
new file mode 100644
index 000000000..a1097094e
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_video.html
@@ -0,0 +1,130 @@
+<!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>
+ <script type="application/javascript" src="head.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;
+
+// We check a pixel somewhere away from the top left corner since
+// MediaEngineDefault puts semi-transparent time indicators there.
+const offsetX = 20;
+const offsetY = 20;
+const threshold = 16;
+const pausedTimeout = 1000;
+const h = new CaptureStreamTestHelper2D(50, 50);
+
+var checkHasFrame = video => h.waitForPixel(video, offsetX, offsetY, px => {
+ let result = h.isOpaquePixelNot(px, h.black, threshold);
+ info("Checking that we have a frame, got [" +
+ Array.slice(px) + "]. Pass=" + result);
+ return result;
+});
+
+var checkVideoPlaying = video => checkHasFrame(video)
+ .then(() => {
+ let startPixel = { data: h.getPixel(video, offsetX, offsetY)
+ , name: "startcolor"
+ };
+ return h.waitForPixel(video, offsetX, offsetY, px => {
+ let result = h.isPixelNot(px, startPixel, threshold)
+ info("Checking playing, [" + Array.slice(px) + "] vs [" +
+ Array.slice(startPixel.data) + "]. Pass=" + result);
+ return result;
+ });
+ });
+
+var checkVideoPaused = video => checkHasFrame(video)
+ .then(() => {
+ let startPixel = { data: h.getPixel(video, offsetX, offsetY)
+ , name: "startcolor"
+ };
+ return h.waitForPixel(video, offsetX, offsetY, px => {
+ let result = h.isOpaquePixelNot(px, startPixel, threshold);
+ info("Checking paused, [" + Array.slice(px) + "] vs [" +
+ Array.slice(startPixel.data) + "]. Pass=" + result);
+ return result;
+ }, pausedTimeout);
+ }).then(result => ok(!result, "Frame shouldn't change within " + pausedTimeout / 1000 + " seconds."));
+
+runTest(() => getUserMedia({video: true, fake: true})
+ .then(stream => {
+ gUMVideoElement =
+ createMediaElement("video", "gUMVideo");
+ gUMVideoElement.srcObject = stream;
+ gUMVideoElement.play();
+
+ info("Capturing");
+ captureStreamElement =
+ createMediaElement("video", "captureStream");
+ captureStreamElement.srcObject = gUMVideoElement.mozCaptureStream();
+ captureStreamElement.play();
+
+ // 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]);
+
+ return checkVideoPlaying(captureStreamElement);
+ })
+ .then(() => {
+ info("Video flowing. Pausing.");
+ gUMVideoElement.pause();
+
+ return checkVideoPaused(captureStreamElement);
+ })
+ .then(() => {
+ info("Video stopped flowing. Playing.");
+ gUMVideoElement.play();
+
+ return checkVideoPlaying(captureStreamElement);
+ })
+ .then(() => {
+ info("Video flowing. Removing source.");
+ var stream = gUMVideoElement.srcObject;
+ gUMVideoElement.srcObject = null;
+
+ return checkVideoPaused(captureStreamElement).then(() => stream);
+ })
+ .then(stream => {
+ info("Video stopped flowing. Setting source.");
+ gUMVideoElement.srcObject = stream;
+ return checkVideoPlaying(captureStreamElement);
+ })
+ .then(() => {
+ info("Video flowing. Changing source by track manipulation. Remove first.");
+ var track = gUMVideoElement.srcObject.getTracks()[0];
+ gUMVideoElement.srcObject.removeTrack(track);
+ return checkVideoPaused(captureStreamElement).then(() => track);
+ })
+ .then(track => {
+ info("Video paused. Changing source by track manipulation. Add first.");
+ gUMVideoElement.srcObject.addTrack(track);
+ gUMVideoElement.play();
+ return checkVideoPlaying(captureStreamElement);
+ })
+ .then(() => {
+ 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/tests/mochitest/test_getUserMedia_mediaStreamClone.html b/dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html
new file mode 100644
index 000000000..8d4d5e559
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html
@@ -0,0 +1,251 @@
+<!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(() => Promise.resolve()
+ .then(() => getUserMedia({audio: true, video: true})).then(stream => {
+ info("Test clone()ing an audio/video gUM stream");
+ var clone = stream.clone();
+
+ checkMediaStreamCloneAgainstOriginal(clone, stream);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getAudioTracks()[0],
+ stream.getAudioTracks()[0]);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getVideoTracks()[0],
+ stream.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("Stopping original tracks");
+ stream.getTracks().forEach(t => t.stop());
+
+ info("Playing from track clones");
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, clone);
+ return playback.playMedia(false);
+ })
+ .then(() => getUserMedia({video: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test addTrack()ing a video track to a stream without affecting its clone");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ var streamClone = stream.clone();
+ var 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();
+ otherStream.stop();
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, streamClone);
+ return playback.playMedia(false)
+ .then(() => stream.getTracks().forEach(t => t.stop()))
+ .then(() => stream.stop());
+ }))
+ .then(() => getUserMedia({audio: true, video: true})).then(stream => {
+ info("Test cloning a stream into inception");
+ var clone = stream;
+ var clones = Array(10).fill().map(() => clone = clone.clone());
+ var 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");
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, inceptionClone);
+ return playback.playMedia(false)
+ .then(() => clones.forEach(c => c.getTracks().forEach(t => t.stop())));
+ })
+ .then(() => getUserMedia({audio: true, video: true})).then(stream => {
+ info("Test adding tracks from many stream clones to the original stream");
+
+ const LOOPS = 3;
+ for (var 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"));
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => {
+ info("Testing audio content routing with MediaStream.clone()");
+ var ac = new AudioContext();
+
+ var osc1kOriginal = createOscillatorStream(ac, 1000);
+ var audioTrack1kOriginal = osc1kOriginal.getTracks()[0];
+ var audioTrack1kClone = osc1kOriginal.clone().getTracks()[0];
+
+ var osc5kOriginal = createOscillatorStream(ac, 5000);
+ var audioTrack5kOriginal = osc5kOriginal.getTracks()[0];
+ var audioTrack5kClone = osc5kOriginal.clone().getTracks()[0];
+
+ return Promise.resolve().then(() => {
+ info("Analysing audio output of original stream (1k + 5k)");
+ var stream = new MediaStream();
+ stream.addTrack(audioTrack1kOriginal);
+ stream.addTrack(audioTrack5kOriginal);
+
+ 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 original tracks to stop");
+ stream.getTracks().forEach(t => t.stop());
+ return 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);
+ })
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output of stream clone (1k + 5k)");
+ var stream = new MediaStream();
+ stream.addTrack(audioTrack1kClone);
+ 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(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output of clone of clone (1k + 5k)");
+ var stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]).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());
+ }).then(() => {
+ info("Analysing audio output of clone() + addTrack()ed tracks (1k + 5k)");
+ var stream =
+ new MediaStream(new MediaStream([ audioTrack1kClone
+ , audioTrack5kClone
+ ]).clone().getTracks());
+
+ 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());
+ }).then(() => {
+ info("Analysing audio output of clone()d tracks in original stream (1k) " +
+ "and clone()d tracks in stream clone (5k)");
+ var stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ var streamClone = stream.clone();
+
+ stream.getTracks().forEach(t => stream.removeTrack(t));
+ stream.addTrack(streamClone.getTracks()[0]);
+ streamClone.removeTrack(streamClone.getTracks()[0]);
+
+ 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)] < 50)
+ .then(() => {
+ analyser.disconnect();
+ var cloneAnalyser = new AudioStreamAnalyser(ac, streamClone);
+ 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());
+ });
+ }).then(() => {
+ info("Analysing audio output enabled and disabled tracks that don't affect each other");
+ var stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ var clone = stream.clone();
+
+ stream.getTracks()[0].enabled = true;
+ stream.getTracks()[1].enabled = false;
+
+ clone.getTracks()[0].enabled = false;
+ clone.getTracks()[1].enabled = true;
+
+ 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)] < 50)
+ .then(() => {
+ analyser.disconnect();
+ var cloneAnalyser = new AudioStreamAnalyser(ac, clone);
+ 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(() => stream.getTracks().forEach(t => t.enabled = true));
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_mediaStreamConstructors.html b/dom/media/tests/mochitest/test_getUserMedia_mediaStreamConstructors.html
new file mode 100644
index 000000000..4ea6e3f44
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_mediaStreamTrackClone.html b/dom/media/tests/mochitest/test_getUserMedia_mediaStreamTrackClone.html
new file mode 100644
index 000000000..e5e076442
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_peerIdentity.html b/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
new file mode 100644
index 000000000..ab75eea77
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
@@ -0,0 +1,38 @@
+<!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"
+});
+function theTest() {
+ function testPeerIdentityConstraint(withConstraint) {
+ var config = { audio: true, video: true };
+ if (withConstraint) {
+ config.peerIdentity = 'user@example.com';
+ }
+ info('getting media with constraints: ' + JSON.stringify(config));
+ return getUserMedia(config)
+ .then(stream => Promise.all([
+ audioIsSilence(withConstraint, stream),
+ videoIsBlack(withConstraint, stream)
+ ]).then(() => stream.getTracks().forEach(t => t.stop())));
+ };
+
+ // both without and with the constraint
+ return testPeerIdentityConstraint(false)
+ .then(() => testPeerIdentityConstraint(true));
+}
+
+runTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html b/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
new file mode 100644
index 000000000..99339a831
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(testAudio, audioStream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html b/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
new file mode 100644
index 000000000..04c56c062
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(testVideo, stream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html b/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
new file mode 100644
index 000000000..784235e08
--- /dev/null
+++ b/dom/media/tests/mochitest/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 LocalMediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithoutStoppingTracks(false)
+ .then(() => streamPlayback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_scarySources.html b/dom/media/tests/mochitest/test_getUserMedia_scarySources.html
new file mode 100644
index 000000000..2f1b2195b
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_scarySources.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="head.js"></script>
+ <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"});
+
+netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+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);
+}, topic, false));
+
+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) =>
+ window.navigator.mozGetUserMediaDevices({}, resolve, reject,
+ innerWindowID, callID));
+ return devices.map(d => d.QueryInterface(Ci.nsIMediaDevice));
+};
+
+runTest(async () => {
+ try {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ return;
+ }
+
+ 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 = devices.filter(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 = devices.filter(d => d.scary);
+ is(devices.length, numScreens, "All screens are scary.");
+ } catch(e) {
+ ok(false, e);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_spinEventLoop.html b/dom/media/tests/mochitest/test_getUserMedia_spinEventLoop.html
new file mode 100644
index 000000000..ae691785f
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_stopAudioStream.html b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html
new file mode 100644
index 000000000..133e398d5
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.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 Stop Audio Stream", bug: "822109" });
+ /**
+ * Run a test to verify that we can start an audio stream in a media element,
+ * call stop() on the stream, and successfully get an ended event fired.
+ */
+ runTest(function () {
+ return getUserMedia({audio: true})
+ .then(stream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var streamPlayback = new LocalMediaStreamPlayback(testAudio, stream);
+
+ return streamPlayback.playMediaWithDeprecatedStreamStop(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
new file mode 100644
index 000000000..32c03b258
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Stop Audio Stream With Followup Audio", bug: "822109" });
+ /**
+ * Run a test to verify that I can complete an audio gum playback in a media
+ * element, stop the stream, and then complete another audio gum playback
+ * in a media element.
+ */
+ runTest(function () {
+ return getUserMedia({audio: true})
+ .then(firstStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var streamPlayback = new LocalMediaStreamPlayback(testAudio, firstStream);
+
+ return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+ .then(() => getUserMedia({audio: true}))
+ .then(secondStream => {
+ streamPlayback.mediaStream = secondStream;
+
+ return streamPlayback.playMedia(false)
+ .then(() => secondStream.stop());
+ });
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html
new file mode 100644
index 000000000..697fc9773
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.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 Stop Video Audio Stream", bug: "822109" });
+ /**
+ * Run a test to verify that we can start a video+audio stream in a
+ * media element, call stop() on the stream, and successfully get an
+ * ended event fired.
+ */
+ runTest(function () {
+ return getUserMedia({video: true, audio: true})
+ .then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var playback = new LocalMediaStreamPlayback(testVideo, stream);
+
+ return playback.playMediaWithDeprecatedStreamStop(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html
new file mode 100644
index 000000000..c22985dc2
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.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 Stop Video+Audio Stream With Followup Video+Audio",
+ bug: "822109"
+ });
+ /**
+ * Run a test to verify that I can complete an video+audio gum playback in a
+ * media element, stop the stream, and then complete another video+audio gum
+ * playback in a media element.
+ */
+ runTest(function () {
+ return getUserMedia({video: true, audio: true})
+ .then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+ .then(() => getUserMedia({video: true, audio: true}))
+ .then(secondStream => {
+ streamPlayback.mediaStream = secondStream;
+
+ return streamPlayback.playMedia(false)
+ .then(() => secondStream.stop());
+ });
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html
new file mode 100644
index 000000000..823e42a18
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.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 Stop Video Stream", bug: "822109" });
+ /**
+ * Run a test to verify that we can start a video stream in a
+ * media element, call stop() on the stream, and successfully get an
+ * ended event fired.
+ */
+ runTest(function () {
+ return getUserMedia({video: true})
+ .then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithDeprecatedStreamStop(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
new file mode 100644
index 000000000..c2c5591ff
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Stop Video Stream With Followup Video", bug: "822109" });
+ /**
+ * Run a test to verify that I can complete an video gum playback in a
+ * media element, stop the stream, and then complete another video gum
+ * playback in a media element.
+ */
+ runTest(function () {
+ return getUserMedia({video: true})
+ .then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+ .then(() => getUserMedia({video: true}))
+ .then(secondStream => {
+ streamPlayback.mediaStream = secondStream;
+
+ return streamPlayback.playMedia(false)
+ .then(() => secondStream.stop());
+ });
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_getUserMedia_trackCloneCleanup.html b/dom/media/tests/mochitest/test_getUserMedia_trackCloneCleanup.html
new file mode 100644
index 000000000..ae72b54a9
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_getUserMedia_trackEnded.html b/dom/media/tests/mochitest/test_getUserMedia_trackEnded.html
new file mode 100644
index 000000000..46d0cabc5
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_trackEnded.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<iframe id="iframe" 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";
+
+ createHTML({
+ title: "getUserMedia MediaStreamTrack 'ended' event on navigating",
+ bug: "1208373",
+ });
+
+ runTest(() => {
+ 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/tests/mochitest/test_ondevicechange.html b/dom/media/tests/mochitest/test_ondevicechange.html
new file mode 100644
index 000000000..20894cff7
--- /dev/null
+++ b/dom/media/tests/mochitest/test_ondevicechange.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1041393
+-->
+<head>
+ <meta charset="utf-8">
+ <title>onndevicechange tests</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1152383">ondevicechange tests</a>
+<script type="application/javascript">
+
+const RESPONSE_WAIT_TIME_MS = 10000;
+
+function wait(time, message) {
+ return new Promise(r => setTimeout(() => r(message), time));
+}
+
+function OnDeviceChangeEvent() {
+ return new Promise(resolve => navigator.mediaDevices.ondevicechange = resolve);
+}
+
+function OnDeviceChangeEventReceived() {
+ return Promise.race([
+ OnDeviceChangeEvent(),
+ wait(RESPONSE_WAIT_TIME_MS).then(() => Promise.reject("Timed out while waiting for devicechange event"))
+ ]);
+}
+
+function OnDeviceChangeEventNotReceived() {
+ return Promise.race([
+ OnDeviceChangeEvent().then(() => Promise.reject("ondevicechange event is fired unexpectedly.")),
+ wait(RESPONSE_WAIT_TIME_MS)
+ ]);
+}
+
+var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+var videoTracks;
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Fake devicechange event is fired periodically, \
+so we need to wait a while to make sure the event is fired or not as we expect.");
+
+var videoTracks;
+
+function wait(time, message) {
+ return new Promise(r => setTimeout(() => r(message), time));
+}
+
+pushPrefs(["media.ondevicechange.fakeDeviceChangeEvent.enabled", true])
+.then(() => pushPrefs(["media.navigator.permission.disabled", false]))
+.then(() => pushPrefs(["media.ondevicechange.enabled", true]))
+.then(() => info("assure devicechange event is NOT fired when gUM is NOT in use and permanent permission is NOT granted"))
+.then(() => OnDeviceChangeEventNotReceived())
+.then(() => ok(true, "devicechange event is NOT fired when gUM is NOT in use and permanent permission is NOT granted"))
+.then(() => pushPrefs(['media.navigator.permission.disabled', true]))
+.then(() => info("assure devicechange event is fired when gUM is NOT in use and permanent permission is granted"))
+.then(() => OnDeviceChangeEventReceived())
+.then(() => ok(true, "devicechange event is fired when gUM is NOT in use and permanent permission is granted"))
+.then(() => navigator.mediaDevices.getUserMedia({video: true, fake: true}))
+.then(st => {videoTracks = st.getVideoTracks();})
+.then(() => info("assure devicechange event is fired when gUM is in use"))
+.then(() => OnDeviceChangeEventReceived())
+.then(() => ok(true, "devicechange event is fired when gUM is in use"))
+.catch(e => ok(false, "Error: " + e))
+.then(() => {
+ if(videoTracks)
+ videoTracks.forEach(track => track.stop());
+})
+.then(() => SimpleTest.finish());
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_addAudioTrackToExistingVideoStream.html b/dom/media/tests/mochitest/test_peerConnection_addAudioTrackToExistingVideoStream.html
new file mode 100644
index 000000000..0d1ea10b6
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addAudioTrackToExistingVideoStream.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: "1246310",
+ title: "Renegotiation: add audio track to existing video-only stream",
+ });
+
+ runNetworkTest(function (options) {
+ 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/tests/mochitest/test_peerConnection_addDataChannel.html b/dom/media/tests/mochitest/test_peerConnection_addDataChannel.html
new file mode 100644
index 000000000..366e8fe41
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html
new file mode 100644
index 000000000..782c8fddd
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_addIceCandidate.html b/dom/media/tests/mochitest/test_peerConnection_addIceCandidate.html
new file mode 100644
index 000000000..93cbdd083
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addIceCandidate.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1087551",
+ title: "addIceCandidate behavior (local and remote) including invalid data"
+ });
+
+ var test;
+ runNetworkTest(function () {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_GET_ANSWER");
+
+ test.chain.insertAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION", [
+ function PC_LOCAL_ADD_CANDIDATE_EARLY(test) {
+ var candidate = new RTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+ sdpMLineIndex: 0});
+ return test.pcLocal._pc.addIceCandidate(candidate).then(
+ generateErrorCallback("addIceCandidate should have failed."),
+ err => {
+ is(err.name, "InvalidStateError", "Error is InvalidStateError");
+ });
+ }
+ ]);
+ test.chain.insertAfter("PC_REMOTE_SET_LOCAL_DESCRIPTION", [
+ function PC_REMOTE_ADD_CANDIDATE_INVALID_INDEX(test) {
+ var invalid_index = new RTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+ sdpMLineIndex: 2});
+ return test.pcRemote._pc.addIceCandidate(invalid_index)
+ .then(
+ generateErrorCallback("addIceCandidate should have failed."),
+ err => {
+ is(err.name, "InvalidCandidateError", "Error is InvalidCandidateError");
+ }
+ );
+ },
+ function PC_REMOTE_ADD_BOGUS_CANDIDATE(test) {
+ var bogus = new RTCIceCandidate(
+ {candidate:"Pony Lords, jump!",
+ sdpMLineIndex: 0});
+ return test.pcRemote._pc.addIceCandidate(bogus)
+ .then(
+ generateErrorCallback("addIceCandidate should have failed."),
+ err => {
+ is(err.name, "InvalidCandidateError", "Error is InvalidCandidateError");
+ }
+ );
+ },
+ function PC_REMOTE_ADD_CANDIDATE_MISSING_INDEX(test) {
+ // Note: it is probably not a good idea to automatically fill a missing
+ // MLineIndex with a default value of zero, see bug 1157034
+ var broken = new RTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host"});
+ return test.pcRemote._pc.addIceCandidate(broken)
+ .then(
+ // FIXME this needs to be updated once bug 1157034 is fixed
+ todo(false, "Missing index in got automatically set to a valid value bz://1157034")
+ );
+ },
+ function PC_REMOTE_ADD_VALID_CANDIDATE(test) {
+ var candidate = new RTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+ sdpMLineIndex: 0});
+ return test.pcRemote._pc.addIceCandidate(candidate)
+ .then(ok(true, "Successfully added valid ICE candidate"));
+ },
+ // bug 1095793
+ function PC_REMOTE_ADD_MISMATCHED_MID_AND_LEVEL_CANDIDATE(test) {
+ var bogus = new mozRTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+ sdpMLineIndex: 0,
+ sdpMid: "sdparta_1"});
+ return test.pcRemote._pc.addIceCandidate(bogus)
+ .then(
+ generateErrorCallback("addIceCandidate should have failed."),
+ err => {
+ is(err.name, "InvalidCandidateError", "Error is InvalidCandidateError");
+ }
+ );
+ },
+ function PC_REMOTE_ADD_MATCHING_MID_AND_LEVEL_CANDIDATE(test) {
+ var candidate = new mozRTCIceCandidate(
+ {candidate:"candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+ sdpMLineIndex: 0,
+ sdpMid: "sdparta_0"});
+ return test.pcRemote._pc.addIceCandidate(candidate)
+ .then(ok(true, "Successfully added valid ICE candidate with matching mid and level"));
+ }
+ ]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
new file mode 100644
index 000000000..73b28f1af
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.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 second audio stream"
+ });
+
+ 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.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ // 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/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
new file mode 100644
index 000000000..cf9736e87
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.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: "1017888",
+ title: "Renegotiation: add second audio stream, no bundle"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ 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.getAllUserMedia([{audio: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ]
+ );
+
+ // 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/tests/mochitest/test_peerConnection_addSecondVideoStream.html b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
new file mode 100644
index 000000000..ebe7d46b1
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.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 second video stream"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ 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.getAllUserMedia([{video: true}]);
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
new file mode 100644
index 000000000..64dc97fc7
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.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: "1017888",
+ title: "Renegotiation: add second video stream, no bundle"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ 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.getAllUserMedia([{video: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html b/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
new file mode 100644
index 000000000..7f4b52b06
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.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">
+"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 => {
+ 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);
+ test.pcLocal.attachLocalTrack(stream.getTracks()[0], localStream);
+
+ const addTrackPromise = haveEvent(remoteStream, "addtrack",
+ wait(50000, new Error("No addtrack event")))
+ .then(trackEvent => {
+ ok(trackEvent instanceof MediaStreamTrackEvent,
+ "Expected event to be instance of MediaStreamTrackEvent");
+ is(trackEvent.type, "addtrack",
+ "Expected addtrack event type");
+ is(trackEvent.track.id, newTrack.id, "Expected track in event");
+ is(trackEvent.track.readyState, "live",
+ "added track should be live");
+ })
+ .then(() => haveNoEvent(remoteStream, "addtrack"));
+
+ const remoteTrack = test.pcRemote._pc.getReceivers()
+ .map(r => r.track)
+ .find(t => t.kind == "video");
+ ok(remoteTrack, "Should have received remote track");
+ const endedPromise = haveEvent(remoteTrack, "ended",
+ wait(50000, new Error("No ended event")));
+
+ eventsPromise = Promise.all([addTrackPromise, endedPromise]);
+
+ remoteStream.addEventListener("removetrack",
+ function onRemovetrack(trackEvent) {
+ ok(false, "UA shouldn't raise 'removetrack' when receiving peer connection");
+ })
+ });
+ },
+ ],
+ [
+ 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/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html b/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
new file mode 100644
index 000000000..6efc4cf8b
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.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: 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.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html b/dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html
new file mode 100644
index 000000000..8160a5edc
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_audioRenegotiationInactiveAnswer.html
@@ -0,0 +1,59 @@
+<!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}], []);
+
+ test.chain.append([
+ 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/tests/mochitest/test_peerConnection_basicAudio.html b/dom/media/tests/mochitest/test_peerConnection_basicAudio.html
new file mode 100644
index 000000000..9dfd11ea7
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html
new file mode 100644
index 000000000..6a5379d37
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioNATRelay.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelay.html
new file mode 100644
index 000000000..898ecba19
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelay.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 port dependent NAT"
+ });
+
+ var test;
+ runNetworkTest(options => {
+ SpecialPowers.pushPrefEnv(
+ {
+ 'set': [
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT']
+ ]
+ }, function (options) {
+ options = options || {};
+ options.expectedLocalCandidateType = "serverreflexive";
+ options.expectedRemoteCandidateType = "relayed";
+ // If both have TURN, it is a toss-up which one will end up using a
+ // relay.
+ options.turn_disabled_local = true;
+ 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/tests/mochitest/test_peerConnection_basicAudioNATRelayTCP.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelayTCP.html
new file mode 100644
index 000000000..d2ef92b37
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATRelayTCP.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: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT that blocks UDP"
+ });
+
+ var test;
+ runNetworkTest(options => {
+ SpecialPowers.pushPrefEnv(
+ {
+ 'set': [
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true]
+ ]
+ }, function (options) {
+ options = options || {};
+ options.expectedLocalCandidateType = "relayed-tcp";
+ options.expectedRemoteCandidateType = "relayed-tcp";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.run();
+ })
+ }, { useIceServer: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_basicAudioNATSrflx.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATSrflx.html
new file mode 100644
index 000000000..d6414d64b
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioNATSrflx.html
@@ -0,0 +1,41 @@
+<!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"
+ });
+
+ var test;
+ runNetworkTest(options => {
+ SpecialPowers.pushPrefEnv(
+ {
+ 'set': [
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT']
+ ]
+ }, function (options) {
+ options = options || {};
+ options.expectedLocalCandidateType = "serverreflexive";
+ options.expectedRemoteCandidateType = "serverreflexive";
+ 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/tests/mochitest/test_peerConnection_basicAudioPcmaPcmuOnly.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioPcmaPcmuOnly.html
new file mode 100644
index 000000000..bd5e4c25d
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioRequireEOC.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioRequireEOC.html
new file mode 100644
index 000000000..e3e44cf1b
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioVideo.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideo.html
new file mode 100644
index 000000000..0af2b207a
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html
new file mode 100644
index 000000000..ae51e4a1c
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
new file mode 100644
index 000000000..c840804a6
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html
new file mode 100644
index 000000000..f9e6c81cd
--- /dev/null
+++ b/dom/media/tests/mochitest/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 disbaled 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/tests/mochitest/test_peerConnection_basicAudioVideoNoRtcpMux.html b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoRtcpMux.html
new file mode 100644
index 000000000..da8dd2d55
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicH264Video.html b/dom/media/tests/mochitest/test_peerConnection_basicH264Video.html
new file mode 100644
index 000000000..520a812c2
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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/tests/mochitest/test_peerConnection_basicScreenshare.html b/dom/media/tests/mochitest/test_peerConnection_basicScreenshare.html
new file mode 100644
index 000000000..08db1b161
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_basicScreenshare.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: "1039666",
+ title: "Basic screenshare-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ SimpleTest.finish();
+ return;
+ }
+ test = new PeerConnectionTest(options);
+ var constraints = {
+ video: {
+ mozMediaSource: "screen",
+ mediaSource: "screen"
+ },
+ fake: false
+ };
+ test.setMediaConstraints([constraints], [constraints]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_basicVideo.html b/dom/media/tests/mochitest/test_peerConnection_basicVideo.html
new file mode 100644
index 000000000..ce071a516
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_basicWindowshare.html b/dom/media/tests/mochitest/test_peerConnection_basicWindowshare.html
new file mode 100644
index 000000000..387ea9aec
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_basicWindowshare.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: "1038926",
+ title: "Basic windowshare-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (IsMacOSX10_6orOlder() || isWinXP) {
+ ok(true, "Screensharing disabled for OSX10.6 and WinXP");
+ SimpleTest.finish();
+ return;
+ }
+ test = new PeerConnectionTest(options);
+ var constraints = {
+ video: {
+ mozMediaSource: "window",
+ mediaSource: "window"
+ },
+ fake: false
+ };
+ test.setMediaConstraints([constraints], [constraints]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_bug1013809.html b/dom/media/tests/mochitest/test_peerConnection_bug1013809.html
new file mode 100644
index 000000000..69d36ee39
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_bug1042791.html b/dom/media/tests/mochitest/test_peerConnection_bug1042791.html
new file mode 100644
index 000000000..f5710e68d
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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/tests/mochitest/test_peerConnection_bug1064223.html b/dom/media/tests/mochitest/test_peerConnection_bug1064223.html
new file mode 100644
index 000000000..acb72d0ba
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_bug1064223.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: "1064223",
+ title: "CreateOffer fails without streams or modern RTCOfferOptions"
+ });
+
+ runNetworkTest(function () {
+ var pc = new mozRTCPeerConnection();
+ var options = { mandatory: { OfferToReceiveVideo: true } }; // obsolete
+
+ pc.createOffer(options).then(() => ok(false, "createOffer must fail"),
+ e => is(e.name, "InternalError",
+ "createOffer must fail"))
+ .catch(e => ok(false, e.message))
+ .then(() => {
+ pc.close();
+ networkTestFinished();
+ })
+ .catch(e => ok(false, e.message));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_bug1227781.html b/dom/media/tests/mochitest/test_peerConnection_bug1227781.html
new file mode 100644
index 000000000..17a7c1106
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_bug822674.html b/dom/media/tests/mochitest/test_peerConnection_bug822674.html
new file mode 100644
index 000000000..b757ec7cd
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_bug825703.html b/dom/media/tests/mochitest/test_peerConnection_bug825703.html
new file mode 100644
index 000000000..c25079753
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_bug825703.html
@@ -0,0 +1,162 @@
+<!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;
+
+ makePC();
+
+ makePC(1, "TypeError");
+
+ makePC({});
+
+ makePC({ iceServers: [] });
+
+ makePC({ iceServers: [{ urls:"" }] }, "SyntaxError");
+
+ 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: ["stun:127.0.0.1", ""] }] }, "SyntaxError");
+
+ makePC({ iceServers: [{ urls:"turns:localhost:3478", username:"p" }] }, "InvalidAccessError");
+
+ makePC({ iceServers: [{ url:"turns:localhost:3478", credential:"p" }] }, "InvalidAccessError");
+
+ 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
+ var 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:"token" },
+ ],
+ };
+ var pc = new RTCPeerConnection(config);
+ is(JSON.stringify(toComparable(pc.getConfiguration())),
+ JSON.stringify(toComparable(config)), "getConfiguration");
+ pc.close();
+
+ var push = prefs => new Promise(resolve =>
+ SpecialPowers.pushPrefEnv(prefs, resolve));
+
+ 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(() => {
+ var consoleService = SpecialPowers.Cc["@mozilla.org/consoleservice;1"]
+ .getService(SpecialPowers.Ci.nsIConsoleService);
+ var warning = "";
+ var listener = SpecialPowers.wrapCallbackObject({
+ QueryInterface(iid) {
+ if (![SpecialPowers.Ci.nsIConsoleListener,
+ SpecialPowers.Ci.nsISupports].some(i => iid.equals(i))) {
+ throw SpecialPowers.Cr.NS_NOINTERFACE;
+ }
+ return this;
+ },
+
+ observe(msg) {
+ if (msg.message.includes("JavaScript Warning")) {
+ warning = msg.message;
+ }
+ }
+ });
+ consoleService.registerListener(listener);
+ lineNumberAndFunction.func();
+ // Console output is asynchronous, so we must queue a task.
+ return wait(0).then(() => {
+ is(warning.split('"')[1],
+ "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");
+ consoleService.unregisterListener(listener);
+ });
+ })
+ .then(networkTestFinished);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_bug827843.html b/dom/media/tests/mochitest/test_peerConnection_bug827843.html
new file mode 100644
index 000000000..01d566981
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_bug827843.html
@@ -0,0 +1,70 @@
+<!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;
+
+ // handle the event which the close() triggers
+ var localClosed = new Promise(resolve => {
+ test.pcLocal.onsignalingstatechange = e => {
+ is(e.target.signalingState, "closed",
+ "Received expected onsignalingstatechange event on 'closed'");
+ resolve();
+ }
+ });
+
+ 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;
+
+ // handle the event which the close() triggers
+ var remoteClosed = new Promise(resolve => {
+ test.pcRemote.onsignalingstatechange = e => {
+ is(e.target.signalingState, "closed",
+ "Received expected onsignalingstatechange event on 'closed'");
+ resolve();
+ }
+ });
+
+ 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");
+
+ return Promise.all([localClosed, remoteClosed]);
+ }
+];
+
+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/tests/mochitest/test_peerConnection_bug834153.html b/dom/media/tests/mochitest/test_peerConnection_bug834153.html
new file mode 100644
index 000000000..40c25eecf
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_callbacks.html b/dom/media/tests/mochitest/test_peerConnection_callbacks.html
new file mode 100644
index 000000000..8bf4b106d
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_callbacks.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;version=1.8">
+ 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.mozSrcObject = 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.mozSrcObject = 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 && v2.mozSrcObject.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."))
+ .then(() => pcall(pc1, pc1.getStats, null))
+ .then(stats => ok(Object.keys(stats).length > 0, "pc1 has stats"))
+ .then(() => pcall(pc2, pc2.getStats, null))
+ .then(stats => ok(Object.keys(stats).length > 0, "pc2 has stats"))
+ .then(() => { v1.pause(); v2.pause(); })
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(networkTestFinished);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html
new file mode 100644
index 000000000..97010929a
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.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;version=1.8">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(2D)::CaptureStream as video-only input to peerconnection",
+ visible: true
+});
+
+runNetworkTest(() => {
+ 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_DRAW_INITIAL_LOCAL_GREEN(test) {
+ h.drawColor(canvas, h.green);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ stream = canvas.captureStream(0);
+ test.pcLocal.attachLocalStream(stream);
+ }
+ ]);
+ 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.waitForPixelColor(mediaElement, h.green, 128,
+ "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.waitForPixelColor(mediaElement, h.red, 128,
+ "pcRemote's remote should become red");
+ }
+ ]);
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
new file mode 100644
index 000000000..d0ebee6ad
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <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;version=1.8">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(WebGL)::CaptureStream as video-only input to peerconnection"
+});
+
+runNetworkTest(() => {
+ 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();
+ }
+ 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 DRAW_LOCAL_GREEN(test) {
+ h.drawColor(canvas, h.green);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ test.pcLocal.canvasStream = canvas.captureStream(0.0);
+ is(test.pcLocal.canvasStream.canvas, canvas, "Canvas attribute is correct");
+ test.pcLocal.attachLocalStream(test.pcLocal.canvasStream);
+ }
+ ]);
+ 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.waitForPixelColor(vremote, h.green, 128,
+ "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.waitForPixelColor(vremote, h.red, 128,
+ "pcRemote's remote should become red");
+ }
+ ]);
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html b/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
new file mode 100644
index 000000000..27bcc9a52
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="text/javascript" src="../../test/manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+var manager = new MediaTestManager;
+
+createHTML({
+ bug: "1081409",
+ title: "Captured video-only over peer connection",
+ visible: true
+}).then(() => new Promise(resolve => {
+ 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));
+
+// Run tests in sequence for log readability.
+PARALLEL_TESTS = 1;
+
+function startTest(media, token) {
+ manager.started(token);
+ var video = document.createElement('video');
+ video.id = "id_" + media.name;
+ video.width = 160;
+ video.height = 120;
+ video.muted = true;
+ video.loop = 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);
+ 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/tests/mochitest/test_peerConnection_certificates.html b/dom/media/tests/mochitest/test_peerConnection_certificates.html
new file mode 100644
index 000000000..6582446bf
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_close.html b/dom/media/tests/mochitest/test_peerConnection_close.html
new file mode 100644
index 000000000..54cae5637
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_close.html
@@ -0,0 +1,147 @@
+<!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 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);
+
+ pc.onsignalingstatechange = function(e) {
+ clearTimeout(eTimeout);
+ is(pc.signalingState, "closed", "signalingState is 'closed'");
+ is(pc.iceConnectionState, "closed", "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(),
+ r => is(r.name, "InvalidStateError", msg));
+
+ doesFail(pc.createOffer(), "createOffer fails on close")
+ .then(() => doesFail(pc.createAnswer(), "createAnswer fails on close"))
+ .then(() => doesFail(pc.setLocalDescription(offer),
+ "setLocalDescription fails on close"))
+ .then(() => doesFail(pc.setRemoteDescription(answer),
+ "setRemoteDescription fails on close"))
+ .then(() => doesFail(pc.addIceCandidate(candidate),
+ "addIceCandidate fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.createOffer(y, n)),
+ "Legacy createOffer fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.createAnswer(y, n)),
+ "Legacy createAnswer fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.setLocalDescription(offer, y, n)),
+ "Legacy setLocalDescription fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.setRemoteDescription(answer, y, n)),
+ "Legacy setRemoteDescription fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.addIceCandidate(candidate, y, n)),
+ "Legacy addIceCandidate 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");
+ };
+
+ // This prevents a mochitest timeout in case the event does not fire
+ eTimeout = setTimeout(function() {
+ ok(false, "Failed to receive expected onsignalingstatechange event in 60s");
+ finish();
+ }, 60000);
+
+ 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'");
+
+ Promise.all([finished, silence]).then(networkTestFinished);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_closeDuringIce.html b/dom/media/tests/mochitest/test_peerConnection_closeDuringIce.html
new file mode 100644
index 000000000..eb8228d03
--- /dev/null
+++ b/dom/media/tests/mochitest/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 = new RTCIceCandidate({"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 = new RTCIceCandidate({"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/tests/mochitest/test_peerConnection_constructedStream.html b/dom/media/tests/mochitest/test_peerConnection_constructedStream.html
new file mode 100644
index 000000000..36df228ee
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_constructedStream.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+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");
+ sentTracks.forEach(t =>
+ ok(receivedStream.getTracks().find(t2 => t.id == t2.id),
+ "The sent track (" + t.id + ") should exist on the receive side"));
+ };
+
+ 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/tests/mochitest/test_peerConnection_errorCallbacks.html b/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
new file mode 100644
index 000000000..6d152a4fa
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "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(new RTCSessionDescription({ 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(new RTCSessionDescription({ 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/tests/mochitest/test_peerConnection_forwarding_basicAudioVideoCombined.html b/dom/media/tests/mochitest/test_peerConnection_forwarding_basicAudioVideoCombined.html
new file mode 100644
index 000000000..5d1253808
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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/tests/mochitest/test_peerConnection_iceFailure.html b/dom/media/tests/mochitest/test_peerConnection_iceFailure.html
new file mode 100644
index 000000000..cbdfd018a
--- /dev/null
+++ b/dom/media/tests/mochitest/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 = new RTCIceCandidate({"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 = new RTCIceCandidate({"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/tests/mochitest/test_peerConnection_insertDTMF.html b/dom/media/tests/mochitest/test_peerConnection_insertDTMF.html
new file mode 100644
index 000000000..0bfb4eef6
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_insertDTMF.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;version=1.8">
+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.indexOf("A") == -1, "calling insertDTMF should replace current characters");
+
+ sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ ok(sender.dtmf.toneBuffer.indexOf("A") != -1, "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);
+ }
+ ]);
+
+ var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+ 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/tests/mochitest/test_peerConnection_localReofferRollback.html b/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
new file mode 100644
index 000000000..0b0b35fe8
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.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: "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.getAllUserMedia([{audio: true}]);
+ },
+
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ if (test.testOptions.steeplechase) {
+ test.pcRemote.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+
+ 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) {
+ return test.setLocalDescription(
+ test.pcRemote,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleIce;
+ },
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_localRollback.html b/dom/media/tests/mochitest/test_peerConnection_localRollback.html
new file mode 100644
index 000000000..2a35920ba
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_localRollback.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 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) {
+ return test.setLocalDescription(
+ test.pcRemote,
+ new RTCSessionDescription({ 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);
+ if (test.testOptions.steeplechase) {
+ test.pcRemote.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+ ]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_multiple_captureStream_canvas_2d.html b/dom/media/tests/mochitest/test_peerConnection_multiple_captureStream_canvas_2d.html
new file mode 100644
index 000000000..557cdb791
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_multiple_captureStream_canvas_2d.html
@@ -0,0 +1,99 @@
+<!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(() => {
+ 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');
+
+ test.setMediaConstraints([{video: true}, {video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function DRAW_INITIAL_LOCAL1_GREEN(test) {
+ h.drawColor(canvas1, h.green);
+ },
+ function DRAW_INITIAL_LOCAL2_BLUE(test) {
+ h.drawColor(canvas2, h.blue);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ 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);
+ }
+ ]);
+
+ 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 green, but this will make sure that one is
+ // green and one is blue
+ return Promise.race([
+ Promise.all([
+ h.waitForPixelColor(vremote1, h.green, 128,
+ "pcRemote's remote1 should become green"),
+ h.waitForPixelColor(vremote2, h.blue, 128,
+ "pcRemote's remote2 should become blue")
+ ]),
+ Promise.all([
+ h.waitForPixelColor(vremote2, h.green, 128,
+ "pcRemote's remote2 should become green"),
+ h.waitForPixelColor(vremote1, h.blue, 128,
+ "pcRemote's remote1 should become blue")
+ ])
+ ]);
+ },
+ function DRAW_LOCAL1_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.
+ stream1.requestFrame();
+ h.drawColor(canvas1, h.red);
+ },
+ function DRAW_LOCAL2_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.
+ stream2.requestFrame();
+ h.drawColor(canvas2, h.red);
+ },
+ function WAIT_FOR_REMOTE1_RED() {
+ return h.waitForPixelColor(vremote1, h.red, 128,
+ "pcRemote's remote1 should become red");
+ },
+ function WAIT_FOR_REMOTE2_RED() {
+ return h.waitForPixelColor(vremote2, h.red, 128,
+ "pcRemote's remote2 should become red");
+ }
+ ]);
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html b/dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html
new file mode 100644
index 000000000..88db25f39
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_noTrickleOffer.html b/dom/media/tests/mochitest/test_peerConnection_noTrickleOffer.html
new file mode 100644
index 000000000..ac88277a2
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html b/dom/media/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html
new file mode 100644
index 000000000..9f8668eb6
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
new file mode 100644
index 000000000..c050c6198
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
new file mode 100644
index 000000000..975091679
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
new file mode 100644
index 000000000..ec8007971
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_promiseSendOnly.html b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
new file mode 100644
index 000000000..61cecafb5
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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 && v2.srcObject.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/tests/mochitest/test_peerConnection_relayOnly.html b/dom/media/tests/mochitest/test_peerConnection_relayOnly.html
new file mode 100644
index 000000000..49f4eca2b
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_relayOnly.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: "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(() => waitForIceConnected(test, test.pcLocal), "ICE failed", "Local ICE"),
+ isFail(() => waitForIceConnected(test, test.pcRemote), "ICE failed", "Remote ICE")
+ ])
+ .then(() => ok(true, "ICE on both sides must fail."));
+}
+
+var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+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/tests/mochitest/test_peerConnection_remoteReofferRollback.html b/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
new file mode 100644
index 000000000..6b518e1d2
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
@@ -0,0 +1,69 @@
+<!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.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+ test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION',
+ [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ if (test.testOptions.steeplechase) {
+ test.pcLocal.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(
+ test.pcRemote,
+ new RTCSessionDescription({ type: "rollback" }),
+ STABLE)
+ .then(() => test.pcRemote.rollbackRemoteTracksIfNotNegotiated());
+ },
+
+ 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);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ ],
+ 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/tests/mochitest/test_peerConnection_remoteRollback.html b/dom/media/tests/mochitest/test_peerConnection_remoteRollback.html
new file mode 100644
index 000000000..099628ab8
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_remoteRollback.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: "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,
+ new RTCSessionDescription({ type: "rollback" }),
+ STABLE)
+ .then(() => test.pcRemote.rollbackRemoteTracksIfNotNegotiated());
+ },
+
+ 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/tests/mochitest/test_peerConnection_removeAudioTrack.html b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
new file mode 100644
index 000000000..dfb5c5b4c
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.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: remove audio track"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveAudio: true });
+ return test.pcLocal.removeSender(0);
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify that media stopped flowing from pcLocal
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
new file mode 100644
index 000000000..097cbcc86
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.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: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ 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.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ // 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/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
new file mode 100644
index 000000000..6d814bcae
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.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: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ 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.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ // 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/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
new file mode 100644
index 000000000..7335266ea
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.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: "1017888",
+ title: "Renegotiation: remove then add video track"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ 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.getAllUserMedia([{video: true}]);
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
new file mode 100644
index 000000000..40f037683
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.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: "1017888",
+ title: "Renegotiation: remove then add video track, no bundle"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ 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.getAllUserMedia([{video: true}]);
+ },
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html b/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
new file mode 100644
index 000000000..88fff9531
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.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: "1017888",
+ title: "Renegotiation: remove video track"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveVideo: true });
+ test.setMediaConstraints([], [{video: true}]);
+ return test.pcLocal.removeSender(0);
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify that media stopped flowing from pcLocal
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_renderAfterRenegotiation.html b/dom/media/tests/mochitest/test_peerConnection_renderAfterRenegotiation.html
new file mode 100644
index 000000000..b034eb9af
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_renderAfterRenegotiation.html
@@ -0,0 +1,85 @@
+<!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;version=1.8">
+ 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(function() {
+ var h = new CaptureStreamTestHelper2D();
+ var canvas = document.createElement('canvas');
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 10;
+ document.getElementById('content').appendChild(canvas);
+
+ v2 = createMediaElement('video', 'v2');
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ h.drawColor(canvas, h.blue);
+ var stream = canvas.captureStream(0);
+ stream.getTracks().forEach(t => pc1.addTrack(t, stream));
+
+ 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.waitForPixelColor(v2, h.blue, 128,
+ "pcRemote's video should become green"))
+ .then(() => {
+ stream.requestFrame();
+ h.drawColor(canvas, h.red);
+ })
+ .then(() => h.waitForPixelColor(v2, h.red, 128,
+ "pcRemote's video should become green"))
+
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(networkTestFinished);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
new file mode 100644
index 000000000..8eb224f8d
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+ 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 navigator.mediaDevices.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");
+ return sender.replaceTrack(newTrack);
+ })
+ .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");
+ ok(pc.getLocalStreams().some(s => s.getTracks()
+ .some(t => t == sender.track)),
+ "track exists among pc's local streams");
+ return sender.replaceTrack(audiotrack)
+ .then(() => ok(false, "replacing with different kind should fail"),
+ e => is(e.name, "IncompatibleMediaStreamTrackError",
+ "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 sender.replaceTrack(newTrack)
+ .then(() => {
+ is(pc.getSenders().length, oldSenderCount, "same sender count");
+ ok(!pc.getSenders().some(sn => sn.track == oldTrack),
+ "Replaced track should be removed from senders");
+ ok(allLocalStreamsHaveSender(pc),
+ "Shouldn't have any streams without a corresponding sender");
+ is(sender.track, newTrack, "sender.track has been replaced");
+ 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) {
+ var stream = test.pcLocal._pc.getLocalStreams()[0];
+ var track = stream.getVideoTracks()[0];
+ try {
+ test.pcLocal._pc.addTrack(track, stream);
+ ok(false, "addTrack existing track should fail");
+ } catch (e) {
+ is(e.name, "InvalidParameterError",
+ "addTrack existing track should fail");
+ }
+ try {
+ test.pcLocal._pc.addTrack(track, stream);
+ ok(false, "addTrack existing track should fail");
+ } catch (e) {
+ is(e.name, "InvalidParameterError",
+ "addTrack existing track should fail");
+ }
+ }
+ ]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html b/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
new file mode 100644
index 000000000..5e36d7a5e
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
@@ -0,0 +1,46 @@
+<!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: replaceTrack followed by adding a second video stream"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video:true}], [{video:true}]);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) {
+ var oldstream = test.pcLocal._pc.getLocalStreams()[0];
+ var oldtrack = oldstream.getVideoTracks()[0];
+ var sender = test.pcLocal._pc.getSenders()[0];
+ return navigator.mediaDevices.getUserMedia({video:true})
+ .then(newstream => {
+ var newtrack = newstream.getVideoTracks()[0];
+ return test.pcLocal.senderReplaceTrack(0, newtrack, newstream.id);
+ })
+ .then(() => {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ return test.pcLocal.getAllUserMedia([{video: true}]);
+ });
+ },
+ ]
+ );
+
+ // TODO(bug 1093835):
+ // figure out how to verify if media flows through the new stream
+ // figure out how to verify that media stopped flowing from old stream
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_restartIce.html b/dom/media/tests/mochitest/test_peerConnection_restartIce.html
new file mode 100644
index 000000000..009fe0ff0
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
new file mode 100644
index 000000000..e92617164
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalAndRemoteRollback.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local and remote 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 });
+ }
+ ]
+ );
+
+ test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+ [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ if (test.testOptions.steeplechase) {
+ test.pcLocal.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ if (test.testOptions.steeplechase) {
+ test.pcRemote.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+
+ function PC_LOCAL_EXPECT_ICE_CONNECTED(test) {
+ test.pcLocal.iceCheckingIceRollbackExpected = true;
+ },
+ function PC_REMOTE_EXPECT_ICE_CONNECTED(test) {
+ test.pcRemote.iceCheckingIceRollbackExpected = true;
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(
+ test.pcRemote,
+ new RTCSessionDescription({ 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);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcRemote.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/tests/mochitest/test_peerConnection_restartIceLocalRollback.html b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalRollback.html
new file mode 100644
index 000000000..c1ac186de
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceLocalRollback.html
@@ -0,0 +1,74 @@
+<!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);
+ if (test.testOptions.steeplechase) {
+ test.pcLocal.endOfTrickleIce.then(() => {
+ send_message({"type": "end_of_trickle_ice"});
+ });
+ }
+ },
+ 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_ROLLBACK(test) {
+ 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;
+ },
+ 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/tests/mochitest/test_peerConnection_restartIceNoBundle.html b/dom/media/tests/mochitest/test_peerConnection_restartIceNoBundle.html
new file mode 100644
index 000000000..ffb20e77d
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_restartIceNoBundleNoRtcpMux.html b/dom/media/tests/mochitest/test_peerConnection_restartIceNoBundleNoRtcpMux.html
new file mode 100644
index 000000000..77720b575
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_restartIceNoRtcpMux.html b/dom/media/tests/mochitest/test_peerConnection_restartIceNoRtcpMux.html
new file mode 100644
index 000000000..f3591d0b4
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_scaleResolution.html b/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
new file mode 100644
index 000000000..ec83780b8
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_scaleResolution.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;version=1.8">
+ 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));
+
+ 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);
+
+ 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);
+
+ pc1.onnegotiationneeded = e =>
+ pc1.createOffer()
+ .then(d => pc1.setLocalDescription(codec == "VP8" ? d : removeAllButCodec(d, 126)))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(d))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .catch(generateErrorCallback());
+
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(stream => {
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ v1.srcObject = stream;
+ var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+
+ return mustRejectWith("Invalid scaleResolutionDownBy must reject", "RangeError",
+ () => sender.setParameters({ encodings:
+ [{ scaleResolutionDownBy: 0.5 } ] }))
+ .then(() => sender.setParameters({ encodings: [{ maxBitrate: 60000,
+ scaleResolutionDownBy: 2 }] }))
+ .then(() => new Promise(resolve => pc2.ontrack = e => resolve(e)))
+ .then(e => v2.srcObject = e.streams[0])
+ .then(() => new Promise(resolve => v2.onloadedmetadata = resolve))
+ .then(() => waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => wait(3000)) // TODO: Bug 1248154
+ .then(() => {
+ ok(v1.videoWidth > 0, "source width is positive");
+ ok(v1.videoHeight > 0, "source height is positive");
+ if (v2.videoWidth == 640 && v2.videoHeight == 480) { // TODO: Bug 1248154
+ info("Skipping test due to Bug 1248154");
+ } else {
+ 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");
+ }
+ })
+ .then(() => {
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ })
+ })
+ .catch(generateErrorCallback());
+ }
+
+ runNetworkTest(() => testScale("VP8").then(() => testScale("H264"))
+ .then(networkTestFinished));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
new file mode 100644
index 000000000..ab5ff099e
--- /dev/null
+++ b/dom/media/tests/mochitest/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, "InvalidStateError", "Error is InvalidStateError");
+ });
+ }
+ ]);
+
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
new file mode 100644
index 000000000..35e0654d1
--- /dev/null
+++ b/dom/media/tests/mochitest/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, "InvalidStateError", "Error is InvalidStateError");
+ });
+ }
+ ]);
+
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html b/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
new file mode 100644
index 000000000..5ac150290
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.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: "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([
+ function PC_REMOTE_SET_LOCAL_OFFER(test) {
+ test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
+ .then(err => {
+ is(err.name, "InvalidStateError", "Error is InvalidStateError");
+ });
+ }
+ ]);
+
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_setParameters.html b/dom/media/tests/mochitest/test_peerConnection_setParameters.html
new file mode 100644
index 000000000..1dc7cfb12
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_setParameters.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;version=1.8">
+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) => {
+
+ 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/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html b/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
new file mode 100644
index 000000000..da1b2451e
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html b/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html
new file mode 100644
index 000000000..6c9546bf0
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html b/dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
new file mode 100644
index 000000000..d651a14f1
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.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: "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([
+ function PC_LOCAL_SET_REMOTE_OFFER(test) {
+ 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/tests/mochitest/test_peerConnection_simulcastOffer.html b/dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html
new file mode 100644
index 000000000..de6aeb038
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_simulcastOffer.html
@@ -0,0 +1,142 @@
+<!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 offer",
+ visible: true
+ });
+
+ var test;
+ var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+ function selectRecvSsrc(pc, index) {
+ var receivers = pc._pc.getReceivers();
+ is(receivers.length, 1, "We have exactly one RTP receiver");
+ var receiver = receivers[0];
+
+ SpecialPowers.wrap(pc._pc).mozSelectSsrc(receiver, index);
+ }
+
+ 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(() => {
+ SimpleTest.requestCompleteLog();
+ var helper;
+
+ test = new PeerConnectionTest({bundle: false});
+ test.setMediaConstraints([{video: true}], []);
+
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ helper = new VideoStreamHelper();
+ test.pcLocal.attachLocalStream(helper.stream());
+ }
+ ]);
+
+ test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
+ function PC_LOCAL_SET_RIDS(test) {
+ var senders = test.pcLocal._pc.getSenders();
+ is(senders.length, 1, "We have exactly one RTP sender");
+ var 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:/), "Modified answer has rid");
+ }
+ ]);
+
+ test.chain.insertAfter('PC_REMOTE_WAIT_FOR_MEDIA_FLOW',[
+ function PC_REMOTE_SET_RTP_FIRST_RID(test) {
+ // Cause pcRemote to filter out everything but the first SSRC. This
+ // lets only one of the simulcast streams through.
+ selectRecvSsrc(test.pcRemote, 0);
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return helper.waitForFrames(vremote);
+ },
+ function PC_REMOTE_CHECK_SIZE_1() {
+ var vlocal = test.pcLocal.localMediaElements[0];
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vlocal, "Should have local video element for pcLocal");
+ ok(vremote, "Should have remote video element for pcRemote");
+ ok(vlocal.videoWidth > 0, "source width is positive");
+ ok(vlocal.videoHeight > 0, "source height is positive");
+ is(vremote.videoWidth, vlocal.videoWidth, "sink is same width as source");
+ is(vremote.videoHeight, vlocal.videoHeight, "sink is same height as source");
+ },
+ function PC_REMOTE_SET_RTP_SECOND_RID(test) {
+ // Now, cause pcRemote to filter out everything but the second SSRC.
+ // This lets only the other simulcast stream through.
+ selectRecvSsrc(test.pcRemote, 1);
+ },
+ function PC_REMOTE_WAIT_FOR_SECOND_MEDIA_FLOW(test) {
+ return test.pcRemote.waitForMediaFlow();
+ },
+ function PC_REMOTE_WAIT_FOR_FRAMES_2() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return helper.waitForFrames(vremote);
+ },
+ // 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.
+ function PC_REMOTE_WAIT_FOR_FRAMES_3() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return helper.waitForFrames(vremote);
+ },
+ function PC_REMOTE_CHECK_SIZE_2() {
+ var vlocal = test.pcLocal.localMediaElements[0];
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vlocal, "Should have local video element for pcLocal");
+ ok(vremote, "Should have remote video element for pcRemote");
+ ok(vlocal.videoWidth > 0, "source width is positive");
+ ok(vlocal.videoHeight > 0, "source height is positive");
+ is(vremote.videoWidth, vlocal.videoWidth / 2, "sink is 1/2 width of source");
+ is(vremote.videoHeight, vlocal.videoHeight / 2, "sink is 1/2 height of source");
+ },
+ function PC_REMOTE_SET_RTP_NONEXISTENT_RID(test) {
+ // Now, cause pcRemote to filter out everything, just to make sure
+ // selectRecvSsrc is working.
+ selectRecvSsrc(test.pcRemote, 2);
+ },
+ function PC_REMOTE_ENSURE_NO_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return helper.verifyNoFrames(vremote);
+ },
+ ]);
+
+ return test.run();
+ })
+ .catch(e => ok(false, "unexpected failure: " + e)));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_syncSetDescription.html b/dom/media/tests/mochitest/test_peerConnection_syncSetDescription.html
new file mode 100644
index 000000000..bc998d641
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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/tests/mochitest/test_peerConnection_throwInCallbacks.html b/dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html
new file mode 100644
index 000000000..ad5cb7d86
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.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;version=1.8">
+ 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.indexOf("Expected") == -1) {
+ 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.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/tests/mochitest/test_peerConnection_toJSON.html b/dom/media/tests/mochitest/test_peerConnection_toJSON.html
new file mode 100644
index 000000000..52a619c47
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_trackDisabling.html b/dom/media/tests/mochitest/test_peerConnection_trackDisabling.html
new file mode 100644
index 000000000..7bd8f5f1b
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_trackDisabling.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;version=1.8">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely"
+});
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+
+ // 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).
+ test.setMediaConstraints([{audio: true, video: true, fake: 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");
+ },
+ 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, offsetX, offsetY,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)));
+ var checkVideoDisabled = video =>
+ h.waitForPixel(video, offsetX, offsetY,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold, offsetX*2, offsetY*2)));
+ return Promise.resolve()
+ .then(() => info("Checking local video enabled"))
+ .then(() => checkVideoEnabled(localVideo))
+ .then(() => info("Checking remote video enabled"))
+ .then(() => checkVideoEnabled(remoteVideo))
+
+ .then(() => info("Disabling original"))
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false)
+
+ .then(() => info("Checking local video disabled"))
+ .then(() => checkVideoDisabled(localVideo))
+ .then(() => info("Checking remote video disabled"))
+ .then(() => checkVideoDisabled(remoteVideo))
+ },
+ 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 freq1k = localAnalyser.binIndexForFrequency(1000);
+ var checkAudioEnabled = analyser =>
+ checkAudio(analyser, array => array[freq1k] > 200);
+ var checkAudioDisabled = analyser =>
+ checkAudio(analyser, array => array[freq1k] < 50);
+
+ return Promise.resolve()
+ .then(() => info("Checking local audio enabled"))
+ .then(() => checkAudioEnabled(localAnalyser))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => checkAudioEnabled(remoteAnalyser))
+
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false)
+
+ .then(() => info("Checking local audio disabled"))
+ .then(() => checkAudioDisabled(localAnalyser))
+ .then(() => info("Checking remote audio disabled"))
+ .then(() => checkAudioDisabled(remoteAnalyser))
+ }
+ ]);
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_trackDisabling_clones.html b/dom/media/tests/mochitest/test_peerConnection_trackDisabling_clones.html
new file mode 100644
index 000000000..98548b215
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_trackDisabling_clones.html
@@ -0,0 +1,149 @@
+<!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;version=1.8">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely, individually for clones"
+});
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+
+ var originalStream;
+ var localVideoOriginal;
+
+ // Always use fake tracks since we depend on audio to have a large 1000Hz
+ // component.
+ test.setMediaConstraints([{audio: true, video: true, fake: 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");
+ },
+ 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, offsetX, offsetY,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)));
+ var checkVideoDisabled = video =>
+ h.waitForPixel(video, offsetX, offsetY,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold)));
+
+ return Promise.resolve()
+ .then(() => info("Checking local original enabled"))
+ .then(() => checkVideoEnabled(localVideoOriginal))
+ .then(() => info("Checking local clone enabled"))
+ .then(() => checkVideoEnabled(localVideoClone))
+ .then(() => info("Checking remote clone enabled"))
+ .then(() => checkVideoEnabled(remoteVideoClone))
+
+ .then(() => info("Disabling original"))
+ .then(() => originalStream.getVideoTracks()[0].enabled = false)
+
+ .then(() => info("Checking local original disabled"))
+ .then(() => checkVideoDisabled(localVideoOriginal))
+ .then(() => info("Checking local clone enabled"))
+ .then(() => checkVideoEnabled(localVideoClone))
+ .then(() => info("Checking remote clone enabled"))
+ .then(() => checkVideoEnabled(remoteVideoClone))
+
+ .then(() => info("Re-enabling original; disabling clone"))
+ .then(() => originalStream.getVideoTracks()[0].enabled = true)
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false)
+
+ .then(() => info("Checking local original enabled"))
+ .then(() => checkVideoEnabled(localVideoOriginal))
+ .then(() => info("Checking local clone disabled"))
+ .then(() => checkVideoDisabled(localVideoClone))
+ .then(() => info("Checking remote clone disabled"))
+ .then(() => checkVideoDisabled(remoteVideoClone))
+ },
+ 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 freq1k = localAnalyserOriginal.binIndexForFrequency(1000);
+ var checkAudioEnabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq1k] > 200);
+ var checkAudioDisabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq1k] < 50);
+
+ return Promise.resolve()
+ .then(() => info("Checking local original enabled"))
+ .then(() => checkAudioEnabled(localAnalyserOriginal))
+ .then(() => info("Checking local clone enabled"))
+ .then(() => checkAudioEnabled(localAnalyserClone))
+ .then(() => info("Checking remote clone enabled"))
+ .then(() => checkAudioEnabled(remoteAnalyserClone))
+
+ .then(() => info("Disabling original"))
+ .then(() => originalStream.getAudioTracks()[0].enabled = false)
+
+ .then(() => info("Checking local original disabled"))
+ .then(() => checkAudioDisabled(localAnalyserOriginal))
+ .then(() => info("Checking local clone enabled"))
+ .then(() => checkAudioEnabled(localAnalyserClone))
+ .then(() => info("Checking remote clone enabled"))
+ .then(() => checkAudioEnabled(remoteAnalyserClone))
+
+ .then(() => info("Re-enabling original; disabling clone"))
+ .then(() => originalStream.getAudioTracks()[0].enabled = true)
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false)
+
+ .then(() => info("Checking local original enabled"))
+ .then(() => checkAudioEnabled(localAnalyserOriginal))
+ .then(() => info("Checking local clone disabled"))
+ .then(() => checkAudioDisabled(localAnalyserClone))
+ .then(() => info("Checking remote clone disabled"))
+ .then(() => checkAudioDisabled(remoteAnalyserClone))
+ }
+ ]);
+ test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_twoAudioStreams.html b/dom/media/tests/mochitest/test_peerConnection_twoAudioStreams.html
new file mode 100644
index 000000000..a88623163
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html b/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
new file mode 100644
index 000000000..2ad0569cf
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.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: "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");
+ },
+ function PC_REMOTE_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+ Object.keys(
+ test.pcRemote.expectedRemoteTrackInfoById).forEach(trackId => {
+ test.pcRemote.expectedRemoteTrackInfoById[trackId].streamId = "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");
+ },
+ function PC_LOCAL_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+ Object.keys(
+ test.pcLocal.expectedRemoteTrackInfoById).forEach(trackId => {
+ test.pcLocal.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+ });
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}, {audio: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreams.html b/dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreams.html
new file mode 100644
index 000000000..6264e7cc8
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_twoAudioVideoStreamsCombined.html b/dom/media/tests/mochitest/test_peerConnection_twoAudioVideoStreamsCombined.html
new file mode 100644
index 000000000..61e0313b4
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_twoVideoStreams.html b/dom/media/tests/mochitest/test_peerConnection_twoVideoStreams.html
new file mode 100644
index 000000000..102c2f11c
--- /dev/null
+++ b/dom/media/tests/mochitest/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/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html b/dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
new file mode 100644
index 000000000..c60c37838
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.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: "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");
+ },
+ function PC_REMOTE_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+ Object.keys(
+ test.pcRemote.expectedRemoteTrackInfoById).forEach(trackId => {
+ test.pcRemote.expectedRemoteTrackInfoById[trackId].streamId = "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");
+ },
+ function PC_LOCAL_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+ Object.keys(
+ test.pcLocal.expectedRemoteTrackInfoById).forEach(trackId => {
+ test.pcLocal.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+ });
+ }
+ ]);
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}, {video: true}]);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html b/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
new file mode 100644
index 000000000..b5fab06b7
--- /dev/null
+++ b/dom/media/tests/mochitest/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.getAllUserMedia([{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/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html b/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
new file mode 100644
index 000000000..4aeb5e7ed
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -0,0 +1,100 @@
+<!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(() => {
+ 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);
+ }
+ ]);
+
+ 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.waitForPixelColor(vremote1, h1.green, 128,
+ "pcRemote's remote should become green");
+ },
+ function 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.
+ stream1.requestFrame();
+ h1.drawColor(canvas1, h1.red);
+ },
+ function WAIT_FOR_REMOTE_RED() {
+ return h1.waitForPixelColor(vremote1, h1.red, 128,
+ "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.getAllUserMedia([{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.waitForPixelColor(vremote2, h2.blue, 128,
+ "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.waitForPixelColor(vremote1, h1.green, 128,
+ "pcRemote's remote1 should become green");
+ }
+ ]);
+
+ test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html b/dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html
new file mode 100644
index 000000000..fb77c332f
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_videoRenegotiationInactiveAnswer.html
@@ -0,0 +1,73 @@
+<!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(function (options) {
+ var helper;
+
+ test = new PeerConnectionTest(options);
+
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ helper = new VideoStreamHelper();
+ test.pcLocal.attachLocalStream(helper.stream());
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return helper.waitForFrames(vremote);
+ }
+ ]);
+
+ 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");
+ return helper.verifyNoFrames(vremote);
+ },
+ ]);
+
+ 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");
+ return helper.waitForFrames(vremote);
+ }
+ ]);
+
+ test.setMediaConstraints([{video: true}], []);
+ test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/tests/mochitest/test_peerConnection_webAudio.html b/dom/media/tests/mochitest/test_peerConnection_webAudio.html
new file mode 100644
index 000000000..ffe7ed49d
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+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/tests/mochitest/test_selftest.html b/dom/media/tests/mochitest/test_selftest.html
new file mode 100644
index 000000000..e755939a2
--- /dev/null
+++ b/dom/media/tests/mochitest/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;version=1.8">
+ 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/tests/mochitest/turnConfig.js b/dom/media/tests/mochitest/turnConfig.js
new file mode 100644
index 000000000..49e6b1c89
--- /dev/null
+++ b/dom/media/tests/mochitest/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 = { };