| <!doctype html> | |
| <meta charset=utf-8> | |
| <title></title> | |
| <script src=/resources/testharness.js></script> | |
| <script src=/resources/testharnessreport.js></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| "use strict"; | |
| function getLines(sdp, startsWith) { | |
| const lines = sdp.split("\r\n").filter(l => l.startsWith(startsWith)); | |
| assert_true(lines.length > 0, `One or more ${startsWith} in sdp`); | |
| return lines; | |
| } | |
| const getUfrags = ({sdp}) => getLines(sdp, "a=ice-ufrag:"); | |
| const getPwds = ({sdp}) => getLines(sdp, "a=ice-pwd:"); | |
| 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 negotiator.setAnswer(pc2); | |
| await pc1.setRemoteDescription(pc2.localDescription); // End on pc1. No race | |
| } | |
| 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 | |
| } | |
| async function assertNoNegotiationNeeded(t, pc, state = "stable") { | |
| assert_equals(pc.signalingState, state, `In ${state} state`); | |
| const event = await Promise.race([ | |
| new Promise(r => pc.onnegotiationneeded = r), | |
| new Promise(r => t.step_timeout(r, 10)) | |
| ]); | |
| assert_equals(event, undefined, "No negotiationneeded event"); | |
| } | |
| // In Chromium, assert_equals() produces test expectations with the values | |
| // compared. Because ufrags are different on each run, this would make Chromium | |
| // test expectations different on each run on tests that failed when comparing | |
| // ufrags. To work around this problem, assert_ufrags_equals() and | |
| // assert_ufrags_not_equals() should be preferred over assert_equals() and | |
| // assert_not_equals(). | |
| function assert_ufrags_equals(x, y, description) { | |
| assert_true(x === y, description); | |
| } | |
| function assert_ufrags_not_equals(x, y, description) { | |
| assert_false(x === y, description); | |
| } | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| pc.close(); | |
| pc.restartIce(); | |
| await assertNoNegotiationNeeded(t, pc, "closed"); | |
| }, "restartIce() has no effect on a closed peer connection"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.restartIce(); | |
| await assertNoNegotiationNeeded(t, pc1); | |
| pc1.addTransceiver("audio"); | |
| await new Promise(r => pc1.onnegotiationneeded = r); | |
| await assertNoNegotiationNeeded(t, pc1); | |
| }, "restartIce() does not trigger negotiation ahead of initial negotiation"); | |
| // 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 => pc2.addIceCandidate(e.candidate); | |
| pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); | |
| // 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}`); | |
| } | |
| </script> |