From a89135bbba417c0e5b7b0d6f35dc0ecbcc59f9ec Mon Sep 17 00:00:00 2001 From: Jan-Ivar Bruaroey Date: Thu, 4 Feb 2021 17:12:49 +0000 Subject: [PATCH] Test both zero and 1-argument SLD in RTCPeerConnection-restartIce.https.html Differential Revision: https://phabricator.services.mozilla.com/D101254 bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1685833 gecko-commit: da549b4201cc188c22846112b7559ceda59b8910 gecko-reviewers: bwc --- .../RTCPeerConnection-restartIce.https.html | 814 +++++++++--------- 1 file changed, 421 insertions(+), 393 deletions(-) diff --git a/webrtc/RTCPeerConnection-restartIce.https.html b/webrtc/RTCPeerConnection-restartIce.https.html index ee247bb19249a8..dc5e1674576e8c 100644 --- a/webrtc/RTCPeerConnection-restartIce.https.html +++ b/webrtc/RTCPeerConnection-restartIce.https.html @@ -16,15 +16,36 @@ const getUfrags = ({sdp}) => getLines(sdp, "a=ice-ufrag:"); const getPwds = ({sdp}) => getLines(sdp, "a=ice-pwd:"); -async function exchangeOfferAnswerEndOnFirst(pc1, pc2) { - await pc1.setLocalDescription(await pc1.createOffer()); +const negotiators = [ + { + tag: "", + async setOffer(pc) { + await pc.setLocalDescription(await pc.createOffer()); + }, + async setAnswer(pc) { + await pc.setLocalDescription(await pc.createAnswer()); + }, + }, + { + tag: " (perfect negotiation)", + async setOffer(pc) { + await pc.setLocalDescription(); + }, + async setAnswer(pc) { + await pc.setLocalDescription(); + }, + }, +]; + +async function exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator) { + await negotiator.setOffer(pc1); await pc2.setRemoteDescription(pc1.localDescription); - await pc2.setLocalDescription(await pc2.createAnswer()); + await negotiator.setAnswer(pc2); await pc1.setRemoteDescription(pc2.localDescription); // End on pc1. No race } -async function exchangeOfferAnswerEndOnSecond(pc1, pc2) { - await pc1.setLocalDescription(await pc1.createOffer()); +async function exchangeOfferAnswerEndOnSecond(pc1, pc2, negotiator) { + await negotiator.setOffer(pc1); await pc2.setRemoteDescription(pc1.localDescription); await pc1.setRemoteDescription(await pc2.createAnswer()); await pc2.setLocalDescription(pc1.remoteDescription); // End on pc2. No race @@ -65,397 +86,404 @@ await assertNoNegotiationNeeded(t, pc1); }, "restartIce() does not trigger negotiation ahead of initial negotiation"); -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - pc1.restartIce(); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() has no effect on initial negotiation"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); -}, "restartIce() fires negotiationneeded after initial negotiation"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "control 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "control 2"); - - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); - - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); -}, "restartIce() causes fresh ufrags"); - -promise_test(async t => { - const config = {bundlePolicy: "max-bundle"}; - const pc1 = new RTCPeerConnection(config); - const pc2 = new RTCPeerConnection(config); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.onicecandidate = e => { - if (e.candidate) { - pc2.addIceCandidate({ - candidate: e.candidate.candidate, - sdpMid: e.candidate.sdpMid - }); +// Run remaining tests twice: once for each negotiator + +for (const negotiator of negotiators) { + const {tag} = negotiator; + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + pc1.restartIce(); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() has no effect on initial negotiation${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + }, `restartIce() fires negotiationneeded after initial negotiation${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "control 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "control 2"); + + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); + }, `restartIce() causes fresh ufrags${tag}`); + + promise_test(async t => { + const config = {bundlePolicy: "max-bundle"}; + const pc1 = new RTCPeerConnection(config); + const pc2 = new RTCPeerConnection(config); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.onicecandidate = e => { + if (e.candidate) { + pc2.addIceCandidate({ + candidate: e.candidate.candidate, + sdpMid: e.candidate.sdpMid + }); + } } - } - pc2.onicecandidate = e => { - if (e.candidate) { - pc1.addIceCandidate({ - candidate: e.candidate.candidate, - sdpMid: e.candidate.sdpMid - }); + pc2.onicecandidate = e => { + if (e.candidate) { + pc1.addIceCandidate({ + candidate: e.candidate.candidate, + sdpMid: e.candidate.sdpMid + }); + } } - } - - // See the explanation below about Chrome's onnegotiationneeded firing - // too early. - const negotiationNeededPromise1 = - new Promise(r => pc1.onnegotiationneeded = r); - pc1.addTransceiver("video"); - pc1.addTransceiver("audio"); - await negotiationNeededPromise1; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [videoTc, audioTc] = pc1.getTransceivers(); - const [videoTp, audioTp] = - pc1.getTransceivers().map(tc => tc.sender.transport); - assert_equals(pc1.getTransceivers().length, 2, 'transceiver count'); - - // On Chrome, it is possible (likely, even) that videoTc.sender.transport.state - // will be 'connected' by the time we get here. We'll race 2 promises here: - // 1. Resolve after onstatechange is called with connected state. - // 2. If already connected, resolve immediately. - await Promise.race([ - new Promise(r => videoTc.sender.transport.onstatechange = - () => videoTc.sender.transport.state == "connected" && r()), - new Promise(r => videoTc.sender.transport.state == "connected" && r()) - ]); - assert_equals(videoTc.sender.transport.state, "connected"); - - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - assert_equals(videoTp, pc1.getTransceivers()[0].sender.transport, - 'offer/answer retains dtls transport'); - assert_equals(audioTp, pc1.getTransceivers()[1].sender.transport, - 'offer/answer retains dtls transport'); - - const negotiationNeededPromise2 = - new Promise(r => pc1.onnegotiationneeded = r); - pc1.restartIce(); - await negotiationNeededPromise2; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [newVideoTp, newAudioTp] = - pc1.getTransceivers().map(tc => tc.sender.transport); - assert_equals(videoTp, newVideoTp, 'ice restart retains dtls transport'); - assert_equals(audioTp, newAudioTp, 'ice restart retains dtls transport'); -}, "restartIce() retains dtls transports"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - - await pc1.setLocalDescription(await pc1.createOffer()); - pc1.restartIce(); - await pc2.setRemoteDescription(pc1.localDescription); - await pc2.setLocalDescription(await pc2.createAnswer()); - // Several tests in this file initializes the onnegotiationneeded listener - // before the setLocalDescription() or setRemoteDescription() that we expect - // to trigger negotiation needed. This allows Chrome to exercise these tests - // without timing out due to a bug that causes onnegotiationneeded to fire too - // early. - // TODO(https://crbug.com/985797): Once Chrome does not fire ONN too early, - // simply do "await new Promise(...)" instead of - // "await negotiationNeededPromise" here and in other tests in this file. - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setRemoteDescription(pc2.localDescription); - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); - await negotiationNeededPromise; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() works in have-local-offer"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription(await pc1.createOffer()); - pc1.restartIce(); - await pc2.setRemoteDescription(pc1.localDescription); - await pc2.setLocalDescription(await pc2.createAnswer()); - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setRemoteDescription(pc2.localDescription); - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - await negotiationNeededPromise; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() works in initial have-local-offer"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - await pc2.setLocalDescription(await pc2.createOffer()); - await pc1.setRemoteDescription(pc2.localDescription); - pc1.restartIce(); - await pc2.setRemoteDescription(await pc1.createAnswer()); - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); - await negotiationNeededPromise; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() works in have-remote-offer"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc2.addTransceiver("audio"); - await pc2.setLocalDescription(await pc2.createOffer()); - await pc1.setRemoteDescription(pc2.localDescription); - pc1.restartIce(); - await pc2.setRemoteDescription(await pc1.createAnswer()); - await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() does nothing in initial have-remote-offer"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnSecond(pc2, pc1); - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "nothing yet 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "nothing yet 2"); - await negotiationNeededPromise; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag2, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() survives remote offer"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - - pc1.restartIce(); - pc2.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnSecond(pc2, pc1); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); - - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); - assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() is satisfied by remote ICE restart"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription(await pc1.createOffer({iceRestart: false})); - await pc2.setRemoteDescription(pc1.localDescription); - await pc2.setLocalDescription(await pc2.createAnswer()); - await pc1.setRemoteDescription(pc2.localDescription); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() trumps {iceRestart: false}"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection(); - const pc2 = new RTCPeerConnection(); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const [oldUfrag1] = getUfrags(pc1.localDescription); - const [oldUfrag2] = getUfrags(pc2.localDescription); - - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription(await pc1.createOffer()); - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription({type: "rollback"}); - await negotiationNeededPromise; - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - const [newUfrag1] = getUfrags(pc1.localDescription); - const [newUfrag2] = getUfrags(pc2.localDescription); - assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); - assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() survives rollback"); - -promise_test(async t => { - const pc1 = new RTCPeerConnection({bundlePolicy: "max-compat"}); - const pc2 = new RTCPeerConnection({bundlePolicy: "max-compat"}); - t.add_cleanup(() => pc1.close()); - t.add_cleanup(() => pc2.close()); - - pc1.addTransceiver("audio"); - pc1.addTransceiver("video"); - await new Promise(r => pc1.onnegotiationneeded = r); - await exchangeOfferAnswerEndOnFirst(pc1, pc2); - - const oldUfrags1 = getUfrags(pc1.localDescription); - const oldUfrags2 = getUfrags(pc2.localDescription); - const oldPwds2 = getPwds(pc2.localDescription); - - pc1.restartIce(); - await new Promise(r => pc1.onnegotiationneeded = r); + // See the explanation below about Chrome's onnegotiationneeded firing + // too early. + const negotiationNeededPromise1 = + new Promise(r => pc1.onnegotiationneeded = r); + pc1.addTransceiver("video"); + pc1.addTransceiver("audio"); + await negotiationNeededPromise1; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [videoTc, audioTc] = pc1.getTransceivers(); + const [videoTp, audioTp] = + pc1.getTransceivers().map(tc => tc.sender.transport); + assert_equals(pc1.getTransceivers().length, 2, 'transceiver count'); + + // On Chrome, it is possible (likely, even) that videoTc.sender.transport.state + // will be 'connected' by the time we get here. We'll race 2 promises here: + // 1. Resolve after onstatechange is called with connected state. + // 2. If already connected, resolve immediately. + await Promise.race([ + new Promise(r => videoTc.sender.transport.onstatechange = + () => videoTc.sender.transport.state == "connected" && r()), + new Promise(r => videoTc.sender.transport.state == "connected" && r()) + ]); + assert_equals(videoTc.sender.transport.state, "connected"); + + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + assert_equals(videoTp, pc1.getTransceivers()[0].sender.transport, + 'offer/answer retains dtls transport'); + assert_equals(audioTp, pc1.getTransceivers()[1].sender.transport, + 'offer/answer retains dtls transport'); + + const negotiationNeededPromise2 = + new Promise(r => pc1.onnegotiationneeded = r); + pc1.restartIce(); + await negotiationNeededPromise2; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [newVideoTp, newAudioTp] = + pc1.getTransceivers().map(tc => tc.sender.transport); + assert_equals(videoTp, newVideoTp, 'ice restart retains dtls transport'); + assert_equals(audioTp, newAudioTp, 'ice restart retains dtls transport'); + }, `restartIce() retains dtls transports${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + await negotiator.setOffer(pc1); + pc1.restartIce(); + await pc2.setRemoteDescription(pc1.localDescription); + await negotiator.setAnswer(pc2); + // Several tests in this file initializes the onnegotiationneeded listener + // before the setLocalDescription() or setRemoteDescription() that we expect + // to trigger negotiation needed. This allows Chrome to exercise these tests + // without timing out due to a bug that causes onnegotiationneeded to fire too + // early. + // TODO(https://crbug.com/985797): Once Chrome does not fire ONN too early, + // simply do "await new Promise(...)" instead of + // "await negotiationNeededPromise" here and in other tests in this file. + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await pc1.setRemoteDescription(pc2.localDescription); + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); + await negotiationNeededPromise; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() works in have-local-offer${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await negotiator.setOffer(pc1); + pc1.restartIce(); + await pc2.setRemoteDescription(pc1.localDescription); + await negotiator.setAnswer(pc2); + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await pc1.setRemoteDescription(pc2.localDescription); + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + await negotiationNeededPromise; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() works in initial have-local-offer${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + await negotiator.setOffer(pc2); + await pc1.setRemoteDescription(pc2.localDescription); + pc1.restartIce(); + await pc2.setRemoteDescription(await pc1.createAnswer()); + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); + await negotiationNeededPromise; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() works in have-remote-offer${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc2.addTransceiver("audio"); + await negotiator.setOffer(pc2); + await pc1.setRemoteDescription(pc2.localDescription); + pc1.restartIce(); + await pc2.setRemoteDescription(await pc1.createAnswer()); + await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() does nothing in initial have-remote-offer${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator); + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "nothing yet 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "nothing yet 2"); + await negotiationNeededPromise; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag2, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() survives remote offer${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + pc1.restartIce(); + pc2.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); + assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() is satisfied by remote ICE restart${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + await pc1.setLocalDescription(await pc1.createOffer({iceRestart: false})); + await pc2.setRemoteDescription(pc1.localDescription); + await negotiator.setAnswer(pc2); + await pc1.setRemoteDescription(pc2.localDescription); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() trumps {iceRestart: false}${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const [oldUfrag1] = getUfrags(pc1.localDescription); + const [oldUfrag2] = getUfrags(pc2.localDescription); + + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + await negotiator.setOffer(pc1); + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await pc1.setLocalDescription({type: "rollback"}); + await negotiationNeededPromise; + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + const [newUfrag1] = getUfrags(pc1.localDescription); + const [newUfrag2] = getUfrags(pc2.localDescription); + assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); + assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() survives rollback${tag}`); + + promise_test(async t => { + const pc1 = new RTCPeerConnection({bundlePolicy: "max-compat"}); + const pc2 = new RTCPeerConnection({bundlePolicy: "max-compat"}); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + pc1.addTransceiver("audio"); + pc1.addTransceiver("video"); + await new Promise(r => pc1.onnegotiationneeded = r); + await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); + + const oldUfrags1 = getUfrags(pc1.localDescription); + const oldUfrags2 = getUfrags(pc2.localDescription); + const oldPwds2 = getPwds(pc2.localDescription); + + pc1.restartIce(); + await new Promise(r => pc1.onnegotiationneeded = r); + + // Engineer a partial ICE restart from pc2 + pc2.restartIce(); + await negotiator.setOffer(pc2); + { + let {type, sdp} = pc2.localDescription; + // Restore both old ice-ufrag and old ice-pwd to trigger a partial restart + sdp = sdp.replace(getUfrags({sdp})[0], oldUfrags2[0]); + sdp = sdp.replace(getPwds({sdp})[0], oldPwds2[0]); + const newUfrags2 = getUfrags({sdp}); + const newPwds2 = getPwds({sdp}); + assert_ufrags_equals(newUfrags2[0], oldUfrags2[0], "control ufrag match"); + assert_ufrags_equals(newPwds2[0], oldPwds2[0], "control pwd match"); + assert_ufrags_not_equals(newUfrags2[1], oldUfrags2[1], "control ufrag non-match"); + assert_ufrags_not_equals(newPwds2[1], oldPwds2[1], "control pwd non-match"); + await pc1.setRemoteDescription({type, sdp}); + } + const negotiationNeededPromise = + new Promise(r => pc1.onnegotiationneeded = r); + await negotiator.setAnswer(pc1); + const newUfrags1 = getUfrags(pc1.localDescription); + assert_ufrags_equals(newUfrags1[0], oldUfrags1[0], "Unchanged 1"); + assert_ufrags_not_equals(newUfrags1[1], oldUfrags1[1], "Restarted 2"); + await negotiationNeededPromise; + await negotiator.setOffer(pc1); + const newestUfrags1 = getUfrags(pc1.localDescription); + assert_ufrags_not_equals(newestUfrags1[0], oldUfrags1[0], "Restarted 1"); + assert_ufrags_not_equals(newestUfrags1[1], oldUfrags1[1], "Restarted 2"); + await assertNoNegotiationNeeded(t, pc1); + }, `restartIce() survives remote offer containing partial restart${tag}`); - // Engineer a partial ICE restart from pc2 - pc2.restartIce(); - await pc2.setLocalDescription(await pc2.createOffer()); - { - let {type, sdp} = pc2.localDescription; - // Restore both old ice-ufrag and old ice-pwd to trigger a partial restart - sdp = sdp.replace(getUfrags({sdp})[0], oldUfrags2[0]); - sdp = sdp.replace(getPwds({sdp})[0], oldPwds2[0]); - const newUfrags2 = getUfrags({sdp}); - const newPwds2 = getPwds({sdp}); - assert_ufrags_equals(newUfrags2[0], oldUfrags2[0], "control ufrag match"); - assert_ufrags_equals(newPwds2[0], oldPwds2[0], "control pwd match"); - assert_ufrags_not_equals(newUfrags2[1], oldUfrags2[1], "control ufrag non-match"); - assert_ufrags_not_equals(newPwds2[1], oldPwds2[1], "control pwd non-match"); - await pc1.setRemoteDescription({type, sdp}); - } - const negotiationNeededPromise = - new Promise(r => pc1.onnegotiationneeded = r); - await pc1.setLocalDescription(await pc1.createAnswer()); - const newUfrags1 = getUfrags(pc1.localDescription); - assert_ufrags_equals(newUfrags1[0], oldUfrags1[0], "Unchanged 1"); - assert_ufrags_not_equals(newUfrags1[1], oldUfrags1[1], "Restarted 2"); - await negotiationNeededPromise; - await pc1.setLocalDescription(await pc1.createOffer()); - const newestUfrags1 = getUfrags(pc1.localDescription); - assert_ufrags_not_equals(newestUfrags1[0], oldUfrags1[0], "Restarted 1"); - assert_ufrags_not_equals(newestUfrags1[1], oldUfrags1[1], "Restarted 2"); - await assertNoNegotiationNeeded(t, pc1); -}, "restartIce() survives remote offer containing partial restart"); +}