summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
commit40a355a42d4a9444dc753c04c6608dade2f06a23 (patch)
tree871fc667d2de662f171103ce5ec067014ef85e61 /testing/web-platform/tests/webrtc
parentAdding upstream version 124.0.1. (diff)
downloadfirefox-upstream/125.0.1.tar.xz
firefox-upstream/125.0.1.zip
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webrtc')
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html8
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html35
-rw-r--r--testing/web-platform/tests/webrtc/RTCIceTransport.html282
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html13
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html2
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html4
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html59
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js142
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html42
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html14
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html90
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html14
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html6
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html2
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html6
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html37
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html2
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html2
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html7
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html20
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html8
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver.https.html96
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html20
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender.https.html87
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html138
-rw-r--r--testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js2
-rw-r--r--testing/web-platform/tests/webrtc/historical.html9
-rw-r--r--testing/web-platform/tests/webrtc/protocol/codecs-filtered-by-direction.https.html79
-rw-r--r--testing/web-platform/tests/webrtc/protocol/codecs-subsequent-offer.https.html50
29 files changed, 1130 insertions, 146 deletions
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html b/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html
index d344cce9f8..aa66bfbb2b 100644
--- a/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html
@@ -301,12 +301,4 @@
}] }));
}, `with empty urls should throw SyntaxError`);
- // Blink and Gecko fall back to url, but it's not in the spec.
- config_test(makePc => {
- assert_throws_js(TypeError, () =>
- makePc({ iceServers: [{
- url: 'stun:stun1.example.net'
- }] }));
- }, 'with url field should throw TypeError');
-
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html b/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html
index ebc79048a3..b84e6a30db 100644
--- a/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html
@@ -128,7 +128,12 @@
t.add_cleanup(() => offerer.close());
offerer.addEventListener('icecandidate',
- e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
+ e => {
+ if (e.candidate) {
+ assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
+ }
+ }
+ );
offerer.addTransceiver('audio');
await offerer.setLocalDescription();
@@ -142,8 +147,13 @@
t.add_cleanup(() => offerer.close());
t.add_cleanup(() => answerer.close());
- answerer.addEventListener('icecandidate',
- e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
+ offerer.addEventListener('icecandidate',
+ e => {
+ if (e.candidate) {
+ assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
+ }
+ }
+ );
offerer.addTransceiver('audio');
const offer = await offerer.createOffer();
@@ -175,9 +185,14 @@
offerer.setConfiguration({iceTransportPolicy: 'relay'});
offerer.addEventListener('icecandidate',
- e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
-
- await Promise.all([
+ e => {
+ if (e.candidate) {
+ assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
+ }
+ }
+ );
+
+ await Promise.all([
exchangeOfferAnswer(offerer, answerer),
waitForIceStateChange(offerer, ['failed']),
waitForIceStateChange(answerer, ['failed']),
@@ -201,9 +216,13 @@
exchangeIceCandidates(offerer, answerer);
const checkNoCandidate =
- e => assert_equals(e.candidate, null, 'Should get no ICE candidates');
+ e => {
+ if (e.candidate) {
+ assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
+ }
+ };
- offerer.addEventListener('icecandidate', checkNoCandidate);
+ offerer.addEventListener('icecandidate', checkNoCandidate);
await Promise.all([
exchangeOfferAnswer(offerer, answerer),
diff --git a/testing/web-platform/tests/webrtc/RTCIceTransport.html b/testing/web-platform/tests/webrtc/RTCIceTransport.html
index fe12c384e5..e80418bdc4 100644
--- a/testing/web-platform/tests/webrtc/RTCIceTransport.html
+++ b/testing/web-platform/tests/webrtc/RTCIceTransport.html
@@ -1,9 +1,11 @@
<!doctype html>
<meta charset=utf-8>
+<meta name="timeout" content="long">
<title>RTCIceTransport</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
+<script src='RTCConfiguration-helper.js'></script>
<script>
'use strict';
@@ -58,7 +60,7 @@
assert_true(dtlsTransport instanceof RTCDtlsTransport,
'Expect sctp.transport to be an RTCDtlsTransport');
- const iceTransport = dtlsTransport.iceTransport;
+ const {iceTransport} = dtlsTransport;
assert_true(iceTransport instanceof RTCIceTransport,
'Expect dtlsTransport.transport to be an RTCIceTransport');
@@ -162,7 +164,7 @@
assert_equals(iceTransport2.role, 'controlled',
`Expect answerer's iceTransport to take the controlled role`);
});
- }, 'Two connected iceTransports should has matching local/remote candidates returned');
+ }, 'Two connected iceTransports should have matching local/remote candidates returned');
promise_test(t => {
const pc1 = new RTCPeerConnection();
@@ -190,4 +192,280 @@
});
}, 'Unconnected iceTransport should have empty remote candidates and selected pair');
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ const {iceTransport} = transceiver.sender.transport;
+ assert_equals(iceTransport.state, 'new');
+ assert_equals(iceTransport.gatheringState, 'new');
+ }, 'RTCIceTransport should be in state "new" initially');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ const {iceTransport} = transceiver.sender.transport;
+ assert_equals(await nextGatheringState(iceTransport), 'gathering');
+ assert_equals(await nextGatheringState(iceTransport), 'complete');
+ }, 'RTCIceTransport should transition to "gathering" then "complete", after sLD');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ const {iceTransport} = transceiver.sender.transport;
+ assert_equals(await nextGatheringState(iceTransport), 'gathering');
+ pc1.close();
+ assert_equals(iceTransport.gatheringState, 'gathering');
+ const result = await Promise.race([
+ gatheringStateReached(iceTransport, 'complete'),
+ new Promise(r => t.step_timeout(r, 1000))]);
+ assert_equals(result, undefined, `Did not expect a statechange after PC.close(), but got one. state is "${result}"`);
+ }, 'PC.close() should not cause the RTCIceTransport gathering state to transition to "complete"');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ // TODO: If the spec settles on exposing the sctp transport in
+ // have-local-offer, we won't need this audio transceiver hack.
+ // See https://github.com/w3c/webrtc-pc/issues/2898 and
+ // https://github.com/w3c/webrtc-pc/issues/2899
+ const transceiver = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ const {iceTransport} = transceiver.sender.transport;
+ assert_equals(await nextGatheringState(iceTransport), 'gathering');
+ assert_equals(await nextGatheringState(iceTransport), 'complete');
+ // TODO: Maybe, maybe not.
+ assert_not_equals(pc1.sctp, null, 'pc1.sctp should be set after sLD');
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_equals(pc1.sctp.transport.iceTransport, transceiver.sender.transport.iceTransport);
+ }, 'RTCIceTransport should transition to "gathering", then "complete" after sLD (DataChannel case)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ // Copy the SDP before it has candidate attrs
+ const offer = pc1.localDescription;
+ const checkingReached = connectionStateReached(sender.transport.iceTransport, 'checking');
+
+ let result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
+ assert_equals(result, undefined, `Did not expect a statechange right after sLD(offer), but got one. state is "${result}"`);
+
+ await pc2.setRemoteDescription(offer);
+
+ const firstPc2CandidatePromise =
+ new Promise(r => pc2.onicecandidate = e => r(e.candidate));
+
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
+ assert_equals(result, undefined, `Did not expect a statechange callback after sRD(answer), but got one. state is "${result}"`);
+
+ const candidate = await firstPc2CandidatePromise;
+ pc1.addIceCandidate(candidate);
+
+ await checkingReached;
+ }, 'RTCIceTransport should not transition to "checking" until after the answer is set _and_ the first remote candidate is received');
+
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const {sender} = pc1.addTransceiver('audio');
+ exchangeIceCandidates(pc1, pc2);
+ const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_equals(await nextConnectionState(sender.transport.iceTransport), 'checking');
+ assert_equals(await nextConnectionState(sender.transport.iceTransport), 'connected');
+ await gatheringDone;
+ pc2.close();
+ await connectionStateReached(sender.transport.iceTransport, 'disconnected');
+ }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ exchangeIceCandidates(pc1, pc2);
+ const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const {sctp} = pc1;
+ assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'checking');
+ assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'connected');
+ await gatheringDone;
+ pc2.close();
+ await connectionStateReached(sctp.transport.iceTransport, 'disconnected');
+ }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const {sender} = pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ const {iceTransport} = sender.transport;
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ pc1.restartIce();
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
+ }, 'Local ICE restart should not result in a different ICE transport');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const {iceTransport} = pc1.sctp.transport;
+
+ pc1.restartIce();
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
+ }, 'Local ICE restart should not result in a different ICE transport (DataChannel case)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const {sender} = pc1.addTransceiver('audio');
+
+ await pc1.setLocalDescription();
+ const {iceTransport} = sender.transport;
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ pc2.restartIce();
+
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc1.localDescription);
+
+ assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
+ }, 'Remote ICE restart should not result in a different ICE transport');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const {iceTransport} = pc1.sctp.transport;
+
+ pc2.restartIce();
+
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc1.localDescription);
+
+ assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
+ }, 'Remote ICE restart should not result in a different ICE transport (DataChannel case)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ // Add two transceivers, one audio and one video. The default bundlePolicy
+ // ("balanced") will result in each being offered with its own transport,
+ // but allowing the answerer to bundle the second transceiver on the
+ // transport of the first, which the answerer will do by default.
+ const audioTransceiver = pc1.addTransceiver('audio');
+ const videoTransceiver = pc1.addTransceiver('video');
+ pc1.createDataChannel('test');
+
+ await pc1.setLocalDescription();
+ const audioIceTransport = audioTransceiver.sender.transport.iceTransport;
+ const videoIceTransport = videoTransceiver.sender.transport.iceTransport;
+
+ assert_not_equals(audioIceTransport, videoIceTransport, 'audio and video should start out with different transports');
+
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const sctpIceTransport = pc1.sctp.transport.iceTransport;
+
+ assert_equals(videoTransceiver.sender.transport.iceTransport, audioIceTransport, 'After negotiation, the video sender should use the bundle ICE transport from the audio sender');
+ assert_equals(pc1.sctp.transport.iceTransport, audioIceTransport, 'After negotiation, the datachannel should use the bundle ICE transport from the audio sender');
+ assert_not_equals(videoIceTransport.state, 'closed', 'Completion of offer/answer should not close the unused ICE transport immediately');
+
+ await connectionStateReached(videoIceTransport, 'closed');
+ }, 'RTCIceTransport should transition to "closed" if the underlying transport is closed because the answer used bundle');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const {sender} = pc1.addTransceiver('audio');
+ exchangeIceCandidates(pc1, pc2);
+ const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
+ await pc1.setLocalDescription();
+ const {iceTransport} = sender.transport;
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_equals(await nextConnectionState(iceTransport), 'checking');
+ assert_equals(await nextConnectionState(iceTransport), 'connected');
+ await gatheringDone;
+
+ const closedEvent = connectionStateReached(iceTransport, 'closed');
+ pc1.close();
+ assert_equals(sender.transport.iceTransport, iceTransport, 'PC.close() should not unset the sender transport');
+ assert_equals(iceTransport.state, 'closed', 'pc.close() should close the sender transport synchronously');
+ const result = await Promise.race([closedEvent, new Promise(r => t.step_timeout(r, 1000))]);
+ assert_equals(result, undefined, 'statechange event should not fire when transitioning to closed due to PC.close()');
+ }, 'RTCIceTransport should synchronously transition to "closed" with no event if the underlying transport is closed due to PC.close()');
+
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html
index cddbd02c7b..373630ff77 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html
@@ -707,6 +707,19 @@ for (const options of [{}, {negotiated: true, id: 0}]) {
await createDataChannelPair(t, options, pc1);
}, `addTrack, then creating ${mode}, should negotiate properly when max-bundle is used`);
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await getNoiseStream({audio: true, video: true});
+ stream.getTracks().forEach((track) => pc1.addTrack(track, stream));
+ await createDataChannelPair(t, options, pc1, pc2);
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ }, `Renegotiation with audio/video and ${mode} should work properly`);
+
/*
This test is disabled until https://github.com/w3c/webrtc-pc/issues/2562
has been resolved; it presupposes that stopping the first transceiver
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html
index 704fa3c646..7287980a5b 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html
@@ -53,7 +53,7 @@
.then(() => {
assert_equals(pc.signalingState, 'have-local-offer');
assert_session_desc_similar(pc.localDescription, offer);
- assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.currentLocalDescription, null);
assert_array_equals(states, ['have-local-offer']);
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html
index 2d2565c3e1..bb23d2a39a 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html
@@ -19,6 +19,7 @@ promise_test(async t => {
await promise;
assert_not_equals(pc.pendingLocalDescription, null,
'pendingLocalDescription is not null after await');
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
}, "pendingLocalDescription is surfaced at the right time");
promise_test(async t => {
@@ -34,6 +35,7 @@ promise_test(async t => {
await promise;
assert_not_equals(pc.pendingRemoteDescription, null,
'pendingRemoteDescription is not null after await');
+ assert_equals(pc.pendingRemoteDescription, pc.remoteDescription);
}, "pendingRemoteDescription is surfaced at the right time");
promise_test(async t => {
@@ -55,6 +57,7 @@ promise_test(async t => {
await promise;
assert_not_equals(pc2.currentLocalDescription, null,
'currentLocalDescription is not null after await');
+ assert_equals(pc2.currentLocalDescription, pc2.localDescription);
}, "currentLocalDescription is surfaced at the right time");
promise_test(async t => {
@@ -76,6 +79,7 @@ promise_test(async t => {
await promise;
assert_not_equals(pc1.currentRemoteDescription, null,
'currentRemoteDescription is not null after await');
+ assert_equals(pc1.currentRemoteDescription, pc1.remoteDescription);
}, "currentRemoteDescription is surfaced at the right time");
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html
index e39b985bef..a28275047e 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html
@@ -13,8 +13,7 @@ promise_test(async t => {
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('audio', { direction: 'recvonly' });
- await initialOfferAnswerWithIceGatheringStateTransitions(
- pc1, pc2);
+ await initialOfferAnswerWithIceGatheringStateTransitions(pc1, pc2);
await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
expectNoMoreGatheringStateChanges(t, pc1);
@@ -23,31 +22,73 @@ promise_test(async t => {
}, 'rolling back an ICE restart when gathering is complete should not result in iceGatheringState changes');
promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ await initialOfferAnswerWithIceGatheringStateTransitions(pc1, pc2);
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+ await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ expectNoMoreGatheringStateChanges(t, pc1);
+ await pc1.setLocalDescription({type: 'rollback'});
+ await new Promise(r => t.step_timeout(r, 1000));
+}, 'rolling back an ICE restart when gathering is complete should not result in iceGatheringState changes (DataChannel case)');
+
+promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
pc.addTransceiver('audio', { direction: 'recvonly' });
- await pc.setLocalDescription(
- await pc.createOffer());
+ await pc.setLocalDescription();
await iceGatheringStateTransitions(pc, 'gathering', 'complete');
+ const backToNew = iceGatheringStateTransitions(pc, 'new');
await pc.setLocalDescription({type: 'rollback'});
- await iceGatheringStateTransitions(pc, 'new');
+ await backToNew;
}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "complete"');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
+ pc.createDataChannel('test');
+ await pc.setLocalDescription();
+ await iceGatheringStateTransitions(pc, 'gathering', 'complete');
+ const backToNew = iceGatheringStateTransitions(pc, 'new');
+ await pc.setLocalDescription({type: 'rollback'});
+ await backToNew;
+}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "complete" (DataChannel case)');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
pc.addTransceiver('audio', { direction: 'recvonly' });
- await pc.setLocalDescription(
- await pc.createOffer());
+ await pc.setLocalDescription();
await iceGatheringStateTransitions(pc, 'gathering');
+ const backToNew = Promise.allSettled([
+ iceGatheringStateTransitions(pc, 'new'),
+ iceGatheringStateTransitions(pc, 'complete', 'new')]);
await pc.setLocalDescription({type: 'rollback'});
// We might go directly to 'new', or we might go to 'complete' first,
// depending on timing. Allow either.
- const results = await Promise.allSettled([
+ const results = await backToNew;
+ assert_true(results.some(result => result.status == 'fulfilled'),
+ 'ICE gathering state should go back to "new", possibly through "complete"');
+}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "gathering"');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.createDataChannel('test');
+ await pc.setLocalDescription();
+ await iceGatheringStateTransitions(pc, 'gathering');
+ const backToNew = Promise.allSettled([
iceGatheringStateTransitions(pc, 'new'),
iceGatheringStateTransitions(pc, 'complete', 'new')]);
+ await pc.setLocalDescription({type: 'rollback'});
+ // We might go directly to 'new', or we might go to 'complete' first,
+ // depending on timing. Allow either.
+ const results = await backToNew;
assert_true(results.some(result => result.status == 'fulfilled'),
'ICE gathering state should go back to "new", possibly through "complete"');
-}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "gathering"');
+}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "gathering" (DataChannel case)');
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
index 5d188328e8..6f35fde76c 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
@@ -236,6 +236,11 @@ async function waitForIceGatheringState(pc, wantedStates) {
}
}
+async function waitForTrackUnmuted(track) {
+ if (track.muted === false) return true;
+ return waitUntilEvent(track, 'unmute');
+}
+
// Resolves when RTP packets have been received.
async function listenForSSRCs(t, receiver) {
while (true) {
@@ -646,7 +651,7 @@ function findTransceiverForSender(pc, sender) {
}
function preferCodec(transceiver, mimeType, sdpFmtpLine) {
- const {codecs} = RTCRtpSender.getCapabilities(transceiver.receiver.track.kind);
+ const {codecs} = RTCRtpReceiver.getCapabilities(transceiver.receiver.track.kind);
// sdpFmtpLine is optional, pick the first partial match if not given.
const selectedCodecIndex = codecs.findIndex(c => {
return c.mimeType === mimeType && (c.sdpFmtpLine === sdpFmtpLine || !sdpFmtpLine);
@@ -696,6 +701,7 @@ const iceGatheringStateTransitions = async (pc, ...states) => {
}, {once: true});
});
}
+ return states;
};
const initialOfferAnswerWithIceGatheringStateTransitions =
@@ -713,6 +719,14 @@ const initialOfferAnswerWithIceGatheringStateTransitions =
await pc2Transitions;
};
+const expectNoMoreIceConnectionStateChanges = async (t, pc) => {
+ pc.oniceconnectionstatechange =
+ t.step_func(() => {
+ assert_unreached(
+ 'Should not get an iceconnectionstatechange right now!');
+ });
+};
+
const expectNoMoreGatheringStateChanges = async (t, pc) => {
pc.onicegatheringstatechange =
t.step_func(() => {
@@ -721,6 +735,132 @@ const expectNoMoreGatheringStateChanges = async (t, pc) => {
});
};
+function gatheringStateReached(object, state) {
+ if (object instanceof RTCIceTransport) {
+ return new Promise(r =>
+ object.addEventListener("gatheringstatechange", function listener() {
+ if (object.gatheringState == state) {
+ object.removeEventListener("gatheringstatechange", listener);
+ r(state);
+ }
+ })
+ );
+ } else if (object instanceof RTCPeerConnection) {
+ return new Promise(r =>
+ object.addEventListener("icegatheringstatechange", function listener() {
+ if (object.iceGatheringState == state) {
+ object.removeEventListener("icegatheringstatechange", listener);
+ r(state);
+ }
+ })
+ );
+ } else {
+ throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
+ }
+}
+
+function nextGatheringState(object) {
+ if (object instanceof RTCIceTransport) {
+ return new Promise(resolve =>
+ object.addEventListener(
+ "gatheringstatechange",
+ () => resolve(object.gatheringState),
+ { once: true }
+ )
+ );
+ } else if (object instanceof RTCPeerConnection) {
+ return new Promise(resolve =>
+ object.addEventListener(
+ "icegatheringstatechange",
+ () => resolve(object.iceGatheringState),
+ { once: true }
+ )
+ );
+ } else {
+ throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
+ }
+}
+
+function emptyCandidate(pc) {
+ return new Promise(r =>
+ pc.addEventListener("icecandidate", function listener(e) {
+ if (e.candidate && e.candidate.candidate == "") {
+ pc.removeEventListener("icecandidate", listener);
+ r(e);
+ }
+ })
+ );
+}
+
+function nullCandidate(pc) {
+ return new Promise(r =>
+ pc.addEventListener("icecandidate", function listener(e) {
+ if (!e.candidate) {
+ pc.removeEventListener("icecandidate", listener);
+ r(e);
+ }
+ })
+ );
+}
+
+function connectionStateReached(object, state) {
+ if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
+ return new Promise(resolve =>
+ object.addEventListener("statechange", function listener() {
+ if (object.state == state) {
+ object.removeEventListener("statechange", listener);
+ resolve(state);
+ }
+ })
+ );
+ } else if (object instanceof RTCPeerConnection) {
+ return new Promise(resolve =>
+ object.addEventListener("connectionstatechange", function listener() {
+ if (object.connectionState == state) {
+ object.removeEventListener("connectionstatechange", listener);
+ resolve(state);
+ }
+ })
+ );
+ } else {
+ throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
+ }
+}
+
+function nextConnectionState(object) {
+ if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
+ return new Promise(resolve =>
+ object.addEventListener("statechange", () => resolve(object.state), {
+ once: true,
+ })
+ );
+ } else if (object instanceof RTCPeerConnection) {
+ return new Promise(resolve =>
+ object.addEventListener(
+ "connectionstatechange",
+ () => resolve(object.connectionState),
+ { once: true }
+ )
+ );
+ } else {
+ throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
+ }
+}
+
+function nextIceConnectionState(pc) {
+ if (pc instanceof RTCPeerConnection) {
+ return new Promise(resolve =>
+ pc.addEventListener(
+ "iceconnectionstatechange",
+ () => resolve(pc.iceConnectionState),
+ { once: true }
+ )
+ );
+ } else {
+ throw "First parameter is not an RTCPeerConnection";
+ }
+}
+
async function queueAWebrtcTask() {
const pc = new RTCPeerConnection();
pc.addTransceiver('audio');
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html
index af55a0c003..04c2b9c333 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html
@@ -27,4 +27,46 @@
// TODO: this should eventually transition to failed but that takes
// somewhat long (15-30s) so is not testable.
}, 'ICE goes to disconnected if the other side goes away');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ caller.addTransceiver('audio', {direction: 'sendrecv'});
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ await listenToIceConnected(caller);
+
+ // Now, we pull a fast one, and convince callee to abandon the transport
+ // without telling caller.
+ await caller.setLocalDescription();
+ await callee.setRemoteDescription(caller.localDescription);
+ const staleAnswer = await callee.createAnswer();
+ await callee.setRemoteDescription({type: 'rollback', sdp: ''});
+
+ const mlineRegex = /m=audio [0-9]+ /;
+ const mungedOfferSdp = caller.localDescription.sdp
+ .replace(mlineRegex, 'm=audio 0 ')
+ .replace('BUNDLE', 'BUNGLE'); // Avoid "But that mid is rejected!" errors
+
+ // callee gets the munged reoffer with a rejected m-section, caller gets a
+ // stale answer that was made before callee saw the rejected m-section.
+ await callee.setRemoteDescription({type: 'offer', sdp: mungedOfferSdp});
+ await callee.setLocalDescription();
+ await caller.setRemoteDescription(staleAnswer);
+ assert_equals(await nextIceConnectionState(caller), 'disconnected');
+
+ // Now, let's fix this with an ICE restart! callee has already negotiated
+ // a rejection of the first m-section, so it will tolerate it being
+ // revived.
+ caller.restartIce();
+ await caller.setLocalDescription();
+ await callee.setRemoteDescription(caller.localDescription);
+ await callee.setLocalDescription();
+ await caller.setRemoteDescription(callee.localDescription);
+ assert_equals(await nextIceConnectionState(caller), 'checking');
+ assert_equals(await nextIceConnectionState(caller), 'connected');
+ }, 'ICE restart when ICE is disconnected results in checking, then connected');
+
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html
index 5361cb2c1a..9e0afb7ce6 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html
@@ -411,4 +411,18 @@ promise_test(async t => {
assert_true(pc1.iceStates.length >= 2);
assert_equals(pc1.iceStates[1], 'checking');
}, 'iceConnectionState can go to checking without explictly calling addIceCandidate');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ exchangeIceCandidates(pc1, pc2);
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc1);
+ expectNoMoreIceConnectionStateChanges(t, pc1);
+ pc1.restartIce();
+ await exchangeOfferAnswer(pc1, pc2);
+ await new Promise(r => t.step_timeout(r, 1000));
+}, 'ICE restart does not result in a transition back to checking');
+
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html
index 6afaf0fbfb..32a68953bc 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html
@@ -1,5 +1,6 @@
<!doctype html>
<meta charset=utf-8>
+<meta name="timeout" content="long">
<title>RTCPeerConnection.prototype.iceGatheringState</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
@@ -118,6 +119,67 @@
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ assert_true(pc1.localDescription.sdp.includes('a=end-of-candidates'));
+ }, 'local description should have a=end-of-candidates when gathering completes');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ const iceTransport = transceiver.sender.transport.iceTransport;
+
+ // This test code assumes that https://github.com/w3c/webrtc-pc/pull/2894
+ // will be merged. The spec will say to dispatch two tasks; one that fires
+ // the empty candidate, and another that fires
+ // RTCIceTransport.gatheringstatechange, then
+ // RTCPeerConnection.icegatheringstatechange, then the global null
+ // candidate.
+ while (true) {
+ const {candidate} = await new Promise(r => pc1.onicecandidate = r);
+ assert_not_equals(candidate, null, 'Global null candidate event should not fire yet');
+ if (candidate.candidate == '') {
+ break;
+ }
+ }
+ assert_equals(iceTransport.gatheringState, 'gathering');
+ assert_equals(pc1.iceGatheringState, 'gathering');
+
+ // Now, we test the stuff that happens in the second queued task.
+ const events = [];
+ await new Promise(r => {
+ iceTransport.ongatheringstatechange = () => {
+ assert_equals(iceTransport.gatheringState, 'complete');
+ assert_equals(pc1.iceGatheringState, 'complete');
+ events.push('gatheringstatechange');
+ };
+ pc1.onicegatheringstatechange = () => {
+ assert_equals(iceTransport.gatheringState, 'complete');
+ assert_equals(pc1.iceGatheringState, 'complete');
+ events.push('icegatheringstatechange');
+ }
+ pc1.onicecandidate = e => {
+ assert_equals(e.candidate, null);
+ assert_equals(iceTransport.gatheringState, 'complete');
+ assert_equals(pc1.iceGatheringState, 'complete');
+ events.push('icecandidate');
+ r();
+ };
+ });
+
+ assert_array_equals(events, [
+ 'gatheringstatechange',
+ 'icegatheringstatechange',
+ 'icecandidate'
+ ], 'events must be fired on the same task in this order');
+}, 'gathering state and candidate callbacks should fire in the correct order');
+
+ 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', { direction: 'recvonly' });
@@ -125,7 +187,33 @@
pc1, pc2);
await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
- }, 'setLocalDescription(reoffer) with a new transport should cause iceGatheringState to go to "checking" and then "complete"');
+ }, 'setLocalDescription(reoffer) with a restarted transport should cause iceGatheringState to go to "gathering" and then "complete"');
+
+ 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', { direction: 'recvonly' });
+ pc1.addTransceiver('video', { direction: 'recvonly' });
+ exchangeIceCandidates(pc1, pc2);
+ await pc1.setLocalDescription();
+ const firstGather = Promise.all([
+ iceGatheringStateTransitions(pc1, 'gathering', 'complete'),
+ iceGatheringStateTransitions(pc2, 'gathering', 'complete')]);
+ const mungedOffer = {type: 'offer', sdp: pc1.localDescription.sdp.replace('BUNDLE', 'BUNGLE')};
+ await pc2.setRemoteDescription(mungedOffer);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ // Let gathering finish, so we don't have two generations gathering at once
+ // This can cause errors depending on timing.
+ await firstGather;
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+ // We only do this so we don't get errors in addCandidate. We don't want
+ // to wait for it, because we might miss the gathering transitions.
+ pc2.setRemoteDescription(pc1.localDescription);
+ await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ }, 'setLocalDescription(reoffer) with two restarted transports should cause iceGatheringState to go to "gathering" and then "complete"');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html
index 4c20789096..1d80eb8295 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html
@@ -75,8 +75,8 @@
assert_session_desc_similar(pc.localDescription, answer);
assert_session_desc_similar(pc.remoteDescription, offer);
- assert_session_desc_similar(pc.currentLocalDescription, answer);
- assert_session_desc_similar(pc.currentRemoteDescription, offer);
+ assert_equals(pc.currentLocalDescription, pc.localDescription);
+ assert_equals(pc.currentRemoteDescription, pc.remoteDescription);
assert_equals(pc.pendingLocalDescription, null);
assert_equals(pc.pendingRemoteDescription, null);
@@ -107,8 +107,8 @@
assert_session_desc_similar(pc.localDescription, answer);
assert_session_desc_similar(pc.remoteDescription, offer);
- assert_session_desc_similar(pc.currentLocalDescription, answer);
- assert_session_desc_similar(pc.currentRemoteDescription, offer);
+ assert_equals(pc.currentLocalDescription, pc.localDescription);
+ assert_equals(pc.currentRemoteDescription, pc.remoteDescription);
assert_equals(pc.pendingLocalDescription, null);
assert_equals(pc.pendingRemoteDescription, null);
@@ -204,7 +204,7 @@
assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should never be set due to sLD(answer)");
assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sLD");
assert_equals(pc2.pendingRemoteDescription.type, "offer");
- assert_equals(pc2.remoteDescription.sdp, pc2.pendingRemoteDescription.sdp);
+ assert_equals(pc2.remoteDescription, pc2.pendingRemoteDescription);
assert_equals(pc2.currentLocalDescription, null, "currentLocalDescription should not be set synchronously after a call to sLD");
assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sLD");
@@ -219,10 +219,10 @@
assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
assert_not_equals(pc2.currentLocalDescription, null, "currentLocalDescription should be updated before the signalingstatechange event");
assert_equals(pc2.currentLocalDescription.type, "answer");
- assert_equals(pc2.currentLocalDescription.sdp, pc2.localDescription.sdp);
+ assert_equals(pc2.currentLocalDescription, pc2.localDescription);
assert_not_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should be updated before the signalingstatechange event");
assert_equals(pc2.currentRemoteDescription.type, "offer");
- assert_equals(pc2.currentRemoteDescription.sdp, pc2.remoteDescription.sdp);
+ assert_equals(pc2.currentRemoteDescription, pc2.remoteDescription);
await sldPromise;
}, "setLocalDescription(answer) should update internal state with a queued task, in the right order");
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html
index 88f1de5ed8..2be83a01ce 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html
@@ -68,7 +68,7 @@
.then(() => {
assert_equals(pc.signalingState, 'have-local-offer');
assert_session_desc_similar(pc.localDescription, offer);
- assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.currentLocalDescription, null);
assert_array_equals(states, ['have-local-offer']);
@@ -90,7 +90,7 @@
.then(() => {
assert_equals(pc.signalingState, 'have-local-offer');
assert_session_desc_similar(pc.localDescription, offer);
- assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.currentLocalDescription, null);
}));
}, 'setLocalDescription with type offer and null sdp should use lastOffer generated from createOffer');
@@ -219,7 +219,7 @@
assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD");
assert_not_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
assert_equals(pc.pendingLocalDescription.type, "offer");
- assert_equals(pc.pendingLocalDescription.sdp, pc.localDescription.sdp);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.currentLocalDescription, null, "currentLocalDescription should never be updated due to sLD(offer)");
assert_equals(pc.currentRemoteDescription, null, "currentRemoteDescription should never be updated due to sLD(offer)");
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html
index 5a7a76319a..23d1e09a9d 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html
@@ -27,6 +27,7 @@ promise_test(async t => {
await offerer.setLocalDescription();
assert_not_equals(offerer.pendingLocalDescription, null);
+ assert_equals(offerer.pendingLocalDescription, offerer.pendingLocalDescription);
}, "Parameterless SLD() in 'stable' sets pendingLocalDescription");
promise_test(async t => {
@@ -63,6 +64,7 @@ promise_test(async t => {
await answerer.setRemoteDescription(await offerer.createOffer());
await answerer.setLocalDescription();
assert_not_equals(answerer.currentLocalDescription, null);
+ assert_equals(answerer.currentLocalDescription, answerer.currentLocalDescription);
}, "Parameterless SLD() in 'have-remote-offer' sets currentLocalDescription");
promise_test(async t => {
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
index 787edc92e7..08d0393cb7 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
@@ -11,7 +11,6 @@
// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
// The following helper functions are called from RTCPeerConnection-helper.js:
- // assert_session_desc_similar
// generateAudioReceiveOnlyOffer
/*
@@ -68,7 +67,8 @@
.then(() => {
assert_equals(pc.signalingState, 'have-local-offer');
assert_not_equals(pc.localDescription, null);
- assert_not_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.localDescription, pc.localDescription);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.currentLocalDescription, null);
return pc.setLocalDescription({ type: 'rollback' });
@@ -148,7 +148,7 @@
assert_not_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sLD");
assert_equals(pc.pendingLocalDescription.type, "offer");
- assert_equals(pc.pendingLocalDescription.sdp, pc.localDescription.sdp);
+ assert_equals(pc.pendingLocalDescription, pc.localDescription);
assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD(offer)");
const stablePromise = new Promise(resolve => {
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html
index d5acb7e1c9..d95ecafc33 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html
@@ -4,6 +4,7 @@
<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';
@@ -211,7 +212,7 @@
assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
assert_equals(pc2.pendingRemoteDescription.type, "offer");
- assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp);
+ assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription);
assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set after a call to sRD(offer)");
await srdPromise;
@@ -235,7 +236,7 @@
assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD");
assert_not_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sRD");
assert_equals(pc2.pendingLocalDescription.type, "offer");
- assert_equals(pc2.pendingLocalDescription.sdp, pc2.localDescription.sdp);
+ assert_equals(pc2.pendingLocalDescription, pc2.localDescription);
// First, we should go through stable (the implicit rollback part)
const stablePromise = new Promise(resolve => {
@@ -259,7 +260,8 @@
assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
assert_equals(pc2.pendingRemoteDescription.type, "offer");
- assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp);
+ assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription);
+ assert_equals(pc2.pendingRemoteDescription, pc2.pendingRemoteDescription);
assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
await srdPromise;
@@ -303,8 +305,20 @@
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
+ const statePromise = new Promise(r => pc1.onsignalingstatechange = r);
+ // SRD with invalid SDP causes rollback.
const p = pc1.setRemoteDescription({type: 'offer', sdp: 'Invalid SDP'});
- await new Promise(r => pc1.onsignalingstatechange = r);
+ // Ensure that p is eventually awaited for
+ t.add_cleanup(async () => Promise.allSettled([p]));
+ // Ensure that the test will fail rather than timing out if state
+ // does not change.
+ const timeoutPromise = new Promise(
+ (resolve, reject) => t.step_timeout(reject, 1000));
+ try {
+ await Promise.any(statePromise, timeoutPromise);
+ } catch (error) {
+ assert_unreached('State should have changed');
+ }
assert_equals(pc1.signalingState, 'stable');
assert_equals(pc1.pendingLocalDescription, null);
assert_equals(pc1.pendingRemoteDescription, null);
@@ -353,4 +367,19 @@
assert_equals(pc2.getTransceivers().length, 1);
}, 'Transceivers added by sRD(offer) should not show up until sRD resolves');
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+ const description = await pc1.createOffer();
+ const sections = SDPUtils.splitSections(description.sdp);
+ // Compose SDP with a duplicated media section (equal MSID lines)
+ // This is not permitted according to RFC 8830 section 2.
+ const sdp = sections[0] + sections[1] + sections[1].replace('a=mid:', 'a=mid:unique');
+ const p = pc2.setRemoteDescription({type: 'offer', sdp: sdp});
+ await promise_rejects_dom(t, 'OperationError', p);
+ }, 'setRemoteDescription(section with duplicate msid) rejects');
+
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html
index 0e6213d708..a72fa66738 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html
@@ -69,7 +69,7 @@
.then(() => {
assert_equals(pc.signalingState, 'have-remote-offer');
assert_not_equals(pc.remoteDescription, null);
- assert_not_equals(pc.pendingRemoteDescription, null);
+ assert_equals(pc.pendingRemoteDescription, pc.remoteDescription);
assert_equals(pc.currentRemoteDescription, null);
return pc.setRemoteDescription({type: 'rollback'});
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html
index c170f766bd..e2bec7dd91 100644
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html
@@ -136,7 +136,7 @@
await pc2.setLocalDescription(answer);
await pc.setRemoteDescription(answer);
await exchangeOffer(pc2, pc);
- assert_equals(pc.remoteDescription.sdp, pc.pendingRemoteDescription.sdp);
+ assert_equals(pc.remoteDescription, pc.pendingRemoteDescription);
assert_session_desc_similar(pc.remoteDescription, offer);
assert_session_desc_similar(pc.currentRemoteDescription, answer);
}, 'Switching role from offerer to answerer after going back to stable state should succeed');
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html
index 7245d477cc..a5913f6e84 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html
@@ -22,6 +22,13 @@ async function connectAndExpectNoCsrcs(t, kind) {
const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
await exchangeAnswer(pc1, pc2);
+ // Some browsers might need an audio element attached to the DOM.
+ const element = document.createElement(kind);
+ element.autoplay = true;
+ element.srcObject = trackEvent.streams[0];
+ document.body.appendChild(element);
+ t.add_cleanup(() => { document.body.removeChild(element) });
+
assert_array_equals(trackEvent.receiver.getContributingSources(), []);
}
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html
index bfa82b979c..2fcf33dc2e 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html
@@ -23,8 +23,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await receiver.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp'));
}, 'receiver.getStats() via addTransceiver should return stats report containing inbound-rtp stats');
@@ -42,8 +42,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const receiver = callee.getReceivers()[0];
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await receiver.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp'));
}, 'receiver.getStats() via addTrack should return stats report containing inbound-rtp stats');
@@ -61,8 +61,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [receiver] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const [transceiver] = callee.getTransceivers();
const statsPromiseFirst = receiver.getStats();
transceiver.stop();
@@ -85,8 +85,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [receiver] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReportFirst = await receiver.getStats();
callee.close();
const statsReportSecond = await receiver.getStats();
@@ -107,8 +107,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const receiver = callee.getReceivers()[0];
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await receiver.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'candidate-pair'));
assert_true(!![...statsReport.values()].find(({type}) => type === 'local-candidate'));
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html
index 8436a44ebc..cb5336f3f3 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html
@@ -23,6 +23,14 @@ async function initiateSingleTrackCallAndReturnReceiver(t, kind) {
exchangeIceCandidates(pc1, pc2);
const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
await exchangeAnswer(pc1, pc2);
+
+ // Some browsers might need an audio element attached to the DOM.
+ const element = document.createElement(kind);
+ element.autoplay = true;
+ element.srcObject = trackEvent.streams[0];
+ document.body.appendChild(element);
+ t.add_cleanup(() => { document.body.removeChild(element) });
+
return trackEvent.receiver;
}
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver.https.html
new file mode 100644
index 0000000000..eea013140f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver.https.html
@@ -0,0 +1,96 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpReceiver</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ assert_equals(transceiver.receiver.transport, null);
+ }, 'RTCRtpReceiver should have a null transport initially');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_true(transceiver.receiver.transport instanceof RTCDtlsTransport);
+ }, 'RTCRtpReceiver should have a transport after sLD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ await pc1.setLocalDescription({type: 'rollback', sdp: ''});
+ assert_equals(transceiver.receiver.transport, null);
+ }, 'RTCRtpReceiver should have a null transport after rollback of sLD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const receiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const transceiver = pc2.getTransceivers()[0];
+ assert_equals(transceiver.receiver.transport, null);
+ }, 'RTCRtpReceiver should have a null transport after sRD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_true(transceiver.sender.transport instanceof RTCDtlsTransport);
+ assert_true(transceiver.receiver.transport instanceof RTCDtlsTransport);
+ assert_equals(transceiver.receiver.transport, transceiver.sender.transport);
+ }, 'RTCRtpReceiver should have the same transport object as its corresponding RTCRtpSender');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
+ t.add_cleanup(() => pc1.close());
+ const audioTransceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const videoTransceiver = pc1.addTransceiver('video', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_equals(videoTransceiver.receiver.transport, audioTransceiver.receiver.transport);
+ }, 'RTCRtpReceivers that share a bundle transport should have the same transport object');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: 'max-compat'});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const audioTransceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const videoTransceiver = pc1.addTransceiver('video', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_not_equals(videoTransceiver.receiver.transport, audioTransceiver.receiver.transport);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc1.setRemoteDescription(await pc2.createAnswer());
+ // pc2 will accept the bundle, so these should be the same now
+ assert_equals(videoTransceiver.receiver.transport, audioTransceiver.receiver.transport);
+ }, 'RTCRtpReceivers that do not necessarily share a bundle transport should not have the same transport object');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ const transportBefore = transceiver.receiver.transport;
+
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc1.setRemoteDescription(await pc2.createAnswer());
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+
+ const transportAfter = transceiver.receiver.transport;
+ assert_equals(transportAfter, transportBefore);
+ }, 'RTCRtpReceiver should have the same transport object after an ICE restart');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html
index 5c27af2134..6aeed650e5 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html
@@ -22,8 +22,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [ receiver ] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await sender.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp'));
}, 'sender.getStats() via addTransceiver should return stats report containing outbound-rtp stats');
@@ -41,8 +41,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [ receiver ] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await sender.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp'));
}, 'sender.getStats() via addTrack should return stats report containing outbound-rtp stats');
@@ -60,8 +60,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [ receiver ] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const [sender] = caller.getSenders();
const [transceiver] = caller.getTransceivers();
@@ -85,8 +85,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [ receiver ] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const [sender] = caller.getSenders();
const statsReportFirst = await sender.getStats();
caller.close();
@@ -108,8 +108,8 @@
exchangeIceCandidates(caller, callee);
await exchangeOfferAnswer(caller, callee);
const [ receiver ] = callee.getReceivers();
- // Wait for RTP
- await new Promise(r => receiver.track.onunmute = r);
+ await listenToConnected(callee);
+ await waitForTrackUnmuted(receiver.track);
const statsReport = await sender.getStats();
assert_true(!![...statsReport.values()].find(({type}) => type === 'candidate-pair'));
assert_true(!![...statsReport.values()].find(({type}) => type === 'local-candidate'));
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender.https.html
index d17115c46a..21058dfeed 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpSender.https.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender.https.html
@@ -17,4 +17,91 @@ test((t) => {
assert_equals(t2.sender.dtmf, null);
}, "Video sender @dtmf is null");
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ assert_equals(transceiver.sender.transport, null);
+ }, 'RTCRtpSender should have a null transport initially');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_true(transceiver.sender.transport instanceof RTCDtlsTransport);
+ }, 'RTCRtpSender should have a transport after sLD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ await pc1.setLocalDescription({type: 'rollback', sdp: ''});
+ assert_equals(transceiver.sender.transport, null);
+ }, 'RTCRtpSender should have a null transport after rollback of sLD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const sender = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const [transceiver] = pc2.getTransceivers();
+ assert_equals(transceiver.sender.transport, null);
+ }, 'RTCRtpSender should have a null transport after sRD(offer)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_true(transceiver.sender.transport instanceof RTCDtlsTransport);
+ assert_true(transceiver.receiver.transport instanceof RTCDtlsTransport);
+ assert_equals(transceiver.sender.transport, transceiver.receiver.transport);
+ }, 'RTCRtpSender should have the same transport object as its corresponding RTCRtpReceiver');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
+ t.add_cleanup(() => pc1.close());
+ const audioTransceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const videoTransceiver = pc1.addTransceiver('video', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_equals(videoTransceiver.sender.transport, audioTransceiver.sender.transport);
+ }, 'RTCRtpSenders that share a bundle transport should have the same transport object');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: 'max-compat'});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const audioTransceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const videoTransceiver = pc1.addTransceiver('video', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ assert_not_equals(videoTransceiver.sender.transport, audioTransceiver.sender.transport);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc1.setRemoteDescription(await pc2.createAnswer());
+ // pc2 will accept the bundle, so these should be the same now
+ assert_equals(videoTransceiver.sender.transport, audioTransceiver.sender.transport);
+ }, 'RTCRtpSenders that do not necessarily share a bundle transport should not have the same transport object');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const transceiver = pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription();
+ const transportBefore = transceiver.sender.transport;
+
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc1.setRemoteDescription(await pc2.createAnswer());
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+
+ const transportAfter = transceiver.sender.transport;
+ assert_equals(transportAfter, transportBefore);
+ }, 'RTCRtpSender should have the same transport object after an ICE restart');
+
</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html
index f779f5a94c..8142132a8c 100644
--- a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html
@@ -7,41 +7,13 @@
<script>
'use strict';
- // Test is based on the following editor draft:
- // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
-
- /*
- 5.4. RTCRtpTransceiver Interface
- interface RTCRtpTransceiver {
- ...
- void setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
- };
-
- setCodecPreferences
- - Setting codecs to an empty sequence resets codec preferences to any
- default value.
-
- - The codecs sequence passed into setCodecPreferences can only contain
- codecs that are returned by RTCRtpSender.getCapabilities(kind) or
- RTCRtpReceiver.getCapabilities(kind), where kind is the kind of the
- RTCRtpTransceiver on which the method is called. Additionally, the
- RTCRtpCodecParameters dictionary members cannot be modified. If
- codecs does not fulfill these requirements, the user agent MUST throw
- an InvalidModificationError.
- */
- /*
- * 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.
- */
-
test((t) => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
transceiver.setCodecPreferences(capabilities.codecs);
- }, `setCodecPreferences() on audio transceiver with codecs returned from RTCRtpSender.getCapabilities('audio') should succeed`);
+ }, `setCodecPreferences() on audio transceiver with codecs returned from RTCRtpReceiver.getCapabilities('audio') should succeed`);
test((t) => {
const pc = new RTCPeerConnection();
@@ -55,15 +27,6 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities1 = RTCRtpSender.getCapabilities('audio');
- const capabilities2 = RTCRtpReceiver.getCapabilities('audio');
- transceiver.setCodecPreferences([...capabilities1.codecs, ... capabilities2.codecs]);
- }, `setCodecPreferences() with both sender receiver codecs combined should succeed`);
-
- test((t) => {
- const pc = new RTCPeerConnection();
- t.add_cleanup(() => pc.close());
- const transceiver = pc.addTransceiver('audio');
transceiver.setCodecPreferences([]);
}, `setCodecPreferences([]) should succeed`);
@@ -71,7 +34,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const { codecs } = capabilities;
if(codecs.length >= 2) {
@@ -87,74 +50,64 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('video');
- const capabilities = RTCRtpSender.getCapabilities('video');
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
const { codecs } = capabilities;
// This test verifies that the mandatory VP8 codec is present
- // and can be set.
- let tried = false;
- codecs.forEach(codec => {
- if (codec.mimeType.toLowerCase() === 'video/vp8') {
- transceiver.setCodecPreferences([codecs[0]]);
- tried = true;
- }
- });
- assert_true(tried, 'VP8 video codec was found and tried');
+ // and can be preferred.
+ const codec = codecs.find(c => c.mimeType === 'video/VP8');
+ assert_true(!!codec, 'VP8 video codec was found');
+ transceiver.setCodecPreferences([codec]);
}, `setCodecPreferences() with only VP8 should succeed`);
test(() => {
const pc = new RTCPeerConnection();
const transceiver = pc.addTransceiver('video');
- const capabilities = RTCRtpSender.getCapabilities('video');
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
const { codecs } = capabilities;
// This test verifies that the mandatory H264 codec is present
- // and can be set.
- let tried = false;
- codecs.forEach(codec => {
- if (codec.mimeType.toLowerCase() === 'video/h264') {
- transceiver.setCodecPreferences([codecs[0]]);
- tried = true;
- }
- });
- assert_true(tried, 'H264 video codec was found and tried');
+ // and can be preferred.
+ const codec = codecs.find(c => c.mimeType === 'video/H264');
+ assert_true(!!codec, 'H264 video codec was found');
+ transceiver.setCodecPreferences([codec]);
}, `setCodecPreferences() with only H264 should succeed`);
async function getRTPMapLinesWithCodecAsFirst(firstCodec)
{
- const capabilities = RTCRtpSender.getCapabilities('video').codecs;
- capabilities.forEach((codec, idx) => {
+ const codecs = RTCRtpReceiver.getCapabilities('video').codecs;
+ codecs.forEach((codec, idx) => {
if (codec.mimeType === firstCodec) {
- capabilities.splice(idx, 1);
- capabilities.unshift(codec);
+ codecs.splice(idx, 1);
+ codecs.unshift(codec);
}
});
const pc = new RTCPeerConnection();
const transceiver = pc.addTransceiver('video');
- transceiver.setCodecPreferences(capabilities);
+ transceiver.setCodecPreferences(codecs);
const offer = await pc.createOffer();
- return offer.sdp.split('\r\n').filter(line => line.indexOf("a=rtpmap") === 0);
+ return offer.sdp.split('\r\n').filter(line => line.startsWith('a=rtpmap:'));
}
promise_test(async () => {
const lines = await getRTPMapLinesWithCodecAsFirst('video/H264');
assert_greater_than(lines.length, 1);
- assert_true(lines[0].indexOf("H264") !== -1, "H264 should be the first codec");
+ assert_true(lines[0].indexOf('H264') !== -1, 'H264 should be the first codec');
}, `setCodecPreferences() should allow setting H264 as first codec`);
promise_test(async () => {
const lines = await getRTPMapLinesWithCodecAsFirst('video/VP8');
assert_greater_than(lines.length, 1);
- assert_true(lines[0].indexOf("VP8") !== -1, "VP8 should be the first codec");
+ assert_true(lines[0].indexOf('VP8') !== -1, 'VP8 should be the first codec');
}, `setCodecPreferences() should allow setting VP8 as first codec`);
test((t) => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('video');
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(capabilities.codecs));
}, `setCodecPreferences() on audio transceiver with codecs returned from getCapabilities('video') should throw InvalidModificationError`);
@@ -190,7 +143,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const codecs = [
...capabilities.codecs,
{
@@ -207,7 +160,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const codecs = [capabilities.codecs[0]];
codecs[0].clockRate = codecs[0].clockRate / 2;
@@ -218,7 +171,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const codecs = [capabilities.codecs[0]];
codecs[0].channels = codecs[0].channels + 11;
@@ -229,7 +182,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const codecs = [capabilities.codecs[0]];
codecs[0].sdpFmtpLine = "modifiedparameter=1";
@@ -240,7 +193,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const capabilities = RTCRtpSender.getCapabilities('audio');
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
const { codecs } = capabilities;
assert_greater_than(codecs.length, 0,
@@ -257,7 +210,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio');
- const {codecs} = RTCRtpSender.getCapabilities('audio');
+ const {codecs} = RTCRtpReceiver.getCapabilities('audio');
// Reorder codecs, put PCMU/PCMA first.
let firstCodec;
let i;
@@ -283,7 +236,7 @@
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('video');
- const {codecs} = RTCRtpSender.getCapabilities('video');
+ const {codecs} = RTCRtpReceiver.getCapabilities('video');
// Reorder codecs, swap H264 and VP8.
let vp8 = -1;
let h264 = -1;
@@ -319,4 +272,37 @@
assert_equals(rtpParameters.codecs[0].name, firstCodec);
}, `setCodecPreferences() modifies the order of video codecs in createOffer`);
+ // Tests the note removed as result of discussion in
+ // https://github.com/w3c/webrtc-pc/issues/2933
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const transceiver = pc1.addTransceiver('video');
+ const {codecs} = RTCRtpReceiver.getCapabilities('video');
+ const vp8 = codecs.find(codec => codec.mimeType === 'video/VP8');
+ const h264 = codecs.find(codec => codec.mimeType === 'video/H264');
+ const thirdCodec = codecs.find(codec => ['video/VP9', 'video/AV1'].includes(codec.mimeType));
+ assert_true(!!vp8);
+ assert_true(!!h264);
+ assert_true(!!thirdCodec);
+
+ transceiver.setCodecPreferences([vp8, thirdCodec]);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const transceiver2 = pc2.getTransceivers()[0];
+ transceiver2.setCodecPreferences([h264, thirdCodec, vp8]);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const mediaSection = SDPUtils.getMediaSections(pc2.localDescription.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ // Order is determined by pc2 but H264 is not present.
+ assert_equals(rtpParameters.codecs.length, 2);
+ assert_equals(rtpParameters.codecs[0].name, thirdCodec.mimeType.substring(6));
+ assert_equals(rtpParameters.codecs[1].name, 'VP8');
+
+ }, `setCodecPreferences() filters on receiver and prefers receiver order`);
+
</script>
diff --git a/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js b/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
index a516aa4c79..5cc3b745b3 100644
--- a/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
+++ b/testing/web-platform/tests/webrtc/back-forward-cache-with-open-webrtc-connection.https.window.js
@@ -16,5 +16,5 @@ promise_test(async t => {
await openWebRTC(rc1);
// The page should not be eligible for BFCache because of open WebRTC connection and live MediaStreamTrack.
await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false);
- await assertNotRestoredFromBFCache(rc1, ['WebRTC', 'LiveMediaStreamTrack']);
+ await assertNotRestoredFromBFCache(rc1, ['webrtc', 'media-stream']);
});
diff --git a/testing/web-platform/tests/webrtc/historical.html b/testing/web-platform/tests/webrtc/historical.html
index ae7a29dec0..e30a7bc7d3 100644
--- a/testing/web-platform/tests/webrtc/historical.html
+++ b/testing/web-platform/tests/webrtc/historical.html
@@ -2,6 +2,7 @@
<title>Historical WebRTC features</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
+<script src="RTCConfiguration-helper.js"></script>
<div id="log"></div>
<script>
[
@@ -48,4 +49,12 @@
assert_false(name in window);
}, name + " interface should not exist");
});
+
+// Blink and Gecko fall back to url, but it's not in the spec.
+config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: [{
+ url: 'stun:stun1.example.net'
+ }] }));
+}, 'with url field should throw TypeError');
</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/codecs-filtered-by-direction.https.html b/testing/web-platform/tests/webrtc/protocol/codecs-filtered-by-direction.https.html
new file mode 100644
index 0000000000..ed1d65e4b0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/codecs-filtered-by-direction.https.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Codecs in offer get filtered by direction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+function codecEquals(c1, c2) {
+ return c1.mimeType === c2.mimeType &&
+ c1.sdpFmtpLine === c2.sdpFmtpLine &&
+ c1.clockRate === c2.clockRate &&
+ c1.channels === c2.channels;
+}
+
+function splitCodecs() {
+ const sendCodecs = RTCRtpSender.getCapabilities('video').codecs;
+ const receiveCodecs = RTCRtpReceiver.getCapabilities('video').codecs;
+ const codecs = {
+ sendrecv: [],
+ sendonly: [],
+ recvonly: [],
+ };
+ // Ignore RTX since it is present in capabilities once and has no apt.
+ for (const receiveCodec of receiveCodecs) {
+ if (receiveCodec.mimeType === 'video/rtx') continue;
+ if (sendCodecs.find(sendCodec => codecEquals(sendCodec, receiveCodec))) {
+ codecs.sendrecv.push(receiveCodec);
+ }
+ }
+
+ for (const sendCodec of sendCodecs) {
+ if (sendCodec.mimeType === 'video/rtx') continue;
+ if (!receiveCodecs.find(receiveCodec => codecEquals(sendCodec, receiveCodec))) {
+ codecs.sendonly.push(sendCodec);
+ }
+ }
+ for (const receiveCodec of receiveCodecs) {
+ if (receiveCodec.mimeType === 'video/rtx') continue;
+ if (!sendCodecs.find(sendCodec => codecEquals(sendCodec, receiveCodec))) {
+ codecs.recvonly.push(receiveCodec);
+ }
+ }
+ return codecs;
+}
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const allCodecs = splitCodecs();
+ const transceiver = pc.addTransceiver('video');
+
+ transceiver.direction = 'sendonly'
+ const offer = await pc.createOffer();
+ const mediaSection = SDPUtils.getMediaSections(offer.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ const sendonlyCodecs = rtpParameters.codecs.filter(c => c.name !== 'rtx');
+ assert_equals(sendonlyCodecs.length, allCodecs.sendrecv.length + allCodecs.sendonly.length);
+}, 'Codecs get filtered by direction for sendonly');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const allCodecs = splitCodecs();
+ const transceiver = pc.addTransceiver('video');
+
+ transceiver.direction = 'recvonly'
+ const offer = await pc.createOffer();
+ const mediaSection = SDPUtils.getMediaSections(offer.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ const recvonlyCodecs = rtpParameters.codecs.filter(c => c.name !== 'rtx');
+ assert_equals(recvonlyCodecs.length, allCodecs.sendrecv.length + allCodecs.recvonly.length);
+}, 'Codecs get filtered by direction for recvonly');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/codecs-subsequent-offer.https.html b/testing/web-platform/tests/webrtc/protocol/codecs-subsequent-offer.https.html
new file mode 100644
index 0000000000..fc9240e950
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/codecs-subsequent-offer.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Codecs in subsequent offer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+// A test for https://www.rfc-editor.org/rfc/rfc8829.html#name-subsequent-offers
+// The "m=" line and corresponding "a=rtpmap" and "a=fmtp" lines MUST only include
+// media formats that have not been excluded by the codec preferences of the
+// associated transceiver and also MUST include all currently available formats.
+
+// A VP8-only offer.
+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 RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendrecv
+a=mid:video
+a=rtpmap:100 VP8/90000
+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 transceiver = pc.getTransceivers()[0];
+ assert_not_equals(transceiver.stopped, true);
+ const offer = await pc.createOffer();
+ const mediaSection = SDPUtils.getMediaSections(offer.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ const vp8 = rtpParameters.codecs.filter(codec => codec.name === 'VP8');
+ const h264 = rtpParameters.codecs.filter(codec => codec.name === 'H264');
+ assert_greater_than(vp8.length, 0);
+ assert_greater_than(h264.length, 0);
+}, 'A subsequent offer after a VP8-only negotiation includes at least all mandatory to implement codecs');
+
+</script>