diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /testing/web-platform/tests/webrtc | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-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')
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> |