diff options
Diffstat (limited to 'dom/media/tests')
209 files changed, 15066 insertions, 0 deletions
diff --git a/dom/media/tests/crashtests/780790.html b/dom/media/tests/crashtests/780790.html new file mode 100644 index 000000000..27eb2a961 --- /dev/null +++ b/dom/media/tests/crashtests/780790.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=780790 +--> +<head> + <meta charset="utf-8"> + <title>Simple gUM test - null success callback</title> + <script type="application/javascript"> + navigator.mozGetUserMedia({video: true, fake: true}, null, null); + </script> +</head> + +<body> +</body> +</html> diff --git a/dom/media/tests/crashtests/791270.html b/dom/media/tests/crashtests/791270.html new file mode 100644 index 000000000..627552fb8 --- /dev/null +++ b/dom/media/tests/crashtests/791270.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=791270 +--> +<head> + <meta charset="utf-8"> + <title>Simple PeerConnection Video Test</title> + <script type="application/javascript"> + var pc = new RTCPeerConnection(); + pc.addStream(undefined); + </script> +</head> + +<body> +</body> +</html> diff --git a/dom/media/tests/crashtests/791278.html b/dom/media/tests/crashtests/791278.html new file mode 100644 index 000000000..fd538f678 --- /dev/null +++ b/dom/media/tests/crashtests/791278.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=791278 +--> +<head> + <meta charset="utf-8"> + <title>Simple PeerConnection Video Test - Invalid callback</title> + <script type="application/javascript"> + var pc1 = new RTCPeerConnection(); + pc1.setLocalDescription(function() {}); + + var pc2 = new RTCPeerConnection(); + pc2.setRemoteDescription(function() {}); + </script> +</head> + +<body> +</body> +</html> diff --git a/dom/media/tests/crashtests/791330.html b/dom/media/tests/crashtests/791330.html new file mode 100644 index 000000000..e1e1f24a0 --- /dev/null +++ b/dom/media/tests/crashtests/791330.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=791330 +--> +<head> + <meta charset="utf-8"> + <title>PeerConnection test - operate on closed connection</title> + <script type="application/javascript"> + function finish() { + document.documentElement.removeAttribute("class"); + } + + function runTest() { + var pc = new RTCPeerConnection(); + pc.close(); + + navigator.mozGetUserMedia({audio: true, fake: true}, function (stream) { + try { + pc.addStream(stream); + pc.createOffer(function (offer) {}); + } + finally { + stream.stop(); + + finish(); + } + }, function () {}); + } + </script> +</head> + +<body onload="setTimeout(runTest, 100)"> +</body> +</html> diff --git a/dom/media/tests/crashtests/799419.html b/dom/media/tests/crashtests/799419.html new file mode 100644 index 000000000..7f815e73c --- /dev/null +++ b/dom/media/tests/crashtests/799419.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=799419 +--> +<head> + <meta charset="utf-8"> + <title>2 Peer Connections Create and Close + Fake Video</title> + <script type="application/javascript"> + function finish() { + document.documentElement.removeAttribute("class"); + } + + function boom() { + var v0 = new RTCPeerConnection(); + var v1 = new RTCPeerConnection(); + var v2 = document.getElementById("pc1video"); + var v3 = document.getElementById("pc2video"); + navigator.mozGetUserMedia({video:true, fake: true}, + function(stream) {}, function() {}); + v0.close(); + v1.close(); + + finish(); + } + </script> +</head> +<body onload="setTimeout(boom, 100)"> +<video id="pc1video" width="100" height="100" controls></video> +<video id="pc2video" width="100" height="100" controls></video> +</body> +</html> diff --git a/dom/media/tests/crashtests/801227.html b/dom/media/tests/crashtests/801227.html new file mode 100644 index 000000000..2dded5451 --- /dev/null +++ b/dom/media/tests/crashtests/801227.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=801227 +--> +<head> + <meta charset="utf-8"> + <title>Abort due to page reload</title> + <script type="application/javascript"> + var pc = new RTCPeerConnection(); + + var index = localStorage.index || 0; + if (index < 3) { + localStorage.index = index + 1; + window.location.reload(); + } + + function finish() { + delete localStorage["index"]; + + pc.close(); + document.documentElement.removeAttribute("class"); + } + + navigator.mozGetUserMedia({ audio: true, fake: true }, function (aStream) { + pc.addStream(aStream); + finish(); + }, finish); + </script> +</head> + +<body> +</body> +</html> + diff --git a/dom/media/tests/crashtests/802982.html b/dom/media/tests/crashtests/802982.html new file mode 100644 index 000000000..032f3085c --- /dev/null +++ b/dom/media/tests/crashtests/802982.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=802982 +--> +<head> + <meta charset="utf-8"> + <title>Excessive getUserMedia calls</title> + <script type="application/javascript"> + + function finish() { + document.documentElement.removeAttribute("class"); + } + + function boom() + { + for (var j = 0; j < 100; ++j) { + navigator.mozGetUserMedia({}, function(){}, function(){}); + } + finish(); // we're not waiting for success/error callbacks here + } + + </script> +</head> + +<body onload="setTimeout(boom, 100)"> +</html> diff --git a/dom/media/tests/crashtests/812785.html b/dom/media/tests/crashtests/812785.html new file mode 100644 index 000000000..f26168e49 --- /dev/null +++ b/dom/media/tests/crashtests/812785.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=812785 +--> +<head> + <meta charset="utf-8"> + <title>Bug 812785 - WebRTC use-after-free crash</title> + <script type="application/javascript"> + var pc1, pc2, pc1_offer, pc2_answer, localAudio, remoteAudio; + + function onFailure(code) { + stop(); + } + + function stop() { + pc1.close(); pc1 = null; + pc2.close(); pc2 = null; + + var index = localStorage.index || 0; + if (index < 5) { + localStorage.index = index + 1; + window.location.reload(); + } + else { + finish(); + } + } + + function start() { + localAudio = document.getElementById("local"); + remoteAudio = document.getElementById("remote"); + + var stream = localAudio.mozCaptureStreamUntilEnded(); + + pc1 = new RTCPeerConnection(); + pc2 = new RTCPeerConnection(); + + pc1.addStream(stream); + pc1.createOffer(function (offer) { + pc1_offer = offer; + pc1.setLocalDescription(pc1_offer, function () { + pc2.setRemoteDescription(pc1_offer, function () { + pc2.createAnswer(function (answer) { + pc2_answer = answer; + pc2.setLocalDescription(pc2_answer, function () { + pc1.setRemoteDescription(pc2_answer, function step6() { + stop(); + }, onFailure); + }, onFailure); + }, onFailure); + }, onFailure); + }, onFailure); + }, onFailure); + } + + function finish() { + delete localStorage["index"]; + + document.documentElement.removeAttribute("class"); + } + </script> +</head> + +<body onload="setTimeout(start, 100)"> + <audio id="local" controls autoplay><source type="audio/wav" src="" /></audio> + <audio id="remote" controls></audio> +</body> +</html> + diff --git a/dom/media/tests/crashtests/822197.html b/dom/media/tests/crashtests/822197.html new file mode 100644 index 000000000..97abc4422 --- /dev/null +++ b/dom/media/tests/crashtests/822197.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=822197 +--> +<head> + <meta charset="utf-8"> + <title>Bug 822197 - Many PeerConnections with CreateOffer</title> + <script type="application/javascript"> + var pcArray = []; + + function finish() { + document.documentElement.removeAttribute("class"); + } + + function start() { + for(var i = 0; i < 70; i++) { + var pc = new RTCPeerConnection(); + pc.createOffer(function() {}, function() {}); + pcArray.push(pc); + } + finish(); + } + </script> +</head> + +<body onload="setTimeout(start, 100)"> +</html> diff --git a/dom/media/tests/crashtests/834100.html b/dom/media/tests/crashtests/834100.html new file mode 100644 index 000000000..004258156 --- /dev/null +++ b/dom/media/tests/crashtests/834100.html @@ -0,0 +1,25 @@ +<html class="reftest-wait"> + <head> + <script language="javascript"> + +function start() { + remotePC = new RTCPeerConnection(); + var cand = new RTCIceCandidate( + {candidate: "1 1 UDP 1 127.0.0.1 34567 type host", + sdpMid: "helloworld", + sdbMid: "helloworld", // Mis-spelt attribute for bug 833948 compatibility. + sdpMLineIndex: 1 + }); + try {remotePC.addIceCandidate(cand);} catch(e) {} // bug 842075 - remove try when fixed + try {remotePC.addIceCandidate(cand, function(sdp){}, finish);} catch(e) {} // bug 842075 - remove try when fixed + finish(); +} + +function finish(arg) { + document.documentElement.removeAttribute("class"); +} + </script> + </head> + <body onload="setTimeout(start, 100)"> + </body> +</html> diff --git a/dom/media/tests/crashtests/836349.html b/dom/media/tests/crashtests/836349.html new file mode 100644 index 000000000..651fde46a --- /dev/null +++ b/dom/media/tests/crashtests/836349.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=836349 +--> +<head> + <meta charset="utf-8"> + <title>Bug 836349</title> + <script type="application/javascript"> + function crash() + { + var pc = new RTCPeerConnection(); + var ld = pc.localDescription; + dump(ld.sdp); + } + </script> +</head> + +<body onload="crash();"> +</html> diff --git a/dom/media/tests/crashtests/837324.html b/dom/media/tests/crashtests/837324.html new file mode 100644 index 000000000..63162a336 --- /dev/null +++ b/dom/media/tests/crashtests/837324.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=837324 +--> +<head> + <meta charset="utf-8"> + <title>Bug 837324</title> + <script type="application/javascript"> + function finish() { + document.documentElement.removeAttribute("class"); + } + + function start() { + var o0 = new RTCPeerConnection(); + var o1 = new RTCIceCandidate({"candidate":"0 -65535 IP 0 stun.sipgate.net 3227326073 type ::ffff:192.0.2.128 host 2302600701","sdpMid":"video 3907077665 RTP/SAVPF 5000","sdpMLineIndex":7}); + try {o0.addIceCandidate(o1);} catch(e) {} // bug 842075 - remove try when fixed + + finish(); + } + </script> +</head> + +<body onload="setTimeout(start, 100)"> +</html> diff --git a/dom/media/tests/crashtests/855796.html b/dom/media/tests/crashtests/855796.html new file mode 100644 index 000000000..da46276c5 --- /dev/null +++ b/dom/media/tests/crashtests/855796.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=855796 +--> +<head> + <meta charset="utf-8"> + <title>Bug 855796</title> + <script type="application/javascript"> + var pc1, pc2, pc1_offer, pc2_answer; + + function onFailure(code) { + stop(); + } + + function stop() { + pc1.close(); + pc1 = null; + pc2.close(); + pc2 = null; + document.documentElement.removeAttribute("class"); + } + + function step1(offer) { + pc1_offer = offer; + pc1_offer.sdp = "v=0\r\no=Mozilla-SIPUA 2208 0 IN IP4 0.0.0.0\r\ns=SIP Call\r\nt=0 0\r\na=ice-pwd:4450d5a4a5f097855c16fa079893be18\r\na=fingerprint:sha-256 23:9A:2E:43:94:42:CF:46:68:FC:62:F9:F4:48:61:DB:2F:8C:C9:FF:6B:25:54:9D:41:09:EF:83:A8:19:FC:B6\r\nm=audio 56187 RTP/SAVPF 109 0 8 101\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:109 opus/48000/2\r\na=ptime:20\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\na=sendrecv\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 56187 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 56187 typ srflx raddr 192.168.178.20 rport 56187\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 52955 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 52955 typ srflx raddr 192.168.178.20 rport 52955\r\nm=video 49929 RTP/SAVPF 120\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:120 VP8/90000\r\na=recvonly\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 49929 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 49929 typ srflx raddr 192.168.178.20 rport 49929\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 50769 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 50769 typ srflx raddr 192.168.178.20 rport 50769\r\nm=application 54054 SCTP/DTLS 5000 \r\nc=IN IP4 77.9.79.167\r\na=fmtp:HuRUu]Dtcl\\zM,7(OmEU%O$gU]x/z\tD protocol=webrtc-datachannel;streams=16\r\na=sendrecv\r\n"; + pc1.setLocalDescription(pc1_offer, step2, onFailure); + } + + function step2() { + pc2.setRemoteDescription(pc1_offer, step3, onFailure); + } + + function step3() { + pc2.createAnswer(step4, onFailure); + } + + function step4(answer) { + pc2_answer = answer; + pc2.setLocalDescription(pc2_answer, step5, onFailure); + } + + function step5() { + pc1.setRemoteDescription(pc2_answer, step6, onFailure); + } + + function step6() { + stop(); + } + + function start() { + pc1 = new RTCPeerConnection(); + pc2 = new RTCPeerConnection(); + navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) { + pc1.addStream(s); + navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) { + pc2.addStream(s); + pc1.createOffer(step1, onFailure); + }, onFailure); + }, onFailure); + } + </script> +</head> + +<body onload="setTimeout(start, 100)"> +</html> diff --git a/dom/media/tests/crashtests/860143.html b/dom/media/tests/crashtests/860143.html new file mode 100644 index 000000000..2d17a8dac --- /dev/null +++ b/dom/media/tests/crashtests/860143.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=860143 +--> +<head> + <meta charset="utf-8"> + <title>bug 860143</title> + <script type="application/javascript"> + function start() { + var o0 = new window.RTCPeerConnection({ + iceServers: [ + { + url: "turn:AAAAAAAAAAAAAAAAA AAAAAAAAAAA AAAAAAAAAAAAA AA AAAAA AAAAAAAAAAA AAAAAAAAAAAAAAAA AAAAA AAAAAAAAAA, AAAAAAAAAAAAAAAA AAA AAAAAAAAAAAAAAA" + } + ] + }); + o0.close(); + } + </script> +</head> + +<body onload="start()"> +</body> +</html> diff --git a/dom/media/tests/crashtests/861958.html b/dom/media/tests/crashtests/861958.html new file mode 100644 index 000000000..d6ea7e2e8 --- /dev/null +++ b/dom/media/tests/crashtests/861958.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=861958 +--> +<head> + <meta charset="utf-8"> + <title>bug 861958</title> + <script type="application/javascript"> + function start() { + var o0 = new window.RTCPeerConnection(); + var o1 = o0.createDataChannel("foo", { + "protocol": "text/char", + "preset": false, + "stream": 512 + }); + o0.close(); + } + </script> +</head> + +<body onload="start()"> +</body> +</html> diff --git a/dom/media/tests/crashtests/863929.html b/dom/media/tests/crashtests/863929.html new file mode 100644 index 000000000..dc534e264 --- /dev/null +++ b/dom/media/tests/crashtests/863929.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=863929 +--> +<head> + <meta charset="utf-8"> + <title>Bug 863929</title> + <script type="application/javascript"> + var pc1, pc2, pc1_offer, pc2_answer; + + function onFailure(code) { + stop(); + } + + function stop() { + pc1.close(); + pc1 = null; + pc2.close(); + pc2 = null; + document.documentElement.removeAttribute("class"); + } + + function step1(offer) { + pc1_offer = offer; + pc1_offer.sdp = 'v=0\r\no=Mozilla-SIPUA 2208 0 IN IP4 0.0.0.0\r\ns=SIP Call\r\nt=0 0\r\na=ice-ufrag:96e36277\r\na=ice-pwd:4450d5a4a5f097855c16fa079893be18\r\na=fingerprint:sha-256 23:9A:2E:43:94:42:CF:46:68:FC:62:F9:F4:48:61:DB:2F:8C:C9:FF:6B:25:54:9D:41:09:EF:83:A8:19:FC:B6\r\nm=audio 56187 RTP/SAVPF 109 0 8 101\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:109 opus/48000/2\r\na=ptime:20\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\na=sendrecv\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 56187 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 56187 typ srflx raddr 192.168.178.20 rport 56187\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 52955 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 52955 typ srflx raddr 192.168.178.20 rport 52955\r\nm=video 49929 RTP/SAVPF 120\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:120 telephone-event/90000\r\na=recvonly\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 49929 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 49929 typ srflx raddr 192.168.178.20 rport 49929\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 50769 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 50769 typ srflx raddr 192.168.178.20 rport 50769\r\nm=application 54054 SCTP/DTLS 5000 \r\nc=IN IP4 77.9.79.167\r\na=fmtp:5000 protocol=webrtc-datachannel;streams=16\r\na=sendrecv\r\n'; + pc1.setLocalDescription(pc1_offer, step2, onFailure); + } + + function step2() { + pc2.setRemoteDescription(pc1_offer, step3, onFailure); + } + + function step3() { + pc2.createAnswer(step4, onFailure); + } + + function step4(answer) { + pc2_answer = answer; + pc2.setLocalDescription(pc2_answer, step5, onFailure); + } + + function step5() { + pc1.setRemoteDescription(pc2_answer, step6, onFailure); + } + + function step6() { + stop(); + } + + function start() { + pc1 = new RTCPeerConnection(); + pc2 = new RTCPeerConnection(); + navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) { + pc1.addStream(s); + navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) { + pc2.addStream(s); + pc1.createOffer(step1, onFailure); + }, onFailure); + }, onFailure); + } + </script> +</head> + +<body onload="start()"> +</html> diff --git a/dom/media/tests/crashtests/crashtests.list b/dom/media/tests/crashtests/crashtests.list new file mode 100644 index 000000000..2acec6650 --- /dev/null +++ b/dom/media/tests/crashtests/crashtests.list @@ -0,0 +1,16 @@ +default-preferences pref(media.peerconnection.enabled,true) pref(media.navigator.permission.disabled,true) + +load 780790.html +load 791270.html +load 791278.html +load 791330.html +load 799419.html +load 802982.html +load 812785.html +load 834100.html +load 836349.html +load 837324.html +load 855796.html +load 860143.html +load 861958.html +load 863929.html 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 = { }; |