diff --git a/webrtc/RTCConfiguration-iceTransportPolicy.html b/webrtc/RTCConfiguration-iceTransportPolicy.html index ebc79048a3ae39..b84e6a30db0fe2 100644 --- a/webrtc/RTCConfiguration-iceTransportPolicy.html +++ b/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/webrtc/RTCIceTransport.html b/webrtc/RTCIceTransport.html index fe12c384e5e5b3..e80418bdc456f0 100644 --- a/webrtc/RTCIceTransport.html +++ b/webrtc/RTCIceTransport.html @@ -1,9 +1,11 @@ + RTCIceTransport + diff --git a/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html b/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html index e39b985bef2fe6..a28275047e4f6a 100644 --- a/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html +++ b/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html @@ -13,8 +13,7 @@ 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); @@ -22,32 +21,74 @@ await new Promise(r => t.step_timeout(r, 1000)); }, '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)'); diff --git a/webrtc/RTCPeerConnection-helper.js b/webrtc/RTCPeerConnection-helper.js index 92e4ccfa43acd3..6f35fde76ce274 100644 --- a/webrtc/RTCPeerConnection-helper.js +++ b/webrtc/RTCPeerConnection-helper.js @@ -701,6 +701,7 @@ const iceGatheringStateTransitions = async (pc, ...states) => { }, {once: true}); }); } + return states; }; const initialOfferAnswerWithIceGatheringStateTransitions = @@ -718,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(() => { @@ -726,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/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html b/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html index af55a0c003512f..04c2b9c333cb89 100644 --- a/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html +++ b/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'); + diff --git a/webrtc/RTCPeerConnection-iceConnectionState.https.html b/webrtc/RTCPeerConnection-iceConnectionState.https.html index 5361cb2c1af2b6..9e0afb7ce693f2 100644 --- a/webrtc/RTCPeerConnection-iceConnectionState.https.html +++ b/webrtc/RTCPeerConnection-iceConnectionState.https.html @@ -411,4 +411,18 @@ 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'); + diff --git a/webrtc/RTCPeerConnection-iceGatheringState.html b/webrtc/RTCPeerConnection-iceGatheringState.html index 6afaf0fbfbd1dc..32a68953bc5931 100644 --- a/webrtc/RTCPeerConnection-iceGatheringState.html +++ b/webrtc/RTCPeerConnection-iceGatheringState.html @@ -1,5 +1,6 @@ + RTCPeerConnection.prototype.iceGatheringState @@ -115,6 +116,67 @@ await new Promise(r => t.step_timeout(r, 500)); }, 'setLocalDescription() with no transports should not cause iceGatheringState to change'); + 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()); @@ -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/webrtc/RTCRtpReceiver.https.html b/webrtc/RTCRtpReceiver.https.html new file mode 100644 index 00000000000000..eea013140f393a --- /dev/null +++ b/webrtc/RTCRtpReceiver.https.html @@ -0,0 +1,96 @@ + + +RTCRtpReceiver + + + diff --git a/webrtc/RTCRtpSender.https.html b/webrtc/RTCRtpSender.https.html index d17115c46af24e..21058dfeedcad9 100644 --- a/webrtc/RTCRtpSender.https.html +++ b/webrtc/RTCRtpSender.https.html @@ -17,4 +17,91 @@ 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'); +