RTCPeerConnection-restartIce.https.html (20243B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title></title> 4 <script src=/resources/testharness.js></script> 5 <script src=/resources/testharnessreport.js></script> 6 <script src="RTCPeerConnection-helper.js"></script> 7 <script> 8 "use strict"; 9 10 function getLines(sdp, startsWith) { 11 const lines = sdp.split("\r\n").filter(l => l.startsWith(startsWith)); 12 assert_true(lines.length > 0, `One or more ${startsWith} in sdp`); 13 return lines; 14 } 15 16 const getUfrags = ({sdp}) => getLines(sdp, "a=ice-ufrag:"); 17 const getPwds = ({sdp}) => getLines(sdp, "a=ice-pwd:"); 18 19 const negotiators = [ 20 { 21 tag: "", 22 async setOffer(pc) { 23 await pc.setLocalDescription(await pc.createOffer()); 24 }, 25 async setAnswer(pc) { 26 await pc.setLocalDescription(await pc.createAnswer()); 27 }, 28 }, 29 { 30 tag: " (perfect negotiation)", 31 async setOffer(pc) { 32 await pc.setLocalDescription(); 33 }, 34 async setAnswer(pc) { 35 await pc.setLocalDescription(); 36 }, 37 }, 38 ]; 39 40 async function exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator) { 41 await negotiator.setOffer(pc1); 42 await pc2.setRemoteDescription(pc1.localDescription); 43 await negotiator.setAnswer(pc2); 44 await pc1.setRemoteDescription(pc2.localDescription); // End on pc1. No race 45 } 46 47 async function exchangeOfferAnswerEndOnSecond(pc1, pc2, negotiator) { 48 await negotiator.setOffer(pc1); 49 await pc2.setRemoteDescription(pc1.localDescription); 50 await pc1.setRemoteDescription(await pc2.createAnswer()); 51 await pc2.setLocalDescription(pc1.remoteDescription); // End on pc2. No race 52 } 53 54 async function assertNoNegotiationNeeded(t, pc, state = "stable") { 55 assert_equals(pc.signalingState, state, `In ${state} state`); 56 const event = await Promise.race([ 57 new Promise(r => pc.onnegotiationneeded = r), 58 new Promise(r => t.step_timeout(r, 10)) 59 ]); 60 assert_equals(event, undefined, "No negotiationneeded event"); 61 } 62 63 // In Chromium, assert_equals() produces test expectations with the values 64 // compared. Because ufrags are different on each run, this would make Chromium 65 // test expectations different on each run on tests that failed when comparing 66 // ufrags. To work around this problem, assert_ufrags_equals() and 67 // assert_ufrags_not_equals() should be preferred over assert_equals() and 68 // assert_not_equals(). 69 function assert_ufrags_equals(x, y, description) { 70 assert_true(x === y, description); 71 } 72 function assert_ufrags_not_equals(x, y, description) { 73 assert_false(x === y, description); 74 } 75 76 promise_test(async t => { 77 const pc = new RTCPeerConnection(); 78 pc.close(); 79 pc.restartIce(); 80 await assertNoNegotiationNeeded(t, pc, "closed"); 81 }, "restartIce() has no effect on a closed peer connection"); 82 83 promise_test(async t => { 84 const pc1 = new RTCPeerConnection(); 85 const pc2 = new RTCPeerConnection(); 86 t.add_cleanup(() => pc1.close()); 87 t.add_cleanup(() => pc2.close()); 88 89 pc1.restartIce(); 90 await assertNoNegotiationNeeded(t, pc1); 91 pc1.addTransceiver("audio"); 92 await new Promise(r => pc1.onnegotiationneeded = r); 93 await assertNoNegotiationNeeded(t, pc1); 94 }, "restartIce() does not trigger negotiation ahead of initial negotiation"); 95 96 // Run remaining tests twice: once for each negotiator 97 98 for (const negotiator of negotiators) { 99 const {tag} = negotiator; 100 101 promise_test(async t => { 102 const pc1 = new RTCPeerConnection(); 103 const pc2 = new RTCPeerConnection(); 104 t.add_cleanup(() => pc1.close()); 105 t.add_cleanup(() => pc2.close()); 106 107 pc1.addTransceiver("audio"); 108 await new Promise(r => pc1.onnegotiationneeded = r); 109 pc1.restartIce(); 110 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 111 await assertNoNegotiationNeeded(t, pc1); 112 }, `restartIce() has no effect on initial negotiation${tag}`); 113 114 promise_test(async t => { 115 const pc1 = new RTCPeerConnection(); 116 const pc2 = new RTCPeerConnection(); 117 t.add_cleanup(() => pc1.close()); 118 t.add_cleanup(() => pc2.close()); 119 120 pc1.addTransceiver("audio"); 121 await new Promise(r => pc1.onnegotiationneeded = r); 122 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 123 pc1.restartIce(); 124 await new Promise(r => pc1.onnegotiationneeded = r); 125 }, `restartIce() fires negotiationneeded after initial negotiation${tag}`); 126 127 promise_test(async t => { 128 const pc1 = new RTCPeerConnection(); 129 const pc2 = new RTCPeerConnection(); 130 t.add_cleanup(() => pc1.close()); 131 t.add_cleanup(() => pc2.close()); 132 133 pc1.addTransceiver("audio"); 134 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 135 136 const [oldUfrag1] = getUfrags(pc1.localDescription); 137 const [oldUfrag2] = getUfrags(pc2.localDescription); 138 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 139 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "control 1"); 140 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "control 2"); 141 142 pc1.restartIce(); 143 await new Promise(r => pc1.onnegotiationneeded = r); 144 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 145 const [newUfrag1] = getUfrags(pc1.localDescription); 146 const [newUfrag2] = getUfrags(pc2.localDescription); 147 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 148 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 149 await assertNoNegotiationNeeded(t, pc1); 150 151 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 152 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); 153 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); 154 }, `restartIce() causes fresh ufrags${tag}`); 155 156 promise_test(async t => { 157 const config = {bundlePolicy: "max-bundle"}; 158 const pc1 = new RTCPeerConnection(config); 159 const pc2 = new RTCPeerConnection(config); 160 t.add_cleanup(() => pc1.close()); 161 t.add_cleanup(() => pc2.close()); 162 163 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 164 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 165 166 // See the explanation below about Chrome's onnegotiationneeded firing 167 // too early. 168 const negotiationNeededPromise1 = 169 new Promise(r => pc1.onnegotiationneeded = r); 170 pc1.addTransceiver("video"); 171 pc1.addTransceiver("audio"); 172 await negotiationNeededPromise1; 173 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 174 175 const [videoTc, audioTc] = pc1.getTransceivers(); 176 const [videoTp, audioTp] = 177 pc1.getTransceivers().map(tc => tc.sender.transport); 178 assert_equals(pc1.getTransceivers().length, 2, 'transceiver count'); 179 180 // On Chrome, it is possible (likely, even) that videoTc.sender.transport.state 181 // will be 'connected' by the time we get here. We'll race 2 promises here: 182 // 1. Resolve after onstatechange is called with connected state. 183 // 2. If already connected, resolve immediately. 184 await Promise.race([ 185 new Promise(r => videoTc.sender.transport.onstatechange = 186 () => videoTc.sender.transport.state == "connected" && r()), 187 new Promise(r => videoTc.sender.transport.state == "connected" && r()) 188 ]); 189 assert_equals(videoTc.sender.transport.state, "connected"); 190 191 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 192 assert_equals(videoTp, pc1.getTransceivers()[0].sender.transport, 193 'offer/answer retains dtls transport'); 194 assert_equals(audioTp, pc1.getTransceivers()[1].sender.transport, 195 'offer/answer retains dtls transport'); 196 197 const negotiationNeededPromise2 = 198 new Promise(r => pc1.onnegotiationneeded = r); 199 pc1.restartIce(); 200 await negotiationNeededPromise2; 201 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 202 203 const [newVideoTp, newAudioTp] = 204 pc1.getTransceivers().map(tc => tc.sender.transport); 205 assert_equals(videoTp, newVideoTp, 'ice restart retains dtls transport'); 206 assert_equals(audioTp, newAudioTp, 'ice restart retains dtls transport'); 207 }, `restartIce() retains dtls transports${tag}`); 208 209 promise_test(async t => { 210 const pc1 = new RTCPeerConnection(); 211 const pc2 = new RTCPeerConnection(); 212 t.add_cleanup(() => pc1.close()); 213 t.add_cleanup(() => pc2.close()); 214 215 pc1.addTransceiver("audio"); 216 await new Promise(r => pc1.onnegotiationneeded = r); 217 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 218 219 const [oldUfrag1] = getUfrags(pc1.localDescription); 220 const [oldUfrag2] = getUfrags(pc2.localDescription); 221 222 await negotiator.setOffer(pc1); 223 pc1.restartIce(); 224 await pc2.setRemoteDescription(pc1.localDescription); 225 await negotiator.setAnswer(pc2); 226 // Several tests in this file initializes the onnegotiationneeded listener 227 // before the setLocalDescription() or setRemoteDescription() that we expect 228 // to trigger negotiation needed. This allows Chrome to exercise these tests 229 // without timing out due to a bug that causes onnegotiationneeded to fire too 230 // early. 231 // TODO(https://crbug.com/985797): Once Chrome does not fire ONN too early, 232 // simply do "await new Promise(...)" instead of 233 // "await negotiationNeededPromise" here and in other tests in this file. 234 const negotiationNeededPromise = 235 new Promise(r => pc1.onnegotiationneeded = r); 236 await pc1.setRemoteDescription(pc2.localDescription); 237 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); 238 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); 239 await negotiationNeededPromise; 240 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 241 const [newUfrag1] = getUfrags(pc1.localDescription); 242 const [newUfrag2] = getUfrags(pc2.localDescription); 243 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 244 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 245 await assertNoNegotiationNeeded(t, pc1); 246 }, `restartIce() works in have-local-offer${tag}`); 247 248 promise_test(async t => { 249 const pc1 = new RTCPeerConnection(); 250 const pc2 = new RTCPeerConnection(); 251 t.add_cleanup(() => pc1.close()); 252 t.add_cleanup(() => pc2.close()); 253 254 pc1.addTransceiver("audio"); 255 await new Promise(r => pc1.onnegotiationneeded = r); 256 await negotiator.setOffer(pc1); 257 pc1.restartIce(); 258 await pc2.setRemoteDescription(pc1.localDescription); 259 await negotiator.setAnswer(pc2); 260 const negotiationNeededPromise = 261 new Promise(r => pc1.onnegotiationneeded = r); 262 await pc1.setRemoteDescription(pc2.localDescription); 263 const [oldUfrag1] = getUfrags(pc1.localDescription); 264 const [oldUfrag2] = getUfrags(pc2.localDescription); 265 await negotiationNeededPromise; 266 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 267 const [newUfrag1] = getUfrags(pc1.localDescription); 268 const [newUfrag2] = getUfrags(pc2.localDescription); 269 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 270 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 271 await assertNoNegotiationNeeded(t, pc1); 272 }, `restartIce() works in initial have-local-offer${tag}`); 273 274 promise_test(async t => { 275 const pc1 = new RTCPeerConnection(); 276 const pc2 = new RTCPeerConnection(); 277 t.add_cleanup(() => pc1.close()); 278 t.add_cleanup(() => pc2.close()); 279 280 pc1.addTransceiver("audio"); 281 await new Promise(r => pc1.onnegotiationneeded = r); 282 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 283 284 const [oldUfrag1] = getUfrags(pc1.localDescription); 285 const [oldUfrag2] = getUfrags(pc2.localDescription); 286 287 await negotiator.setOffer(pc2); 288 await pc1.setRemoteDescription(pc2.localDescription); 289 pc1.restartIce(); 290 await pc2.setRemoteDescription(await pc1.createAnswer()); 291 const negotiationNeededPromise = 292 new Promise(r => pc1.onnegotiationneeded = r); 293 await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race 294 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1"); 295 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2"); 296 await negotiationNeededPromise; 297 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 298 const [newUfrag1] = getUfrags(pc1.localDescription); 299 const [newUfrag2] = getUfrags(pc2.localDescription); 300 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 301 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 302 await assertNoNegotiationNeeded(t, pc1); 303 }, `restartIce() works in have-remote-offer${tag}`); 304 305 promise_test(async t => { 306 const pc1 = new RTCPeerConnection(); 307 const pc2 = new RTCPeerConnection(); 308 t.add_cleanup(() => pc1.close()); 309 t.add_cleanup(() => pc2.close()); 310 311 pc2.addTransceiver("audio"); 312 await negotiator.setOffer(pc2); 313 await pc1.setRemoteDescription(pc2.localDescription); 314 pc1.restartIce(); 315 await pc2.setRemoteDescription(await pc1.createAnswer()); 316 await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race 317 await assertNoNegotiationNeeded(t, pc1); 318 }, `restartIce() does nothing in initial have-remote-offer${tag}`); 319 320 promise_test(async t => { 321 const pc1 = new RTCPeerConnection(); 322 const pc2 = new RTCPeerConnection(); 323 t.add_cleanup(() => pc1.close()); 324 t.add_cleanup(() => pc2.close()); 325 326 pc1.addTransceiver("audio"); 327 await new Promise(r => pc1.onnegotiationneeded = r); 328 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 329 330 const [oldUfrag1] = getUfrags(pc1.localDescription); 331 const [oldUfrag2] = getUfrags(pc2.localDescription); 332 333 pc1.restartIce(); 334 await new Promise(r => pc1.onnegotiationneeded = r); 335 const negotiationNeededPromise = 336 new Promise(r => pc1.onnegotiationneeded = r); 337 await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator); 338 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "nothing yet 1"); 339 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "nothing yet 2"); 340 await negotiationNeededPromise; 341 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 342 const [newUfrag1] = getUfrags(pc1.localDescription); 343 const [newUfrag2] = getUfrags(pc2.localDescription); 344 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 345 assert_ufrags_not_equals(newUfrag2, oldUfrag2, "ufrag 2 changed"); 346 await assertNoNegotiationNeeded(t, pc1); 347 }, `restartIce() survives remote offer${tag}`); 348 349 promise_test(async t => { 350 const pc1 = new RTCPeerConnection(); 351 const pc2 = new RTCPeerConnection(); 352 t.add_cleanup(() => pc1.close()); 353 t.add_cleanup(() => pc2.close()); 354 355 pc1.addTransceiver("audio"); 356 await new Promise(r => pc1.onnegotiationneeded = r); 357 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 358 359 const [oldUfrag1] = getUfrags(pc1.localDescription); 360 const [oldUfrag2] = getUfrags(pc2.localDescription); 361 362 pc1.restartIce(); 363 pc2.restartIce(); 364 await new Promise(r => pc1.onnegotiationneeded = r); 365 await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator); 366 const [newUfrag1] = getUfrags(pc1.localDescription); 367 const [newUfrag2] = getUfrags(pc2.localDescription); 368 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 369 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 370 await assertNoNegotiationNeeded(t, pc1); 371 372 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 373 assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1"); 374 assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2"); 375 await assertNoNegotiationNeeded(t, pc1); 376 }, `restartIce() is satisfied by remote ICE restart${tag}`); 377 378 promise_test(async t => { 379 const pc1 = new RTCPeerConnection(); 380 const pc2 = new RTCPeerConnection(); 381 t.add_cleanup(() => pc1.close()); 382 t.add_cleanup(() => pc2.close()); 383 384 pc1.addTransceiver("audio"); 385 await new Promise(r => pc1.onnegotiationneeded = r); 386 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 387 388 const [oldUfrag1] = getUfrags(pc1.localDescription); 389 const [oldUfrag2] = getUfrags(pc2.localDescription); 390 391 pc1.restartIce(); 392 await new Promise(r => pc1.onnegotiationneeded = r); 393 await pc1.setLocalDescription(await pc1.createOffer({iceRestart: false})); 394 await pc2.setRemoteDescription(pc1.localDescription); 395 await negotiator.setAnswer(pc2); 396 await pc1.setRemoteDescription(pc2.localDescription); 397 const [newUfrag1] = getUfrags(pc1.localDescription); 398 const [newUfrag2] = getUfrags(pc2.localDescription); 399 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 400 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 401 await assertNoNegotiationNeeded(t, pc1); 402 }, `restartIce() trumps {iceRestart: false}${tag}`); 403 404 promise_test(async t => { 405 const pc1 = new RTCPeerConnection(); 406 const pc2 = new RTCPeerConnection(); 407 t.add_cleanup(() => pc1.close()); 408 t.add_cleanup(() => pc2.close()); 409 410 pc1.addTransceiver("audio"); 411 await new Promise(r => pc1.onnegotiationneeded = r); 412 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 413 414 const [oldUfrag1] = getUfrags(pc1.localDescription); 415 const [oldUfrag2] = getUfrags(pc2.localDescription); 416 417 pc1.restartIce(); 418 await new Promise(r => pc1.onnegotiationneeded = r); 419 await negotiator.setOffer(pc1); 420 const negotiationNeededPromise = 421 new Promise(r => pc1.onnegotiationneeded = r); 422 await pc1.setLocalDescription({type: "rollback"}); 423 await negotiationNeededPromise; 424 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 425 const [newUfrag1] = getUfrags(pc1.localDescription); 426 const [newUfrag2] = getUfrags(pc2.localDescription); 427 assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed"); 428 assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed"); 429 await assertNoNegotiationNeeded(t, pc1); 430 }, `restartIce() survives rollback${tag}`); 431 432 promise_test(async t => { 433 const pc1 = new RTCPeerConnection({bundlePolicy: "max-compat"}); 434 const pc2 = new RTCPeerConnection({bundlePolicy: "max-compat"}); 435 t.add_cleanup(() => pc1.close()); 436 t.add_cleanup(() => pc2.close()); 437 438 pc1.addTransceiver("audio"); 439 pc1.addTransceiver("video"); 440 await new Promise(r => pc1.onnegotiationneeded = r); 441 await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator); 442 443 const oldUfrags1 = getUfrags(pc1.localDescription); 444 const oldUfrags2 = getUfrags(pc2.localDescription); 445 const oldPwds2 = getPwds(pc2.localDescription); 446 447 pc1.restartIce(); 448 await new Promise(r => pc1.onnegotiationneeded = r); 449 450 // Engineer a partial ICE restart from pc2 451 pc2.restartIce(); 452 await negotiator.setOffer(pc2); 453 { 454 let {type, sdp} = pc2.localDescription; 455 // Restore both old ice-ufrag and old ice-pwd to trigger a partial restart 456 sdp = sdp.replace(getUfrags({sdp})[0], oldUfrags2[0]); 457 sdp = sdp.replace(getPwds({sdp})[0], oldPwds2[0]); 458 const newUfrags2 = getUfrags({sdp}); 459 const newPwds2 = getPwds({sdp}); 460 assert_ufrags_equals(newUfrags2[0], oldUfrags2[0], "control ufrag match"); 461 assert_ufrags_equals(newPwds2[0], oldPwds2[0], "control pwd match"); 462 assert_ufrags_not_equals(newUfrags2[1], oldUfrags2[1], "control ufrag non-match"); 463 assert_ufrags_not_equals(newPwds2[1], oldPwds2[1], "control pwd non-match"); 464 await pc1.setRemoteDescription({type, sdp}); 465 } 466 const negotiationNeededPromise = 467 new Promise(r => pc1.onnegotiationneeded = r); 468 await negotiator.setAnswer(pc1); 469 const newUfrags1 = getUfrags(pc1.localDescription); 470 assert_ufrags_equals(newUfrags1[0], oldUfrags1[0], "Unchanged 1"); 471 assert_ufrags_not_equals(newUfrags1[1], oldUfrags1[1], "Restarted 2"); 472 await negotiationNeededPromise; 473 await negotiator.setOffer(pc1); 474 const newestUfrags1 = getUfrags(pc1.localDescription); 475 assert_ufrags_not_equals(newestUfrags1[0], oldUfrags1[0], "Restarted 1"); 476 assert_ufrags_not_equals(newestUfrags1[1], oldUfrags1[1], "Restarted 2"); 477 await assertNoNegotiationNeeded(t, pc1); 478 }, `restartIce() survives remote offer containing partial restart${tag}`); 479 480 } 481 482 </script>