diff options
Diffstat (limited to 'testing/web-platform/tests/webrtc/protocol')
33 files changed, 2717 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/additional-codecs.html b/testing/web-platform/tests/webrtc/protocol/additional-codecs.html new file mode 100644 index 0000000000..5462d61479 --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/additional-codecs.html @@ -0,0 +1,56 @@ +<!doctype html> +<meta charset=utf-8> +<title>Send additional codec supported by the other side</title> +<meta name=timeout content=long> +<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://www.rfc-editor.org/rfc/rfc8829.html#section-5.3.1 +// in particular "but MAY include formats that are locally +// supported but not present in the offer" + +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + exchangeIceCandidates(pc1, pc2); + + const stream = await getNoiseStream({video: true}); + t.add_cleanup(() => stream.getTracks().forEach(t => t.stop())); + pc1.addTrack(stream.getTracks()[0], stream); + pc2.addTrack(stream.getTracks()[0], stream); + // Only offer VP8. + pc1.getTransceivers()[0].setCodecPreferences([{ + clockRate: 90000, + mimeType: 'video/VP8' + }]); + await pc1.setLocalDescription(); + + // Add H264 to the SDP. + const sdp = pc1.localDescription.sdp.split('\n') + .map(l => { + if (l.startsWith('m=')) { + return l.trim() + ' 63'; // 63 is the least-likely to be used PT. + } + return l.trim(); + }).join('\r\n') + + 'a=rtpmap:63 H264/90000\r\n' + + 'a=fmtp:63 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n' + await pc2.setRemoteDescription({ + type: 'offer', + sdp: sdp.replaceAll('VP8', 'no-such-codec'), // Remove VP8 + }); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + await listenToConnected(pc2); + const stats = await pc1.getStats(); + const rtp = [...stats.values()].find(({type}) => type === 'outbound-rtp'); + assert_true(!!rtp); + assert_equals(stats.get(rtp.codecId).mimeType, 'video/H264'); +}, 'Listing an additional codec in the answer causes it to be sent.'); +</script> 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..73ea477e04 --- /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 0 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..c3941e409f --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html @@ -0,0 +1,77 @@ +<!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'; + +// 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 acceptableValues = { + 'tlsVersion': acceptableTlsVersions, + 'dtlsCipher': acceptableDtlsCiphersuites, + 'srtpCipher': acceptableSrtpCiphersuites, +}; + +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 = [...statsReport.values()].find(({type}) => 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 = [...statsReport.values()].find(({type}) => 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..9d1739244d --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html @@ -0,0 +1,63 @@ +<!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> + +function makeZeroFingerprint(algorithm) { + const length = algorithm === 'sha-1' ? 160 : parseInt(algorithm.split('-')[1], 10); + let zeros = []; + for (let i = 0; i < length; i += 8) { + zeros.push('00'); + } + return 'a=fingerprint:' + algorithm + ' ' + zeros.join(':'); +} + +// Tests that an invalid fingerprint leads to a connectionState 'failed'. +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + pc1.createDataChannel('datachannel'); + exchangeIceCandidates(pc1, pc2); + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + await pc1.setRemoteDescription({ + type: answer.type, + sdp: answer.sdp.replace(/a=fingerprint:sha-256 .*/g, makeZeroFingerprint('sha-256')), + }); + await pc2.setLocalDescription(answer); + + await waitForConnectionStateChange(pc1, ['failed']); + await waitForConnectionStateChange(pc2, ['failed']); +}, 'Connection fails if one side provides a wrong DTLS fingerprint'); + +['sha-1', 'sha-256', 'sha-384', 'sha-512'].forEach(hashFunc => { + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + pc1.createDataChannel('datachannel'); + + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + await pc1.setRemoteDescription({ + type: answer.type, + sdp: answer.sdp.replace(/a=fingerprint:sha-256 .*/g, makeZeroFingerprint(hashFunc)), + }); + await pc2.setLocalDescription(answer); + }, 'SDP negotiation with a ' + hashFunc + ' fingerprint succeds'); +}); + +</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..d1d8bb62bf --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html @@ -0,0 +1,133 @@ +<!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)).join(''); + } + 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)).join(''); + } + 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'); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + // Some implementations may refuse 15 as invalid id because of + // https://www.rfc-editor.org/rfc/rfc8285#section-4.2 + // which only applies to one-byte extensions with ids 0-14. + const sdp = createOfferSdp({audio: [{ + id: 15, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + }]}); + await pc.setRemoteDescription({type: 'offer', sdp}); +}, 'Supports header extensions with id=15'); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const sdp = createOfferSdp({audio: [{ + id: 16, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + }, { + id: 17, uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + }]}); + await pc.setRemoteDescription({type: 'offer', sdp}); + 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, 2); + assert_true(!!answer_extensions.find(e => e.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid')); + assert_true(!!answer_extensions.find(e => e.uri === 'urn:ietf:params:rtp-hdrext:ssrc-audio-level')); +}, 'Supports two-byte header extensions'); +</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..dcf7ad1b54 --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html @@ -0,0 +1,49 @@ +<!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(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + return promise_rejects_dom(t, 'InvalidAccessError', + pc.setRemoteDescription({type: 'offer', sdp})); +}, '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..f3732ca44c --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html @@ -0,0 +1,102 @@ +<!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:1 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:2 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/transceiver-mline-recycling.html b/testing/web-platform/tests/webrtc/protocol/transceiver-mline-recycling.html new file mode 100644 index 0000000000..068c5acae3 --- /dev/null +++ b/testing/web-platform/tests/webrtc/protocol/transceiver-mline-recycling.html @@ -0,0 +1,87 @@ +<!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> +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + const negotiate = async () => { + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + }; + + // Add audio, negotiate, stop the transceiver, negotiate again, + // add another audio transceiver and negotiate. This should re-use the m-line. + pc1.addTransceiver('audio'); + await negotiate(); + pc1.getTransceivers()[0].stop(); + await negotiate(); + pc1.addTransceiver('audio'); + await negotiate(); + let numberOfMediaSections = SDPUtils.splitSections(pc1.localDescription.sdp).length - 1; + assert_equals(numberOfMediaSections, 1, 'Audio m-line gets reused for audio transceiver'); + + // Stop the audio transceiver, negotiate, add a video transceiver, negotiate. + // This should reuse the m-line. + pc1.getTransceivers()[0].stop(); + await negotiate(); + pc1.addTransceiver('video'); + await negotiate(); + numberOfMediaSections = SDPUtils.splitSections(pc1.localDescription.sdp).length - 1; + assert_equals(numberOfMediaSections, 1, 'Audio m-line gets reused for video transceiver'); + + // Add another video transceiver after stopping the current one. + // This should re-use the m-line. + pc1.getTransceivers()[0].stop(); + await negotiate(); + pc1.addTransceiver('video'); + await negotiate(); + numberOfMediaSections = SDPUtils.splitSections(pc1.localDescription.sdp).length - 1; + assert_equals(numberOfMediaSections, 1, 'Video m-line gets reused for video transceiver'); +}, 'Reuses m-lines in local negotiation'); + +promise_test(async t => { + // SDP with a rejected video m-line. + 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 0 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=setup:actpass +a=ice-ufrag:ETEn +a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l +`; + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + await pc1.setRemoteDescription({type: 'offer', sdp}); + await pc1.setLocalDescription(); + assert_equals(pc1.getTransceivers().length, 0); + pc1.addTransceiver('audio'); + let offer = await pc1.createOffer(); + let numberOfMediaSections = SDPUtils.splitSections(offer.sdp).length - 1; + assert_equals(numberOfMediaSections, 1, 'Remote video m-line gets reused for audio transceiver'); + + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + await pc2.setRemoteDescription({type: 'offer', sdp}); + await pc2.setLocalDescription(); + assert_equals(pc2.getTransceivers().length, 0); + pc1.addTransceiver('video'); + offer = await pc2.createOffer(); + numberOfMediaSections = SDPUtils.splitSections(offer.sdp).length - 1; + assert_equals(numberOfMediaSections, 1, 'Remote video m-line gets reused for video transceiver'); +}, 'Reuses m-lines in remote negotiation'); +</script>
\ No newline at end of file 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> |