summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/protocol
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webrtc/protocol')
-rw-r--r--testing/web-platform/tests/webrtc/protocol/README.txt22
-rw-r--r--testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html49
-rw-r--r--testing/web-platform/tests/webrtc/protocol/bundle.https.html150
-rw-r--r--testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html218
-rw-r--r--testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html85
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-certificates.html42
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html37
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html135
-rw-r--r--testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html115
-rw-r--r--testing/web-platform/tests/webrtc/protocol/handover-datachannel.html61
-rw-r--r--testing/web-platform/tests/webrtc/protocol/handover.html72
-rw-r--r--testing/web-platform/tests/webrtc/protocol/ice-state.https.html130
-rw-r--r--testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html55
-rw-r--r--testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html41
-rw-r--r--testing/web-platform/tests/webrtc/protocol/missing-fields.html47
-rw-r--r--testing/web-platform/tests/webrtc/protocol/msid-generate.html160
-rw-r--r--testing/web-platform/tests/webrtc/protocol/msid-parse.html83
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html40
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html109
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html78
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html101
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html61
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html153
-rw-r--r--testing/web-platform/tests/webrtc/protocol/sctp-format.html25
-rw-r--r--testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html55
-rw-r--r--testing/web-platform/tests/webrtc/protocol/simulcast-answer.html101
-rw-r--r--testing/web-platform/tests/webrtc/protocol/simulcast-offer.html33
-rw-r--r--testing/web-platform/tests/webrtc/protocol/split.https.html98
-rw-r--r--testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html34
-rw-r--r--testing/web-platform/tests/webrtc/protocol/video-codecs.https.html95
-rw-r--r--testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html44
31 files changed, 2529 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/protocol/README.txt b/testing/web-platform/tests/webrtc/protocol/README.txt
new file mode 100644
index 0000000000..5e17fbf9c3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/README.txt
@@ -0,0 +1,22 @@
+This directory contains files that test for behavior relevant to webrtc,
+but which is specified in protocol specifications from the IETF, not in
+API recommendations from the W3C.
+
+The main specifications are given in the following RFCs:
+
+- RFC 7742, "WebRTC Video Processing and Codec Requirements"
+- RFC 7874, "WebRTC Audio Codec and Processing Requirements"
+- RFC 8825 (draft-ietf-rtcweb-overview)
+- RFC 8826 (draft-ietf-rtcweb-security)
+- RFC 8827 (draft-ietf-rtcweb-security-arch)
+- RFC 8828 (draft-ietf-rtcweb-ip-handling)
+- RFC 8829 (draft-ietf-rtcweb-jsep)
+- RFC 8831 (draft-ietf-rtcweb-data-channel)
+- RFC 8832 (draft-ietf-rtcweb-data-protocol)
+- RFC 8834 (draft-ietf-rtcweb-rtp-usage)
+- RFC 8835 (draft-ietf-rtcweb-transports)
+- RFC 8851 (draft-ietf-mmusic-rid)
+- RFC 8853 (draft-ietf-mmusic-sdp-simulcast)
+- RFC 8854 (draft-ietf-rtcweb-fec)
+
+This list is incomplete.
diff --git a/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html b/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html
new file mode 100644
index 0000000000..066fc2e085
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection RTP payload types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+
+// Test that when creating an offer we do not run out of valid payload types.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ pc1.addTransceiver('video', { direction: 'recvonly' });
+ const offer = await pc1.createOffer();
+
+ // Extract all payload types from the m= lines.
+ const payloadTypes = offer.sdp.split('\n')
+ .map(line => line.trim())
+ .filter(line => line.startsWith('m='))
+ .map(line => line.split(' ').slice(3).join(' '))
+ .join(' ')
+ .split(' ')
+ .map(payloadType => parseInt(payloadType, 10));
+
+ // The list of allowed payload types is taken from here
+ // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1.
+ const forbiddenPayloadTypes = payloadTypes
+ .filter(payloadType => {
+ if (payloadType >= 96 && payloadType <= 127) {
+ return false;
+ }
+ if (payloadType >= 72 && payloadType < 96) {
+ return true;
+ }
+ if (payloadType >= 35 && payloadType < 72) {
+ return false;
+ }
+ // TODO: Check against static payload type list.
+ return false;
+ });
+ assert_equals(forbiddenPayloadTypes.length, 0)
+}, 'createOffer with the maximum set of codecs does not generate invalid payload types');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/bundle.https.html b/testing/web-platform/tests/webrtc/protocol/bundle.https.html
new file mode 100644
index 0000000000..3d2b835baf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/bundle.https.html
@@ -0,0 +1,150 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection BUNDLE</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+
+
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ // remove the a=group:BUNDLE from the SDP when signaling.
+ const sdp = offer.sdp.replace(/a=group:BUNDLE (.*)\r\n/, '');
+ const ontrack = new Promise(r => callee.ontrack = r);
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ const {streams: [recvStream]} = await ontrack;
+ assert_equals(recvStream.getTracks().length, 2, "Tracks should be added to the stream before sRD resolves.");
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = recvStream;
+ v.id = recvStream.id;
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const senders = caller.getSenders();
+ const dtlsTransports = senders.map(s => s.transport);
+ assert_equals(dtlsTransports.length, 2);
+ assert_not_equals(dtlsTransports[0], dtlsTransports[1]);
+
+ const iceTransports = dtlsTransports.map(t => t.iceTransport);
+ assert_equals(iceTransports.length, 2);
+ assert_not_equals(iceTransports[0], iceTransports[1]);
+}, 'not negotiating BUNDLE creates two separate ice and dtls transports');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ const ontrack = new Promise(r => callee.ontrack = r);
+ await callee.setRemoteDescription(offer);
+ await caller.setLocalDescription(offer);
+ const secondTransport = caller.getSenders()[1].transport; // Save a reference to this transport.
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ const {streams: [recvStream]} = await ontrack;
+ assert_equals(recvStream.getTracks().length, 2, "Tracks should be added to the stream before sRD resolves.");
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = recvStream;
+ v.id = recvStream.id;
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const senders = caller.getSenders();
+ const dtlsTransports = senders.map(s => s.transport);
+ assert_equals(dtlsTransports.length, 2);
+ assert_equals(dtlsTransports[0], dtlsTransports[1]);
+ assert_not_equals(dtlsTransports[1], secondTransport);
+ assert_equals(secondTransport.state, 'closed');
+}, 'bundles on the first transport and closes the second');
+
+promise_test(async t => {
+ const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=setup:actpass
+`;
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+ await pc.setLocalDescription();
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 2);
+ assert_false(transceivers[0].stopped);
+ assert_true(transceivers[1].stopped);
+}, 'max-bundle with an offer without bundle only negotiates the first m-line');
+
+promise_test(async t => {
+ const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE audio video
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=bundle-only
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+`;
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+}, 'sRD(offer) works with no transport attributes in a bundle-only m-section');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html b/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html
new file mode 100644
index 0000000000..c54f26e6d8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Candidate exchange</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+
+class StateLogger {
+ constructor(source, eventname, field) {
+ source.addEventListener(eventname, event => {
+ this.events.push(source[field]);
+ });
+ this.events = [source[field]];
+ }
+}
+
+class IceStateLogger extends StateLogger {
+ constructor(source) {
+ super(source, 'iceconnectionstatechange', 'iceConnectionState');
+ }
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ // Note - it's been claimed that this state sometimes jumps straight
+ // to "completed". If so, this test should be flaky.
+ await waitForIceStateChange(pc1, ['connected']);
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Two way ICE exchange works');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = e => {
+ candidates.push(e.candidate);
+ }
+ // Candidates from PC2 are not delivered to pc1, so pc1 will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ const waiter = waitForIceGatheringState(pc1, ['complete']);
+ await waiter;
+ for (const candidate of candidates) {
+ if (candidate) {
+ pc2.addIceCandidate(candidate);
+ }
+ }
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding only caller -> callee candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = e => {
+ candidates.push(e.candidate);
+ }
+ // Candidates from pc1 are not delivered to pc2. so pc2 will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ const waiter = waitForIceGatheringState(pc2, ['complete']);
+ await waiter;
+ for (const candidate of candidates) {
+ if (candidate) {
+ pc1.addIceCandidate(candidate);
+ }
+ }
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding only callee -> caller candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let pc2ToPc1Candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = e => {
+ pc2ToPc1Candidates.push(e.candidate);
+ // This particular test verifies that candidates work
+ // properly if added from the pc2 onicecandidate event.
+ if (!e.candidate) {
+ for (const candidate of pc2ToPc1Candidates) {
+ if (candidate) {
+ pc1.addIceCandidate(candidate);
+ }
+ }
+ }
+ }
+ // Candidates from |pc1| are not delivered to |pc2|. |pc2| will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding callee -> caller candidates from end-of-candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let pc1ToPc2Candidates = [];
+ let pc2ToPc1Candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = e => {
+ pc1ToPc2Candidates.push(e.candidate);
+ }
+ pc2.onicecandidate = e => {
+ pc2ToPc1Candidates.push(e.candidate);
+ }
+ const offer = await pc1.createOffer();
+ await Promise.all([pc1.setLocalDescription(offer),
+ pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await waitForIceGatheringState(pc1, ['complete']);
+ await pc2.setLocalDescription(answer).then(() => {
+ for (const candidate of pc1ToPc2Candidates) {
+ if (candidate) {
+ pc2.addIceCandidate(candidate);
+ }
+ }
+ });
+ await waitForIceGatheringState(pc2, ['complete']);
+ pc1.setRemoteDescription(answer).then(async () => {
+ for (const candidate of pc2ToPc1Candidates) {
+ if (candidate) {
+ await pc1.addIceCandidate(candidate);
+ }
+ }
+ });
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair =
+ pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ // When we supply remote candidates, we expect a jump to the 'host' candidate,
+ // but it might also remain as 'prflx'.
+ assert_true(candidate_pair.remote.type == 'host' ||
+ candidate_pair.remote.type == 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Explicit offer/answer exchange gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = assert_unreached;
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await new Promise(resolve => {
+ pc1.onicecandidate = resolve;
+ });
+}, 'Candidates always arrive after setLocalDescription(offer) resolves');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = assert_unreached;
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await new Promise(resolve => {
+ pc2.onicecandidate = resolve;
+ });
+}, 'Candidates always arrive after setLocalDescription(answer) resolves');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html b/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html
new file mode 100644
index 0000000000..f13f221b88
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html
@@ -0,0 +1,85 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../RTCStats-helper.js"></script>
+<script>
+'use strict';
+
+// draft-ietf-rtcweb-security-20 section 6.5
+//
+// All Implementations MUST support DTLS 1.2 with the
+// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite and the P-256
+// curve [FIPS186].
+// ....... The DTLS-SRTP protection profile
+// SRTP_AES128_CM_HMAC_SHA1_80 MUST be supported for SRTP.
+// Implementations MUST favor cipher suites which support (Perfect
+// Forward Secrecy) PFS over non-PFS cipher suites and SHOULD favor AEAD
+// over non-AEAD cipher suites.
+
+const acceptableTlsVersions = new Set([
+ 'FEFD', // DTLS 1.2 - RFC 6437 section 4.1
+ '0304', // TLS 1.3 - RFC 8446 section 5.1
+]);
+
+const acceptableDtlsCiphersuites = new Set([
+ 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
+]);
+
+const acceptableSrtpCiphersuites = new Set([
+ 'SRTP_AES128_CM_HMAC_SHA1_80',
+ 'AES_CM_128_HMAC_SHA1_80',
+]);
+
+const acceptableTlsGroups = new Set([
+ 'P-256',
+]);
+
+const acceptableValues = {
+ 'tlsVersion': acceptableTlsVersions,
+ 'dtlsCipher': acceptableDtlsCiphersuites,
+ 'srtpCipher': acceptableSrtpCiphersuites,
+ 'tlsGroup': acceptableTlsGroups,
+};
+
+function verifyStat(name, transportStats) {
+ assert_not_equals(typeof transportStats, 'undefined');
+ assert_true(name in transportStats, 'Value present:');
+ assert_true(acceptableValues[name].has(transportStats[name]));
+}
+
+for (const name of Object.keys(acceptableValues)) {
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('foo');
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForState(pc1.sctp.transport, 'connected');
+ const statsReport = await pc1.getStats();
+ const transportStats = findStatsFromReport(statsReport,
+ stats => stats.type === 'transport')
+ verifyStat(name, transportStats);
+ }, name + ' is acceptable on data-only');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForState(transceiver.sender.transport, 'connected');
+ const statsReport = await pc1.getStats();
+ const transportStats = findStatsFromReport(statsReport,
+ stats => stats.type === 'transport')
+ verifyStat(name, transportStats);
+ }, name + ' is acceptable on video-only');
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html b/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html
new file mode 100644
index 0000000000..bc4794cbc1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection DTLS certifcate interop</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate
+const certificateParameters = {
+ ecdsa: {
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ },
+ rsa: {
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: 'SHA-256',
+ },
+};
+
+Object.keys(certificateParameters).forEach(async localType => {
+ Object.keys(certificateParameters).forEach(async remoteType => {
+ promise_test(async t => {
+ const localParameters = certificateParameters[localType];
+ const remoteParameters = certificateParameters[remoteType];
+ const firstCertificate = await RTCPeerConnection.generateCertificate(localParameters);
+ const secondCertificate = await RTCPeerConnection.generateCertificate(remoteParameters);
+ const pc1 = new RTCPeerConnection({certificates: [firstCertificate]});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({certificates: [secondCertificate]});
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForConnectionStateChange(pc1, ['connected']);
+ }, `RTCPeerConnection establishes using ${localType} and ${remoteType} certificates`);
+ });
+});
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html b/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html
new file mode 100644
index 0000000000..0ddc8488ae
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>DTLS fingerprint validation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+
+// Tests that an invalid fingerprint leads to a connectionState 'failed'.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ exchangeIceCandidates(pc1, pc2);
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(new RTCSessionDescription({
+ type: answer.type,
+ sdp: answer.sdp.replace(/a=fingerprint:sha-256 .*/g,
+ 'a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:' +
+ '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'),
+ }));
+ await pc2.setLocalDescription(answer);
+
+ await waitForConnectionStateChange(pc1, ['failed']);
+ await waitForConnectionStateChange(pc2, ['failed']);
+}, 'Connection fails if one side provides a wrong DTLS fingerprint');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html b/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html
new file mode 100644
index 0000000000..892e6db413
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection a=setup SDP parameter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+// Tests for correct behavior of DTLS a=setup parameter.
+
+// SDP copied from JSEP Example 7.1
+// It contains two media streams with different ufrags, and bundle
+// turned on.
+const kSdp = `v=0
+o=- 4962303333179871722 1 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=ice-options:trickle
+a=group:BUNDLE a1 v1
+a=group:LS a1 v1
+m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
+c=IN IP4 203.0.113.100
+a=mid:a1
+a=sendrecv
+a=rtpmap:96 opus/48000/2
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:97 telephone-event/8000
+a=rtpmap:98 telephone-event/48000
+a=maxptime:120
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10101 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+m=video 10102 UDP/TLS/RTP/SAVPF 100 101
+c=IN IP4 203.0.113.100
+a=mid:v1
+a=sendrecv
+a=rtpmap:100 VP8/90000
+a=rtpmap:101 rtx/90000
+a=fmtp:101 apt=100
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
+a=ice-ufrag:BGKk
+a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10103 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+`;
+
+for (let setup of ['actpass', 'active', 'passive']) {
+ promise_test(async t => {
+ const sdp = kSdp.replace(/a=setup:actpass/g,
+ 'a=setup:' + setup);
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: sdp});
+ const answer = await pc1.createAnswer();
+ const resultingSetup = answer.sdp.match(/a=setup:\S+/);
+ if (setup === 'active') {
+ assert_equals(resultingSetup[0], 'a=setup:passive');
+ } else if (setup === 'passive') {
+ assert_equals(resultingSetup[0], 'a=setup:active');
+ } else if (setup === 'actpass') {
+ // For actpass, either active or passive are legal, although
+ // active is RECOMMENDED by RFC 5763 / 8842.
+ assert_in_array(resultingSetup[0], ['a=setup:active', 'a=setup:passive']);
+ }
+ await pc1.setLocalDescription(answer);
+ }, 'PC should accept initial offer with setup=' + setup);
+}
+
+for (let setup of ['actpass', 'active', 'passive']) {
+ const roleMap = {
+ actpass: 'client',
+ active: 'server',
+ passive: 'client',
+ };
+ promise_test(async t => {
+ const sdp = kSdp.replace(/a=setup:actpass/g,
+ 'a=setup:' + setup);
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: sdp});
+ const answer = await pc1.createAnswer();
+ const resultingSetup = answer.sdp.match(/a=setup:\S+/);
+ if (setup === 'active') {
+ assert_equals(resultingSetup[0], 'a=setup:passive');
+ } else if (setup === 'passive') {
+ assert_equals(resultingSetup[0], 'a=setup:active');
+ } else if (setup === 'actpass') {
+ // For actpass, either active or passive are legal, although
+ // active is RECOMMENDED by RFC 5763 / 8842.
+ assert_in_array(resultingSetup[0], ['a=setup:active', 'a=setup:passive']);
+ }
+ await pc1.setLocalDescription(answer);
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport' && report.dtlsRole) {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.dtlsRole, roleMap[setup]);
+ }, 'PC with setup=' + setup + ' should have a dtlsRole of ' + roleMap[setup]);
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel("wpt");
+ await pc1.setLocalDescription();
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport' && report.dtlsRole) {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.dtlsRole, 'unknown');
+}, 'dtlsRole is `unknown` before negotiation of the DTLS handshake');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html b/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html
new file mode 100644
index 0000000000..cb0b581c30
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html
@@ -0,0 +1,115 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection H.264 profile levels</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function mungeLevel(sdp, level) {
+ level_hex = Math.round(level * 10).toString(16);
+ return {
+ type: sdp.type,
+ sdp: sdp.sdp.replace(/(profile-level-id=....)(..)/g,
+ "$1" + level_hex)
+ }
+}
+
+// Numbers taken from
+// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
+let levelTable = {
+ 1: {mbs: 1485, fs: 99},
+ 1.1: {mbs: 3000, fs: 396},
+ 1.2: {mbs: 6000, fs: 396},
+ 1.3: {mbs: 11880, fs: 396},
+ 2: {mbs: 11880, fs: 396},
+ 2.1: {mbs: 19800, fs: 792},
+ 2.2: {mbs: 20250, fs: 1620},
+ 3: {mbs: 40500, fs: 1620},
+ 3.1: {mbs: 108000, fs: 3600},
+ 3.2: {mbs: 216000, fs: 5120},
+ 4: {mbs: 245760, fs: 8192},
+ 4.1: {mbs: 245760, fs: 8192},
+ 4.2: {mbs: 522240, fs: 8704},
+ 5: {mbs: 589824, fs: 22800},
+ 5.1: {mbs: 983040, fs: 36864},
+ 5.2: {mbs: 2073600, fs: 36864},
+ 6: {mbs: 4177920, fs: 139264},
+ 6.1: {mbs: 8355840, fs: 139264},
+ 6.2: {mbs: 16711680, fs: 139264},
+};
+
+function sizeFitsLevel(width, height, fps, level) {
+ const frameSizeMacroblocks = width * height / 256;
+ const macroblocksPerSecond = frameSizeMacroblocks * fps;
+ assert_less_than_equal(frameSizeMacroblocks,
+ levelTable[level].fs, 'frame size');
+ assert_less_than_equal(macroblocksPerSecond,
+ levelTable[level].mbs, 'macroblocks/second');
+}
+
+// Constant for now, may be variable later.
+const framesPerSecond = 30;
+
+for (let level of Object.keys(levelTable)) {
+ promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/H264'), 'H264 not supported');
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const v = document.createElement('video');
+
+ // Generate the largest video we can get from the attached device.
+ // This means platform inconsistency.
+ // The fake video in Chrome WPT tests is 3840x2160.
+ const stream = await navigator.mediaDevices.getUserMedia(
+ {video: {width: 12800, height: 7200, frameRate: framesPerSecond}});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
+ streams: [stream],
+ });
+ preferCodec(transceiver, 'video/H264');
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = new Promise(r => pc2.ontrack = r);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer),
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(mungeLevel(answer, level));
+
+ v.srcObject = new MediaStream([(await trackEvent).track]);
+ let metadataLoaded = new Promise((resolve) => {
+ v.autoplay = true;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ });
+ await metadataLoaded;
+ // Ensure that H.264 is in fact used.
+ const statsReport = await transceiver.sender.getStats();
+ for (const stats of statsReport.values()) {
+ if (stats.type === 'outbound-rtp') {
+ const activeCodec = stats.codecId;
+ const codecStats = statsReport.get(activeCodec);
+ assert_implements_optional(codecStats.mimeType ==='video/H264',
+ 'Level ' + level + ' H264 video is not supported');
+ }
+ }
+ // TODO(hta): This will not catch situations where the initial size is
+ // within the permitted bounds, but resolution or framerate changes to
+ // outside the permitted bounds after a while. Should be addressed.
+ sizeFitsLevel(v.videoWidth, v.videoHeight, framesPerSecond, level);
+ }, 'Level ' + level + ' H264 video is appropriately constrained');
+
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html b/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html
new file mode 100644
index 0000000000..8f224f822a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Handovers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ const offerDatachannel1 = offerPc.createDataChannel('initial');
+ exchangeIceCandidates(offerPc, answerPcFirst);
+
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ const datachannelAtAnswerPcFirst = await new Promise(
+ r => answerPcFirst.ondatachannel = ({channel}) => r(channel));
+ const iceTransport = offerPc.sctp.transport;
+ // Check that messages get through.
+ datachannelAtAnswerPcFirst.send('hello');
+ const message1 = await awaitMessage(offerDatachannel1);
+ assert_equals(message1, 'hello');
+
+ // Renegotiate with PC 2
+ // Note - ICE candidates will also be sent to answerPc1, but that shouldn't matter.
+ exchangeIceCandidates(offerPc, answerPcSecond);
+ const offer2 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer2);
+ await answerPcSecond.setRemoteDescription(offer2);
+ const answer2 = await answerPcSecond.createAnswer();
+ await offerPc.setRemoteDescription(answer2);
+ await answerPcSecond.setLocalDescription(answer2);
+
+ // Kill the first PC. This should not affect anything, but leaving it may cause untoward events.
+ answerPcFirst.close();
+
+ const answerDataChannel2 = answerPcSecond.createDataChannel('second back');
+
+ const datachannelAtOfferPcSecond = await new Promise(r => offerPc.ondatachannel = ({channel}) => r(channel));
+
+ await new Promise(r => datachannelAtOfferPcSecond.onopen = r);
+
+ datachannelAtOfferPcSecond.send('hello again');
+ const message2 = await awaitMessage(answerDataChannel2);
+ assert_equals(message2, 'hello again');
+}, 'Handover with datachannel reinitiated from new callee completes');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/handover.html b/testing/web-platform/tests/webrtc/protocol/handover.html
new file mode 100644
index 0000000000..748cbeff8d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/handover.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Handovers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ offerPc.addTransceiver('audio');
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ // Renegotiate with PC 2
+ const offer2 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer2);
+ await answerPcSecond.setRemoteDescription(offer2);
+ const answer2 = await answerPcSecond.createAnswer();
+ await offerPc.setRemoteDescription(answer2);
+ await answerPcSecond.setLocalDescription(answer2);
+}, 'Negotiation of handover initiated at caller works');
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ offerPc.addTransceiver('audio');
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ // Renegotiate with PC 2
+ // The offer from PC 2 needs to be consistent on at least the following:
+ // - Number, type and order of media sections
+ // - MID values
+ // - Payload type values
+ // Do a "fake" offer/answer using the original offer against PC2 to achieve this.
+ await answerPcSecond.setRemoteDescription(offer1);
+ // Discard the output of this round.
+ await answerPcSecond.setLocalDescription(await answerPcSecond.createAnswer());
+
+ // Now we can initiate an offer from the new PC.
+ const offer2 = await answerPcSecond.createOffer();
+ await answerPcSecond.setLocalDescription(offer2);
+ await offerPc.setRemoteDescription(offer2);
+ const answer2 = await offerPc.createAnswer();
+ await answerPcSecond.setRemoteDescription(answer2);
+ await offerPc.setLocalDescription(answer2);
+}, 'Negotiation of handover initiated at callee works');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/ice-state.https.html b/testing/web-platform/tests/webrtc/protocol/ice-state.https.html
new file mode 100644
index 0000000000..becce59509
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/ice-state.https.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Tests for correct behavior of ICE state.
+
+// SDP copied from JSEP Example 7.1
+// It contains two media streams with different ufrags, and bundle
+// turned on.
+const kSdp = `v=0
+o=- 4962303333179871722 1 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=ice-options:trickle
+a=group:BUNDLE a1 v1
+a=group:LS a1 v1
+m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
+c=IN IP4 203.0.113.100
+a=mid:a1
+a=sendrecv
+a=rtpmap:96 opus/48000/2
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:97 telephone-event/8000
+a=rtpmap:98 telephone-event/48000
+a=maxptime:120
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10101 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+m=video 10102 UDP/TLS/RTP/SAVPF 100 101
+c=IN IP4 203.0.113.100
+a=mid:v1
+a=sendrecv
+a=rtpmap:100 VP8/90000
+a=rtpmap:101 rtx/90000
+a=fmtp:101 apt=100
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
+a=ice-ufrag:BGKk
+a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10103 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+`;
+
+// Returns a promise that resolves when |pc.iceConnectionState| is in one of the
+// wanted states, and rejects if it is in one of the unwanted states.
+// This is a variant of the function in RTCPeerConnection-helper.js.
+function waitForIceStateChange(pc, wantedStates, unwantedStates=[]) {
+ return new Promise((resolve, reject) => {
+ if (wantedStates.includes(pc.iceConnectionState)) {
+ resolve();
+ return;
+ } else if (unwantedStates.includes(pc.iceConnectionState)) {
+ reject('Unexpected state encountered: ' + pc.iceConnectionState);
+ return;
+ }
+ pc.addEventListener('iceconnectionstatechange', () => {
+ if (wantedStates.includes(pc.iceConnectionState)) {
+ resolve();
+ } else if (unwantedStates.includes(pc.iceConnectionState)) {
+ reject('Unexpected state encountered: ' + pc.iceConnectionState);
+ }
+ });
+ });
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForIceStateChange(pc1, ['connected', 'completed']);
+}, 'PC should enter connected (or completed) state when candidates are sent');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ const offer = await pc1.createOffer();
+ assert_greater_than_equal(offer.sdp.search('a=ice-options:trickle'), 0);
+}, 'PC should generate offer with a=ice-options:trickle');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: kSdp});
+ const answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+ assert_greater_than_equal(answer.sdp.search('a=ice-options:trickle'), 0);
+ // When we use trickle ICE, and don't signal end-of-caniddates, we
+ // expect failure to result in 'disconnected' state rather than 'failed'.
+ const stateWaiter = waitForIceStateChange(pc1, ['disconnected'],
+ ['failed']);
+ // Add a bogus candidate. The candidate is drawn from the
+ // IANA "test-net-3" pool (RFC5737), so is guaranteed not to respond.
+ const candidateStr1 =
+ 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
+ await pc1.addIceCandidate({candidate: candidateStr1,
+ sdpMid: 'a1',
+ usernameFragment: 'ETEn'});
+ await stateWaiter;
+}, 'PC should enter disconnected state when a failing candidate is sent');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html b/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html
new file mode 100644
index 0000000000..bd151284cb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Tests for validating ice-ufrag and ice-pwd syntax defined in
+// https://tools.ietf.org/html/rfc5245#section-15.4
+// Alphanumeric, '+' and '/' are allowed.
+
+const preamble = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+m=video 1 RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/30
+a=setup:actpass
+`;
+const valid_ufrag = 'a=ice-ufrag:ETEn\r\n';
+const valid_pwd = 'a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l\r\n';
+const not_ice_char = '$'; // A snowman emoji would be cool but is not interoperable.
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const sdp = preamble +
+ valid_ufrag.replace('ETEn', 'E' + not_ice_char + 'En') +
+ valid_pwd;
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp}));
+}, 'setRemoteDescription with a ice-ufrag containing a non-ice-char fails');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const sdp = preamble +
+ valid_ufrag +
+ valid_pwd.replace('K0Wp', 'K' + not_ice_char + 'Wp');
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp}));
+}, 'setRemoteDescription with a ice-pwd containing a non-ice-char fails');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html b/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html
new file mode 100644
index 0000000000..50527f88df
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Tests for the construction of initial offers according to
+ // draft-ietf-rtcweb-jsep-24 section 5.2.1
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ let offer_lines = offer.sdp.split('\r\n');
+ // The first 3 lines are dictated by JSEP.
+ assert_equals(offer_lines[0], "v=0");
+ assert_equals(offer_lines[1].slice(0, 2), "o=");
+
+ assert_regexp_match(offer_lines[1], /^o=\S+ \d+ \d+ IN IP4 \S+$/);
+ const fields = RegExp(/^o=\S+ (\d+) (\d+) IN IP4 (\S+)/).exec(offer_lines[1]);
+ // Per RFC 3264, the sess-id should be representable in an uint64
+ // Note: JSEP -24 has this wrong - see bug:
+ // https://github.com/rtcweb-wg/jsep/issues/855
+ assert_less_than(Number(fields[1]), 2**64);
+ // Per RFC 3264, the version should be less than 2^62 to avoid overflow
+ assert_less_than(Number(fields[2]), 2**62);
+ // JSEP says that the address part SHOULD be a meaningless address
+ // "such as" IN IP4 0.0.0.0. This is to prevent unintentional disclosure
+ // of IP addresses, so this is important enough to verify. Right now we
+ // allow 127.0.0.1 and 0.0.0.0, but there are other things we could allow.
+ // Maybe 0.0.0.0/8, 127.0.0.0/8, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24?
+ // (See RFC 3330, RFC 5737)
+ assert_true(fields[3] == "0.0.0.0" || fields[3] == "127.0.0.1",
+ fields[3] + " must be a meaningless IPV4 address")
+
+ assert_regexp_match(offer_lines[2], /^s=\S+$/);
+ // After this, the order is not dictated by JSEP.
+ // TODO: Check lines subsequent to the s= line.
+ }, 'Offer conforms to basic SDP requirements');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/missing-fields.html b/testing/web-platform/tests/webrtc/protocol/missing-fields.html
new file mode 100644
index 0000000000..d5aafd230e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/missing-fields.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP parse tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+function removeSdpLines(description, toRemove) {
+ const edited = description.sdp.split('\n').filter(function(line) {
+ return (!line.startsWith(toRemove));
+ }).join('\n');
+ return {type: description.type, sdp: edited};
+}
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.addTrack(trackFactories.audio());
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ let remote_offer = removeSdpLines(offer, 'a=mid:');
+ remote_offer = removeSdpLines(remote_offer, 'a=group:');
+ await callee.setRemoteDescription(remote_offer);
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+}, 'Offer description with no mid is accepted');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.addTrack(trackFactories.audio());
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ let remote_answer = removeSdpLines(answer, 'a=mid:');
+ remote_answer = removeSdpLines(remote_answer, 'a=group:');
+ await caller.setRemoteDescription(remote_answer);
+}, 'Answer description with no mid is accepted');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/msid-generate.html b/testing/web-platform/tests/webrtc/protocol/msid-generate.html
new file mode 100644
index 0000000000..29226c704e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/msid-generate.html
@@ -0,0 +1,160 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection MSID generation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+
+function msidLines(desc) {
+ const sections = SDPUtils.splitSections(desc.sdp);
+ return SDPUtils.matchPrefix(sections[1], 'a=msid:');
+}
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dc = pc.createDataChannel('foo');
+ const desc = await pc.createOffer();
+ assert_equals(msidLines(desc).length, 0);
+}, 'No media track produces no MSID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0]);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], /^a=msid:-/);
+}, 'AddTrack without a stream produces MSID with no stream ID');
+
+// token-char from RFC 4566
+// This is printable characters except whitespace, and ["(),/:;<=>?@[\]]
+const token_char = '\\x21\\x23-\\x27\\x2A-\\x2B\\x2D-\\x2E\\x30-\\x39\\x41-\\x5A\\x5E-\\x7E';
+
+// msid-value from RFC 8830
+const msid_attr = RegExp(`^a=msid:[${token_char}]{1,64}( [${token_char}]{1,64})?$`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0], stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTrack with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ pc.addTrack(stream1.getTracks()[0], stream1, stream2);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'AddTrack with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0], stream1, stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTrack with the stream twice produces single MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0]);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], /^a=msid:-/);
+}, 'AddTransceiver without a stream produces MSID with no stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1]});
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTransceiver with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1, stream2]});
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'AddTransceiver with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1, stream1]});
+const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTransceiver with the stream twice produces single MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'SetStreams with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1, stream2);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'SetStreams with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1, stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'SetStreams with the stream twice produces single MSID with a stream ID');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/msid-parse.html b/testing/web-platform/tests/webrtc/protocol/msid-parse.html
new file mode 100644
index 0000000000..5596446e00
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/msid-parse.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection MSID parsing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+const preamble = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+m=video 1 RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/30
+a=setup:actpass
+`;
+
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer', sdp: preamble});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1, 'Stream count');
+}, 'Description with no msid produces a track with a stream');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:- foobar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 0);
+}, 'Description with msid:- appid produces a track with no stream');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo bar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1);
+ assert_equals(trackevent.streams[0].id, 'foo');
+}, 'Description with msid:foo bar produces a stream with id foo');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo bar\n'
+ + 'a=msid:baz bar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 2);
+}, 'Description with two msid produces two streams');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1);
+ assert_equals(trackevent.streams[0].id, 'foo');
+}, 'Description with msid foo but no track id is accepted');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html b/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html
new file mode 100644
index 0000000000..4177420050
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- This file contains a test that waits for two seconds. -->
+<meta name="timeout" content="long">
+<title>RTP clockrate</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+async function initiateSingleTrackCallAndReturnReceiver(t, kind) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({[kind]:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ pc1.addTrack(track, stream);
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+ await waitForConnectionStateChange(pc2, ['connected']);
+ return trackEvent.receiver;
+}
+
+promise_test(async t => {
+ // the getSynchronizationSources API exposes the rtp timestamp.
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, 'video');
+ const first = await listenForSSRCs(t, receiver);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ const second = await listenForSSRCs(t, receiver);
+ // rtpTimestamp may wrap at 0xffffffff, take care of that.
+ const actualClockRate = ((second[0].rtpTimestamp - first[0].rtpTimestamp + 0xffffffff) % 0xffffffff) / (second[0].timestamp - first[0].timestamp) * 1000;
+ assert_approx_equals(actualClockRate, 90000, 9000, 'Video clockrate is approximately 90000');
+}, 'video rtp timestamps increase by approximately 90000 per second');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html b/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html
new file mode 100644
index 0000000000..de08b2197f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection payload type demuxing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+ stream.getTracks().forEach(track => caller.addTrack(track.clone(), stream.clone()));
+
+ let callCount = 0;
+ let metadataToBeLoaded = new Promise(resolve => {
+ callee.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ if (++callCount === 2) {
+ resolve();
+ }
+ });
+ };
+ });
+
+ // Restrict first transceiver to VP8, second to H264.
+ const {codecs} = RTCRtpSender.getCapabilities('video');
+ const vp8 = codecs.find(c => c.mimeType === 'video/VP8');
+ const h264 = codecs.find(c => c.mimeType === 'video/H264');
+ caller.getTransceivers()[0].setCodecPreferences([vp8]);
+ caller.getTransceivers()[1].setCodecPreferences([h264]);
+
+ const offer = await caller.createOffer();
+ // Replace the mid header extension and all ssrc lines
+ // with bogus. The receiver will be forced to do payload type demuxing.
+ const sdp = offer.sdp
+ .replace(/rtp-hdrext:sdes/g, 'rtp-hdrext:something')
+ .replace(/a=ssrc:/g, 'a=notssrc');
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ await metadataToBeLoaded;
+}, 'Can demux two video tracks with different payload types on a bundled connection');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({bundlePolicy: 'max-compat'});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+ stream.getTracks().forEach(track => caller.addTrack(track.clone(), stream.clone()));
+
+ let callCount = 0;
+ let metadataToBeLoaded = new Promise(resolve => {
+ callee.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ if (++callCount === 2) {
+ resolve();
+ }
+ });
+ };
+ });
+
+ const offer = await caller.createOffer();
+ // Replace BUNDLE, the mid header extension and all ssrc lines
+ // with bogus. The receiver will be forced to do payload type demuxing
+ // which is still possible because the different m-lines arrive on
+ // different ports/sockets.
+ const sdp = offer.sdp.replace('BUNDLE', 'SOMETHING')
+ .replace(/rtp-hdrext:sdes/g, 'rtp-hdrext:something')
+ .replace(/a=ssrc:/g, 'a=notssrc');
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ await metadataToBeLoaded;
+}, 'Can demux two video tracks with the same payload type on an unbundled connection');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html b/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html
new file mode 100644
index 0000000000..045701c171
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection RTP extensions</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+async function setup() {
+ const pc1 = new RTCPeerConnection();
+ pc1.addTransceiver('audio');
+ // Make sure there is more than one rid, since there's no reason to use
+ // rtp-stream-id/repaired-rtp-stream-id otherwise. Some implementations
+ // may use them for unicast anyway, which isn't a spec violation, just
+ // a little silly.
+ pc1.addTransceiver('video', {sendEncodings: [{rid: '0'}, {rid: '1'}]});
+ const offer = await pc1.createOffer();
+ pc1.close();
+ return offer.sdp;
+}
+
+// Extensions that MUST be supported
+const mandatoryExtensions = [
+ // Directly referenced in WebRTC RTP usage
+ 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', // RFC 8834 5.2.2
+ 'urn:ietf:params:rtp-hdrext:sdes:mid', // RFC 8834 5.2.4
+ 'urn:3gpp:video-orientation', // RFC 8834 5.2.5
+ // Required for support of simulcast with RID
+ 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', // RFC 8852 4.3
+ 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', // RFC 8852 4.4
+];
+
+// For further testing:
+// - Add test for rapid synchronization - RFC 8834 5.2.1
+// - Add test for encrypted header extensions (RFC 6904)
+// - Separate tests for extensions in audio and video sections
+
+for (const extension of mandatoryExtensions) {
+ promise_test(async t => {
+ const sdp = await setup();
+ const extensions = SDPUtils.matchPrefix(sdp, 'a=extmap:')
+ .map(SDPUtils.parseExtmap);
+ assert_true(!!extensions.find(ext => ext.uri === extension));
+ }, `RTP header extension ${extension} is present in offer`);
+}
+
+// Test for illegal remote behavior: Reassignment of hdrext ID
+// in a subsequent offer/answer cycle.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ // Do a second offer/answer cycle.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+
+ // Swap the extension number of the two required extensions
+ answer.sdp = answer.sdp.replace('urn:ietf:params:rtp-hdrext:ssrc-audio-level',
+ 'xyzzy')
+ .replace('urn:ietf:params:rtp-hdrext:sdes:mid',
+ 'urn:ietf:params:rtp-hdrext:ssrc-audio-level')
+ .replace('xyzzy',
+ 'urn:ietf:params:rtp-hdrext:sdes:mid');
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc1.setRemoteDescription(answer));
+}, 'RTP header extension reassignment causes failure');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html b/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html
new file mode 100644
index 0000000000..c377a613f6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>payload type handling (assuming rtcp-mux)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+// Tests behaviour from https://www.rfc-editor.org/rfc/rfc8834.html#name-header-extensions
+
+function createOfferSdp(extmaps) {
+ let sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+`;
+ sdp += 'a=group:BUNDLE ' + ['audio', 'video'].filter(kind => extmaps[kind]).join(' ') + '\r\n';
+ if (extmaps.audio) {
+ sdp += `m=audio 9 RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+` + extmaps.audio.map(ext => SDPUtils.writeExtmap(ext));
+ }
+ if (extmaps.video) {
+ sdp += `m=video 9 RTP/SAVPF 112
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:112 VP8/90000
+a=setup:actpass
+` + extmaps.video.map(ext => SDPUtils.writeExtmap(ext));
+ }
+ return sdp;
+}
+
+[
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.4
+ {
+ audio: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}],
+ video: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}],
+ description: 'MID',
+ },
+ {
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.2
+ audio: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level'}],
+ description: 'Audio level',
+ },
+ {
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.5
+ video: [{id: 1, uri: 'urn:3gpp:video-orientation'}],
+ description: 'Video orientation',
+ }
+].forEach(testcase => {
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(testcase)});
+ const answer = await pc.createAnswer();
+ const sections = SDPUtils.splitSections(answer.sdp);
+ sections.shift();
+ sections.forEach(section => {
+ const rtpParameters = SDPUtils.parseRtpParameters(section);
+ assert_equals(rtpParameters.headerExtensions.length, 1);
+ assert_equals(rtpParameters.headerExtensions[0].id, testcase[SDPUtils.getKind(section)][0].id);
+ assert_equals(rtpParameters.headerExtensions[0].uri, testcase[SDPUtils.getKind(section)][0].uri);
+ });
+ }, testcase.description + ' header extension is supported.');
+});
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('video');
+ const offer = await pc.createOffer();
+ const section = SDPUtils.splitSections(offer.sdp)[1];
+ const extensions = SDPUtils.matchPrefix(section, 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ const extension_not_mid = extensions.find(e => e.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid');
+ await pc.setRemoteDescription({type :'offer', sdp: offer.sdp.replace(extension_not_mid.uri, 'bogus')});
+
+ await pc.setLocalDescription();
+ const answer_section = SDPUtils.splitSections(pc.localDescription.sdp)[1];
+ const answer_extensions = SDPUtils.matchPrefix(answer_section, 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ assert_equals(answer_extensions.length, extensions.length - 1);
+ assert_false(!!extensions.find(e => e.uri === 'bogus'));
+ for (const answer_extension of answer_extensions) {
+ assert_true(!!extensions.find(e => e.uri === answer_extension.uri));
+ }
+}, 'Negotiates the subset of supported extensions offered');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html b/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html
new file mode 100644
index 0000000000..af7656d131
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>payload type handling (assuming rtcp-mux)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+// Tests behaviour from https://tools.ietf.org/html/rfc5761#section-4
+
+function createOfferSdp(opusPayloadType) {
+ return `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+m=audio 9 RTP/SAVPF ${opusPayloadType}
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:${opusPayloadType} opus/48000/2
+a=setup:actpass
+`;
+}
+
+promise_test(async t => {
+ for (let payloadType = 96; payloadType <= 127; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)});
+ const answer = await pc.createAnswer();
+ assert_true(answer.sdp.includes(`a=rtpmap:${payloadType} opus/48000/2`));
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 96-127 works');
+
+// This is written as a separate test since it currently fails in Chrome.
+promise_test(async t => {
+ for (let payloadType = 35; payloadType <= 63; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)});
+ const answer = await pc.createAnswer();
+ assert_true(answer.sdp.includes(`a=rtpmap:${payloadType} opus/48000/2`));
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 35-63 works');
+
+promise_test(async t => {
+ for (let payloadType = 64; payloadType <= 95; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)}),
+ 'Failed to reject on PT ' + payloadType);
+
+
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 64-95 throws an InvalidAccessError');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html b/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html
new file mode 100644
index 0000000000..78519c75cc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html
@@ -0,0 +1,153 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTX codec integrity checks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+// Tests for conformance to rules for RTX codecs.
+// Basic rule: Offers and answers must contain RTX codecs, and the
+// RTX codecs must have an a=fmtp line that points to a non-RTX codec.
+
+// Helper function for doing one round of offer/answer exchange
+// between two local peer connections.
+// Calls setRemoteDescription(offer/answer) before
+// setLocalDescription(offer/answer) to ensure the remote description
+// is set and candidates can be added before the local peer connection
+// starts generating candidates and ICE checks.
+async function doSignalingHandshake(localPc, remotePc, options={}) {
+ let offer = await localPc.createOffer();
+ // Modify offer if callback has been provided
+ if (options.modifyOffer) {
+ offer = await options.modifyOffer(offer);
+ }
+
+ // Apply offer.
+ await remotePc.setRemoteDescription(offer);
+ await localPc.setLocalDescription(offer);
+
+ let answer = await remotePc.createAnswer();
+ // Modify answer if callback has been provided
+ if (options.modifyAnswer) {
+ answer = await options.modifyAnswer(answer);
+ }
+
+ // Apply answer.
+ await localPc.setRemoteDescription(answer);
+ await remotePc.setLocalDescription(answer);
+}
+
+function verifyRtxReferences(description) {
+ const mediaSection = SDPUtils.getMediaSections(description.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ for (const codec of rtpParameters.codecs) {
+ if (codec.name === 'rtx') {
+ assert_own_property(codec.parameters, 'apt', 'rtx codec has apt parameter');
+ const referenced_codec = rtpParameters.codecs.find(
+ c => c.payloadType === parseInt(codec.parameters.apt));
+ assert_true(referenced_codec !== undefined, `Found referenced codec`);
+ }
+ }
+}
+
+
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ verifyRtxReferences(offer);
+}, 'Initial offer should have sensible RTX mappings');
+
+async function negotiateAndReturnAnswer(t) {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ await doSignalingHandshake(pc1, pc2);
+ return pc2.localDescription;
+}
+
+promise_test(async t => {
+ const answer = await negotiateAndReturnAnswer(t);
+ verifyRtxReferences(answer);
+}, 'Self-negotiated answer should have sensible RTX parameters');
+
+promise_test(async t => {
+ const sampleOffer = `v=0
+o=- 1878890426675213188 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE video
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:RGPK
+a=ice-pwd:rAyHEAKC7ckxQgWaRZXukz+Z
+a=ice-options:trickle
+a=fingerprint:sha-256 8C:29:0A:8F:11:06:BF:1C:58:B3:CA:E6:F1:F1:DC:99:4C:6C:89:E9:FF:BC:D4:38:11:18:1F:40:19:C8:49:37
+a=setup:actpass
+a=mid:video
+a=recvonly
+a=rtcp-mux
+a=rtpmap:97 rtx/90000
+a=fmtp:97 apt=98
+a=rtpmap:98 VP8/90000
+a=rtcp-fb:98 ccm fir
+a=rtcp-fb:98 nack
+a=rtcp-fb:98 nack pli
+a=rtcp-fb:98 goog-remb
+a=rtcp-fb:98 transport-cc
+`;
+ const pc = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc.addTrack(track);
+ await pc.setRemoteDescription({type: 'offer', sdp: sampleOffer});
+ const answer = await pc.createAnswer();
+ verifyRtxReferences(answer);
+}, 'A remote offer generates sensible RTX references in answer');
+
+promise_test(async t => {
+ const sampleOffer = `v=0
+o=- 1878890426675213188 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE video
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:RGPK
+a=ice-pwd:rAyHEAKC7ckxQgWaRZXukz+Z
+a=ice-options:trickle
+a=fingerprint:sha-256 8C:29:0A:8F:11:06:BF:1C:58:B3:CA:E6:F1:F1:DC:99:4C:6C:89:E9:FF:BC:D4:38:11:18:1F:40:19:C8:49:37
+a=setup:actpass
+a=mid:video
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtpmap:97 rtx/90000
+a=fmtp:97 apt=98
+a=rtpmap:98 VP8/90000
+a=rtcp-fb:98 ccm fir
+a=rtcp-fb:98 nack
+a=rtcp-fb:98 nack pli
+a=rtcp-fb:98 goog-remb
+a=rtcp-fb:98 transport-cc
+a=rtpmap:99 rtx/90000
+a=fmtp:99 apt=96
+`;
+ const pc = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc.addTrack(track);
+ await pc.setRemoteDescription({type: 'offer', sdp: sampleOffer});
+ const answer = await pc.createAnswer();
+ verifyRtxReferences(answer);
+}, 'A remote offer with duplicate codecs generates sensible RTX references in answer');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/sctp-format.html b/testing/web-platform/tests/webrtc/protocol/sctp-format.html
new file mode 100644
index 0000000000..207e51d4c3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/sctp-format.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP SCTP format test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.createDataChannel('channel');
+ const offer = await caller.createOffer();
+ const [preamble, media_section, postamble] = offer.sdp.split('\r\nm=');
+ assert_true(typeof(postamble) === 'undefined');
+ assert_greater_than(media_section.search(
+ /^application \d+ UDP\/DTLS\/SCTP webrtc-datachannel\r\n/), -1);
+ assert_greater_than(media_section.search(/\r\na=sctp-port:\d+\r\n/), -1);
+ assert_greater_than(media_section.search(/\r\na=mid:/), -1);
+}, 'Generated Datachannel SDP uses correct SCTP offer syntax');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html b/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html
new file mode 100644
index 0000000000..e938c84c8b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection MUST NOT support SDES</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/webrtc/third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+// Test support for
+// https://www.rfc-editor.org/rfc/rfc8826#section-4.3.1
+
+const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=crypto:0 AES_CM_128_HMAC_SHA1_80 inline:2nra27hTUb9ilyn2rEkBEQN9WOFts26F/jvofasw
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+`;
+
+// Negative test for Chrome legacy behavior.
+promise_test(async t => {
+ const sdes_constraint = {'mandatory': {'DtlsSrtpKeyAgreement': false}};
+ const pc = new RTCPeerConnection(null, sdes_constraint);
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ const offer = await pc.createOffer();
+ assert_false(offer.sdp.includes('\na=crypto:'));
+}, 'does not create offers with SDES');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ try {
+ await pc.setRemoteDescription({type: 'offer', sdp});
+ assert_unreached("Must not accept SDP without fingerprint");
+ } catch (e) {
+ // TODO: which error is correct? See
+ // https://github.com/w3c/webrtc-pc/issues/2672
+ assert_true(['OperationError', 'InvalidAccessError'].includes(e.name));
+ }
+}, 'rejects a remote offer that only includes SDES and no DTLS fingerprint');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html b/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html
new file mode 100644
index 0000000000..5e19bc08ff
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Answer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const offer_sdp = `v=0
+o=- 3840232462471583827 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:Li6+
+a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
+a=ice-options:trickle
+a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
+a=setup:actpass
+a=mid:0
+a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtcp-fb:96 goog-remb
+a=rtcp-fb:96 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rid:foo recv
+a=rid:bar recv
+a=rid:baz recv
+a=simulcast:recv foo;bar;baz
+`;
+// Tests for the construction of answers with simulcast according to:
+// draft-ietf-mmusic-sdp-simulcast-13
+// draft-ietf-mmusic-rid-15
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp});
+ const transceiver = pc.getTransceivers()[0];
+ // The created transceiver should be in "recvonly" state. Allow it to send.
+ transceiver.direction = 'sendonly';
+ const answer = await pc.createAnswer();
+ const answer_lines = answer.sdp.split('\r\n');
+ // Check for a RID line for each layer.
+ for (const rid of expected_rids) {
+ const result = answer_lines.find(line => line.startsWith(`a=rid:${rid}`));
+ assert_not_equals(result, undefined, `RID attribute for '${rid}' missing.`);
+ }
+
+ // Check for simulcast attribute with send direction and all RIDs.
+ const result = answer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, 'Could not find simulcast attribute.');
+}, 'createAnswer() with multiple send encodings should create simulcast answer');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ // Try to disable the `bar` encoding in a=simulcast by prefixing it with the
+ // `~` character.
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp.replace(/(a=simulcast:.*)bar/, '$1~bar')});
+ const transceiver = pc.getTransceivers()[0];
+ transceiver.direction = 'sendonly';
+ await pc.setLocalDescription();
+
+ const parameters = pc.getSenders()[0].getParameters();
+ const barEncoding = parameters.encodings.find(encoding => encoding.rid === 'bar');
+ assert_not_equals(barEncoding, undefined);
+ assert_not_equals(barEncoding.active, false);
+}, 'Using the ~rid SDP syntax in a remote offer does not control the local encodings active flag');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp});
+ const transceiver = pc.getTransceivers()[0];
+ transceiver.direction = 'sendonly';
+ await pc.setLocalDescription();
+
+ // Disabling the encoding should not change the rid to ~rid.
+ const parameters = pc.getSenders()[0].getParameters();
+ parameters.encodings.forEach(e => e.active = false);
+ await pc.getSenders()[0].setParameters(parameters);
+ const offer = await pc.createOffer();
+
+ const offer_lines = offer.sdp.split('\r\n');
+ const result = offer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, 'Could not find simulcast attribute.');
+}, 'Disabling encodings locally does not change the SDP');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html b/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html
new file mode 100644
index 0000000000..77ae7f9510
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Offer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+// Tests for the construction of offers with simulcast according to:
+// draft-ietf-mmusic-sdp-simulcast-13
+// draft-ietf-mmusic-rid-15
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+ pc.addTransceiver('video', {
+ sendEncodings: expected_rids.map(rid => ({rid}))
+ });
+
+ const offer = await pc.createOffer();
+ let offer_lines = offer.sdp.split('\r\n');
+ // Check for a RID line for each layer.
+ for (const rid of expected_rids) {
+ let result = offer_lines.find(line => line.startsWith(`a=rid:${rid}`));
+ assert_not_equals(result, undefined, `RID attribute for '${rid}' missing.`);
+ }
+
+ // Check for simulcast attribute with send direction and all RIDs.
+ let result = offer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, "Could not find simulcast attribute.");
+}, 'createOffer() with multiple send encodings should create simulcast offer');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/split.https.html b/testing/web-platform/tests/webrtc/protocol/split.https.html
new file mode 100644
index 0000000000..3fc3bda2a5
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/split.https.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection BUNDLE</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/webrtc/third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const calleeAudio = new RTCPeerConnection();
+ t.add_cleanup(() => calleeAudio.close());
+ const calleeVideo = new RTCPeerConnection();
+ t.add_cleanup(() => calleeVideo.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+
+ let metadataToBeLoaded;
+ calleeVideo.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ metadataToBeLoaded = new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ });
+ };
+
+ caller.addEventListener('icecandidate', (e) => {
+ // route depending on sdpMlineIndex
+ if (e.candidate) {
+ const target = e.candidate.sdpMLineIndex === 0 ? calleeAudio : calleeVideo;
+ target.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate});
+ } else {
+ calleeAudio.addIceCandidate();
+ calleeVideo.addIceCandidate();
+ }
+ });
+ calleeAudio.addEventListener('icecandidate', (e) => {
+ if (e.candidate) {
+ caller.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate});
+ }
+ // Note: caller.addIceCandidate is only called for video to avoid calling it twice.
+ });
+ calleeVideo.addEventListener('icecandidate', (e) => {
+ if (e.candidate) {
+ caller.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate});
+ } else {
+ caller.addIceCandidate();
+ }
+ });
+
+ const offer = await caller.createOffer();
+ const sections = SDPUtils.splitSections(offer.sdp);
+ // Remove the a=group:BUNDLE from the SDP when signaling.
+ const bundle = SDPUtils.matchPrefix(sections[0], 'a=group:BUNDLE')[0];
+ sections[0] = sections[0].replace(bundle + '\r\n', '');
+
+ const audioSdp = sections[0] + sections[1];
+ const videoSdp = sections[0] + sections[2];
+
+ await calleeAudio.setRemoteDescription({type: 'offer', sdp: audioSdp});
+ await calleeVideo.setRemoteDescription({type: 'offer', sdp: videoSdp});
+ await caller.setLocalDescription(offer);
+
+ const answerAudio = await calleeAudio.createAnswer();
+ const answerVideo = await calleeVideo.createAnswer();
+ const audioSections = SDPUtils.splitSections(answerAudio.sdp);
+ const videoSections = SDPUtils.splitSections(answerVideo.sdp);
+
+ // Remove the fingerprint from the session part of the SDP if present
+ // and move it to the media section.
+ SDPUtils.matchPrefix(audioSections[0], 'a=fingerprint:').forEach(line => {
+ audioSections[0] = audioSections[0].replace(line + '\r\n', '');
+ audioSections[1] += line + '\r\n';
+ });
+ SDPUtils.matchPrefix(videoSections[0], 'a=fingerprint:').forEach(line => {
+ videoSections[0] = videoSections[0].replace(line + '\r\n', '');
+ videoSections[1] += line + '\r\n';
+ });
+
+ const sdp = audioSections[0] + audioSections[1] + videoSections[1];
+ await caller.setRemoteDescription({type: 'answer', sdp});
+ await calleeAudio.setLocalDescription(answerAudio);
+ await calleeVideo.setLocalDescription(answerVideo);
+
+ await metadataToBeLoaded;
+ assert_equals(calleeAudio.connectionState, 'connected');
+ assert_equals(calleeVideo.connectionState, 'connected');
+}, 'Connect audio and video to two independent PeerConnections');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html b/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html
new file mode 100644
index 0000000000..f5176d1c87
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP handling of unknown media types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ pc1.addTrack(stream.getTracks()[0], stream);
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription({
+ type: 'offer',
+ sdp: offer.sdp
+ .replace('m=audio ', 'm=unicorns ')
+ });
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ // Do not attempt to call pc1.setRemoteDescription.
+
+ const [preamble, media_section, postamble] = answer.sdp.split('\r\nm=');
+ assert_true(typeof(postamble) === 'undefined');
+ assert_greater_than(media_section.search(
+ /^unicorns 0/), -1);
+}, 'Unknown media types are rejected with the port set to 0');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html b/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html
new file mode 100644
index 0000000000..4ce0618bca
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html
@@ -0,0 +1,95 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+/*
+ * Chromium note: this requires build bots with H264 support. See
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=840659
+ * for details on how to enable support.
+ */
+// Tests for conformance to RFC 7742,
+// "WebRTC Video Processing and Codec Requirements"
+// The document was formerly known as draft-ietf-rtcweb-video-codecs.
+//
+// This tests that the browser is a WebRTC Browser as defined there.
+
+// TODO: Section 3.2: screen capture video MUST be prepared
+// to handle resolution changes.
+
+// TODO: Section 4: MUST support generating CVO (orientation)
+
+// Section 5: Browsers MUST implement VP8 and H.264 Constrained Baseline
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ let video_section_found = false;
+ for (let section of offer.sdp.split(/\r\nm=/)) {
+ if (section.search('video') != 0) {
+ continue;
+ }
+ video_section_found = true;
+ // RTPMAP lines have the format a=rtpmap:<pt> <codec>/<clock rate>
+ let rtpmap_regex = /\r\na=rtpmap:(\d+) (\S+)\/\d+\r\n/g;
+ let match = rtpmap_regex.exec(offer.sdp);
+ let payload_type_map = new Array();
+ while (match) {
+ payload_type_map[match[1]] = match[2];
+ match = rtpmap_regex.exec(offer.sdp);
+ }
+ assert_true(payload_type_map.indexOf('VP8') > -1,
+ 'VP8 is supported');
+ assert_true(payload_type_map.indexOf('H264') > -1,
+ 'H.264 is supported');
+ // TODO: Verify that one of the H.264 PTs supports constrained baseline
+ }
+ assert_true(video_section_found);
+}, 'H.264 and VP8 should be supported in initial offer');
+
+async function negotiateParameters() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ await exchangeOfferAnswer(pc1, pc2);
+ return sender.getParameters();
+}
+
+function parseFmtp(fmtp) {
+ const params = fmtp.split(';');
+ return params.map(param => param.split('='));
+}
+promise_test(async t => {
+ const params = await negotiateParameters();
+ assert_true(!!params.codecs.find(codec => codec.mimeType === 'video/H264'));
+ assert_true(!!params.codecs.find(codec => codec.mimeType === 'video/VP8'));
+}, 'H.264 and VP8 should be negotiated after handshake');
+
+// TODO: Section 6: Recipients MUST be able to decode 320x240@20 fps
+// TODO: Section 6.1: VP8 MUST support RFC 7741 payload formats
+// TODO: Section 6.1: VP8 MUST respect max-fr/max-fs
+// TODO: Section 6.1: VP8 MUST encode and decode square pixels
+// TODO: Section 6.2: H.264 MUST support RFC 6184 payload formats
+// TODO: Section 6.2: MUST support Constrained Baseline level 1.2
+// TODO: Section 6.2: SHOULD support Constrained High level 1.3
+// TODO: Section 6.2: MUST support packetization mode 1.
+promise_test(async t => {
+ const params = await negotiateParameters();
+ const h264 = params.codecs.filter(codec => codec.mimeType === 'video/H264');
+ h264.map(codec => {
+ const codec_params = parseFmtp(codec.sdpFmtpLine);
+ assert_true(!!codec_params.find(x => x[0] === 'profile-level-id'));
+ })
+}, 'All H.264 codecs MUST include profile-level-id');
+
+// TODO: Section 6.2: SHOULD interpret max-mbps, max-smbps, max-fs et al
+// TODO: Section 6.2: MUST NOT include sprop-parameter-sets
+// TODO: Section 6.2: MUST support SEI "filler payload"
+// TODO: Section 6.2: MUST support SEI "full frame freeze"
+// TODO: Section 6.2: MUST be prepared to receive User Data messages
+// TODO: Section 6.2: MUST encode and decode square pixels unless signaled
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html b/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html
new file mode 100644
index 0000000000..16ea635949
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test support for
+// https://tools.ietf.org/html/rfc7741#section-6.1
+
+const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=setup:actpass
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+`;
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription({type: 'offer', sdp});
+ await pc.setLocalDescription();
+ const receiver = pc.getReceivers()[0];
+ const parameters = receiver.getParameters();
+ const {sdpFmtpLine} = parameters.codecs[0];
+ assert_true(!!sdpFmtpLine);
+ assert_true(sdpFmtpLine.split(';').includes('max-fr=30'));
+ assert_true(sdpFmtpLine.split(';').includes('max-fs=3600'));
+}, 'setRemoteDescription parses max-fr and max-fs fmtp parameters');
+</script>