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');
+