RTCPeerConnection-setRemoteDescription-rollback.html (26293B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCPeerConnection.prototype.setRemoteDescription rollback</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 // Test is based on the following editor draft: 11 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 12 13 // The following helper functions are called from RTCPeerConnection-helper.js: 14 // assert_session_desc_similar 15 // generateAudioReceiveOnlyOffer 16 // generateDataChannelOffer 17 18 /* 19 4.3.2. Interface Definition 20 [Constructor(optional RTCConfiguration configuration)] 21 interface RTCPeerConnection : EventTarget { 22 Promise<void> setLocalDescription( 23 RTCSessionDescriptionInit description); 24 25 readonly attribute RTCSessionDescription? localDescription; 26 readonly attribute RTCSessionDescription? currentLocalDescription; 27 readonly attribute RTCSessionDescription? pendingLocalDescription; 28 29 Promise<void> setRemoteDescription( 30 RTCSessionDescriptionInit description); 31 32 readonly attribute RTCSessionDescription? remoteDescription; 33 readonly attribute RTCSessionDescription? currentRemoteDescription; 34 readonly attribute RTCSessionDescription? pendingRemoteDescription; 35 ... 36 }; 37 38 4.6.2. RTCSessionDescription Class 39 dictionary RTCSessionDescriptionInit { 40 required RTCSdpType type; 41 DOMString sdp = ""; 42 }; 43 44 4.6.1. RTCSdpType 45 enum RTCSdpType { 46 "offer", 47 "pranswer", 48 "answer", 49 "rollback" 50 }; 51 */ 52 53 /* 54 4.3.1.6. Set the RTCSessionSessionDescription 55 2.2.3. Otherwise, if description is set as a remote description, then run one 56 of the following steps: 57 - If description is of type "rollback", then this is a rollback. 58 Set connection.pendingRemoteDescription to null and signaling state to stable. 59 */ 60 promise_test(t => { 61 const pc = new RTCPeerConnection(); 62 t.add_cleanup(() => pc.close()); 63 64 const states = []; 65 pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState)); 66 67 return generateDataChannelOffer(pc) 68 .then(offer => pc.setRemoteDescription(offer)) 69 .then(() => { 70 assert_equals(pc.signalingState, 'have-remote-offer'); 71 assert_not_equals(pc.remoteDescription, null); 72 assert_equals(pc.pendingRemoteDescription, pc.remoteDescription); 73 assert_equals(pc.currentRemoteDescription, null); 74 75 return pc.setRemoteDescription({type: 'rollback'}); 76 }) 77 .then(() => { 78 assert_equals(pc.signalingState, 'stable'); 79 assert_equals(pc.remoteDescription, null); 80 assert_equals(pc.pendingRemoteDescription, null); 81 assert_equals(pc.currentRemoteDescription, null); 82 83 assert_array_equals(states, ['have-remote-offer', 'stable']); 84 }); 85 }, 'setRemoteDescription(rollback) in have-remote-offer state should revert to stable state'); 86 87 /* 88 4.3.1.6. Set the RTCSessionSessionDescription 89 2.3. If the description's type is invalid for the current signaling state of 90 connection, then reject p with a newly created InvalidStateError and abort 91 these steps. 92 93 [jsep] 94 4.1.8.2. Rollback 95 - Rollback can only be used to cancel proposed changes; 96 there is no support for rolling back from a stable state to a 97 previous stable state 98 */ 99 promise_test(t => { 100 const pc = new RTCPeerConnection(); 101 t.add_cleanup(() => pc.close()); 102 return promise_rejects_dom(t, 'InvalidStateError', 103 pc.setRemoteDescription({type: 'rollback'})); 104 }, `setRemoteDescription(rollback) from stable state should reject with InvalidStateError`); 105 106 promise_test(async t => { 107 const pc = new RTCPeerConnection(); 108 t.add_cleanup(() => pc.close()); 109 await pc.setLocalDescription(); 110 await promise_rejects_dom(t, 'InvalidStateError', pc.setRemoteDescription({ type: 'rollback' })); 111 }, `setRemoteDescription(rollback) after setting a local offer should reject with InvalidStateError`); 112 113 promise_test(t => { 114 const pc = new RTCPeerConnection(); 115 t.add_cleanup(() => pc.close()); 116 return generateAudioReceiveOnlyOffer(pc) 117 .then(offer => pc.setRemoteDescription(offer)) 118 .then(() => pc.setRemoteDescription({ 119 type: 'rollback', 120 sdp: '!<Invalid SDP Content>;' 121 })); 122 }, `setRemoteDescription(rollback) should ignore invalid sdp content and succeed`); 123 124 promise_test(async t => { 125 const pc1 = new RTCPeerConnection(); 126 const pc2 = new RTCPeerConnection(); 127 t.add_cleanup(() => pc1.close()); 128 t.add_cleanup(() => pc2.close()); 129 130 // We don't use this right away 131 pc1.addTransceiver('audio', { direction: 'recvonly' }); 132 const offer1 = await pc1.createOffer(); 133 134 // Create offer from pc2, apply and rollback on pc1 135 pc2.addTransceiver('audio', { direction: 'recvonly' }); 136 const offer2 = await pc2.createOffer(); 137 await pc1.setRemoteDescription(offer2); 138 await pc1.setRemoteDescription({type: "rollback"}); 139 140 // Then try applying pc1's old offer 141 await pc1.setLocalDescription(offer1); 142 }, `local offer created before setRemoteDescription(remote offer) then rollback should still be usable`); 143 144 promise_test(async t => { 145 const pc1 = new RTCPeerConnection(); 146 const pc2 = new RTCPeerConnection(); 147 t.add_cleanup(() => pc1.close()); 148 t.add_cleanup(() => pc2.close()); 149 150 const stream = await getNoiseStream({video: true}); 151 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 152 pc1.addTrack(stream.getTracks()[0], stream); 153 154 // We don't use this right away. pc1 has provisionally decided that the 155 // (only) transceiver is bound to level 0. 156 const offer1 = await pc1.createOffer(); 157 158 // Create offer from pc2, apply and rollback on pc1 159 pc2.addTransceiver('audio', { direction: 'recvonly' }); 160 pc2.addTransceiver('video', { direction: 'recvonly' }); 161 const offer2 = await pc2.createOffer(); 162 // pc1 now should change its mind about what level its video transceiver is 163 // bound to. It was 0, now it is 1. 164 await pc1.setRemoteDescription(offer2); 165 166 // Rolling back should put things back the way they were. 167 await pc1.setRemoteDescription({type: "rollback"}); 168 169 // Then try applying pc1's old offer 170 await pc1.setLocalDescription(offer1); 171 }, "local offer created before setRemoteDescription(remote offer) with different transceiver level assignments then rollback should still be usable"); 172 173 promise_test(async t => { 174 const pc1 = new RTCPeerConnection(); 175 t.add_cleanup(() => pc1.close()); 176 const pc2 = new RTCPeerConnection(); 177 t.add_cleanup(() => pc2.close()); 178 179 const stream = await getNoiseStream({video: true}); 180 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 181 pc1.addTrack(stream.getTracks()[0], stream); 182 183 await pc2.setRemoteDescription(await pc1.createOffer()); 184 assert_equals(pc2.getTransceivers().length, 1); 185 await pc2.setRemoteDescription({type: "rollback"}); 186 assert_equals(pc2.getTransceivers().length, 0); 187 }, "rollback of a remote offer should remove a transceiver"); 188 189 promise_test(async t => { 190 const pc1 = new RTCPeerConnection(); 191 t.add_cleanup(() => pc1.close()); 192 const pc2 = new RTCPeerConnection(); 193 t.add_cleanup(() => pc2.close()); 194 195 const stream = await getNoiseStream({video: true}); 196 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 197 pc1.addTrack(stream.getTracks()[0], stream); 198 199 await pc2.setRemoteDescription(await pc1.createOffer()); 200 assert_equals(pc2.getTransceivers().length, 1); 201 202 const stream2 = await getNoiseStream({video: true}); 203 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 204 const track = stream2.getVideoTracks()[0]; 205 await pc2.getTransceivers()[0].sender.replaceTrack(track); 206 207 await pc2.setRemoteDescription({type: "rollback"}); 208 assert_equals(pc2.getTransceivers().length, 0); 209 }, "rollback of a remote offer should remove touched transceiver"); 210 211 promise_test(async t => { 212 const pc1 = new RTCPeerConnection(); 213 t.add_cleanup(() => pc1.close()); 214 const pc2 = new RTCPeerConnection(); 215 t.add_cleanup(() => pc2.close()); 216 217 const stream = await getNoiseStream({video: true}); 218 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 219 pc1.addTrack(stream.getTracks()[0], stream); 220 221 await pc2.setRemoteDescription(await pc1.createOffer()); 222 assert_equals(pc2.getTransceivers().length, 1); 223 224 const stream2 = await getNoiseStream({video: true}); 225 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 226 pc2.addTrack(stream2.getTracks()[0], stream2); 227 228 await pc2.setRemoteDescription({type: "rollback"}); 229 assert_equals(pc2.getTransceivers().length, 1); 230 assert_equals(pc2.getTransceivers()[0].mid, null); 231 assert_equals(pc2.getTransceivers()[0].receiver.transport, null); 232 }, "rollback of a remote offer should keep a transceiver"); 233 234 promise_test(async t => { 235 const pc1 = new RTCPeerConnection(); 236 t.add_cleanup(() => pc1.close()); 237 const pc2 = new RTCPeerConnection(); 238 t.add_cleanup(() => pc2.close()); 239 240 const stream = await getNoiseStream({video: true}); 241 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 242 pc1.addTrack(stream.getTracks()[0], stream); 243 244 const stream2 = await getNoiseStream({video: true}); 245 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 246 pc2.addTrack(stream2.getTracks()[0], stream2); 247 248 await pc2.setRemoteDescription(await pc1.createOffer()); 249 assert_equals(pc2.getTransceivers().length, 1); 250 251 await pc2.setRemoteDescription({type: "rollback"}); 252 assert_equals(pc2.getTransceivers().length, 1); 253 assert_equals(pc2.getTransceivers()[0].mid, null); 254 assert_equals(pc2.getTransceivers()[0].receiver.transport, null); 255 }, "rollback of a remote offer should keep a transceiver created by addtrack"); 256 257 promise_test(async t => { 258 const pc1 = new RTCPeerConnection(); 259 t.add_cleanup(() => pc1.close()); 260 const pc2 = new RTCPeerConnection(); 261 t.add_cleanup(() => pc2.close()); 262 263 const stream = await getNoiseStream({video: true}); 264 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 265 pc1.addTrack(stream.getTracks()[0], stream); 266 267 await pc2.setRemoteDescription(await pc1.createOffer()); 268 assert_equals(pc2.getTransceivers().length, 1); 269 270 const stream2 = await getNoiseStream({video: true}); 271 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 272 pc2.addTrack(stream2.getTracks()[0], stream2); 273 await pc2.getTransceivers()[0].sender.replaceTrack(null); 274 await pc2.setRemoteDescription({type: "rollback"}); 275 assert_equals(pc2.getTransceivers().length, 1); 276 }, "rollback of a remote offer should keep a transceiver without tracks"); 277 278 promise_test(async t => { 279 const pc = new RTCPeerConnection(); 280 t.add_cleanup(() => pc.close()); 281 282 const stream = await getNoiseStream({video: true}); 283 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 284 pc.addTrack(stream.getTracks()[0], stream); 285 286 const states = []; 287 const signalingstatechangeResolver = new Resolver(); 288 pc.onsignalingstatechange = () => { 289 states.push(pc.signalingState); 290 signalingstatechangeResolver.resolve(); 291 }; 292 293 const offer = await pc.createOffer(); 294 await pc.setLocalDescription(offer); 295 assert_not_equals(pc.getTransceivers()[0].sender.transport, null); 296 await pc.setLocalDescription({type: "rollback"}); 297 assert_equals(pc.getTransceivers().length, 1); 298 assert_equals(pc.getTransceivers()[0].mid, null) 299 assert_equals(pc.getTransceivers()[0].sender.transport, null); 300 await pc.setLocalDescription(offer); 301 assert_equals(pc.getTransceivers().length, 1); 302 await signalingstatechangeResolver.promise; 303 assert_array_equals(states, ['have-local-offer', 'stable', 'have-local-offer']); 304 }, "explicit rollback of local offer should remove transceivers and transport"); 305 306 promise_test(async t => { 307 const pc1 = new RTCPeerConnection(); 308 t.add_cleanup(() => pc1.close()); 309 const pc2 = new RTCPeerConnection(); 310 t.add_cleanup(() => pc2.close()); 311 312 const states = []; 313 const signalingstatechangeResolver = new Resolver(); 314 pc1.onsignalingstatechange = () => { 315 states.push(pc1.signalingState); 316 signalingstatechangeResolver.resolve(); 317 }; 318 const stream1 = await getNoiseStream({audio: true}); 319 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 320 pc1.addTransceiver(stream1.getTracks()[0], stream1); 321 322 const stream2 = await getNoiseStream({audio: true}); 323 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 324 pc2.addTransceiver(stream2.getTracks()[0], stream2); 325 326 await pc1.setLocalDescription(await pc1.createOffer()); 327 pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event right now")); 328 await pc1.setRemoteDescription(await pc2.createOffer()); 329 await pc1.setLocalDescription(await pc1.createAnswer()); 330 await signalingstatechangeResolver.promise; 331 assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']); 332 await new Promise(r => pc1.onnegotiationneeded = r); 333 }, "when using addTransceiver, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded until we settle in stable"); 334 335 promise_test(async t => { 336 const pc1 = new RTCPeerConnection(); 337 t.add_cleanup(() => pc1.close()); 338 const pc2 = new RTCPeerConnection(); 339 t.add_cleanup(() => pc2.close()); 340 341 const states = []; 342 const signalingstatechangeResolver = new Resolver(); 343 pc1.onsignalingstatechange = () => { 344 states.push(pc1.signalingState); 345 signalingstatechangeResolver.resolve(); 346 }; 347 const stream1 = await getNoiseStream({audio: true}); 348 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 349 pc1.addTrack(stream1.getTracks()[0], stream1); 350 351 const stream2 = await getNoiseStream({audio: true}); 352 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 353 pc2.addTrack(stream2.getTracks()[0], stream2); 354 355 await pc1.setLocalDescription(await pc1.createOffer()); 356 pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event in this test")); 357 await pc1.setRemoteDescription(await pc2.createOffer()); 358 await pc1.setLocalDescription(await pc1.createAnswer()); 359 assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']); 360 await new Promise(r => t.step_timeout(r, 0)); 361 }, "when using addTrack, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded"); 362 363 promise_test(async t => { 364 const pc1 = new RTCPeerConnection(); 365 const pc2 = new RTCPeerConnection(); 366 t.add_cleanup(() => pc1.close()); 367 t.add_cleanup(() => pc2.close()); 368 369 const stream = await getNoiseStream({audio: true}); 370 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 371 pc1.addTrack(stream.getTracks()[0], stream); 372 373 await pc1.setLocalDescription(await pc1.createOffer()); 374 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 375 376 await pc2.setLocalDescription(await pc2.createAnswer()); 377 await pc1.setRemoteDescription(pc2.localDescription); 378 379 // In stable state add video on both end and make sure video transceiver is not killed. 380 381 const stream1 = await getNoiseStream({video: true}); 382 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 383 pc1.addTrack(stream1.getTracks()[0], stream1); 384 await pc1.setLocalDescription(await pc1.createOffer()); 385 386 const stream2 = await getNoiseStream({video: true}); 387 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 388 pc2.addTrack(stream2.getTracks()[0], stream2); 389 const offer2 = await pc2.createOffer(); 390 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 391 await pc2.setRemoteDescription({type: "rollback"}); 392 assert_equals(pc2.getTransceivers().length, 2); 393 await pc2.setLocalDescription(offer2); 394 }, "rollback of a remote offer to negotiated stable state should enable " + 395 "applying of a local offer"); 396 397 promise_test(async t => { 398 const pc1 = new RTCPeerConnection(); 399 const pc2 = new RTCPeerConnection(); 400 t.add_cleanup(() => pc1.close()); 401 t.add_cleanup(() => pc2.close()); 402 403 const stream = await getNoiseStream({audio: true}); 404 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 405 pc1.addTrack(stream.getTracks()[0], stream); 406 407 await pc1.setLocalDescription(await pc1.createOffer()); 408 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 409 410 await pc2.setLocalDescription(await pc2.createAnswer()); 411 await pc1.setRemoteDescription(pc2.localDescription); 412 413 // Both ends want to add video at the same time. pc2 rolls back. 414 415 const stream2 = await getNoiseStream({video: true}); 416 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 417 pc2.addTrack(stream2.getTracks()[0], stream2); 418 await pc2.setLocalDescription(await pc2.createOffer()); 419 assert_equals(pc2.getTransceivers().length, 2); 420 assert_not_equals(pc2.getTransceivers()[1].sender.transport, null); 421 await pc2.setLocalDescription({type: "rollback"}); 422 assert_equals(pc2.getTransceivers().length, 2); 423 // Rollback didn't touch audio transceiver and transport is intact. 424 assert_not_equals(pc2.getTransceivers()[0].sender.transport, null); 425 // Video transport got killed. 426 assert_equals(pc2.getTransceivers()[1].sender.transport, null); 427 const stream1 = await getNoiseStream({video: true}); 428 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 429 pc1.addTrack(stream1.getTracks()[0], stream1); 430 await pc1.setLocalDescription(await pc1.createOffer()); 431 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 432 }, "rollback of a local offer to negotiated stable state should enable " + 433 "applying of a remote offer"); 434 435 promise_test(async t => { 436 const pc1 = new RTCPeerConnection(); 437 const pc2 = new RTCPeerConnection(); 438 t.add_cleanup(() => pc1.close()); 439 t.add_cleanup(() => pc2.close()); 440 441 const stream = await getNoiseStream({audio: true}); 442 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 443 pc1.addTrack(stream.getTracks()[0], stream); 444 445 await pc1.setLocalDescription(await pc1.createOffer()); 446 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 447 448 await pc2.setLocalDescription(await pc2.createAnswer()); 449 await pc1.setRemoteDescription(pc2.localDescription); 450 451 // pc1 adds video and pc2 adds audio. pc2 rolls back. 452 assert_equals(pc2.getTransceivers()[0].direction, "recvonly"); 453 const stream2 = await getNoiseStream({audio: true}); 454 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 455 pc2.addTrack(stream2.getTracks()[0], stream2); 456 assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); 457 await pc2.setLocalDescription(await pc2.createOffer()); 458 assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); 459 await pc2.setLocalDescription({type: "rollback"}); 460 assert_equals(pc2.getTransceivers().length, 1); 461 // setLocalDescription didn't change direction. So direction remains "sendrecv" 462 assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); 463 // Rollback didn't touch audio transceiver and transport is intact. Still can receive audio. 464 assert_not_equals(pc2.getTransceivers()[0].receiver.transport, null); 465 const stream1 = await getNoiseStream({video: true}); 466 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 467 pc1.addTrack(stream1.getTracks()[0], stream1); 468 await pc1.setLocalDescription(await pc1.createOffer()); 469 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 470 }, "rollback a local offer with audio direction change to negotiated " + 471 "stable state and then add video receiver"); 472 473 promise_test(async t => { 474 const pc1 = new RTCPeerConnection(); 475 const pc2 = new RTCPeerConnection(); 476 t.add_cleanup(() => pc1.close()); 477 t.add_cleanup(() => pc2.close()); 478 479 pc1.addTransceiver('video', {direction: 'sendonly'}); 480 pc2.addTransceiver('video', {direction: 'sendonly'}); 481 await pc1.setLocalDescription(await pc1.createOffer()); 482 const pc1FirstMid = pc1.getTransceivers()[0].mid; 483 await pc2.setLocalDescription(await pc2.createOffer()); 484 const pc2FirstMid = pc2.getTransceivers()[0].mid; 485 // I don't think it is mandated that this has to be true, but any implementation I know of would 486 // have predictable mids (e.g. 0, 1, 2...) so pc1 and pc2 should offer with the same mids. 487 assert_equals(pc1FirstMid, pc2FirstMid); 488 await pc1.setRemoteDescription(pc2.pendingLocalDescription); 489 // We've implicitly rolled back and the SRD caused a second transceiver to be created. 490 // As such, the first transceiver's mid will now be null, and the second transceiver's mid will 491 // match the remote offer. 492 assert_equals(pc1.getTransceivers().length, 2); 493 assert_equals(pc1.getTransceivers()[0].mid, null); 494 assert_equals(pc1.getTransceivers()[1].mid, pc2FirstMid); 495 // If we now do an offer the first transceiver will get a different mid than in the first 496 // pc1.createOffer()! 497 pc1.setLocalDescription(await pc1.createAnswer()); 498 await pc1.setLocalDescription(await pc1.createOffer()); 499 assert_not_equals(pc1.getTransceivers()[0].mid, pc1FirstMid); 500 }, "two transceivers with same mids"); 501 502 promise_test(async t => { 503 const pc1 = new RTCPeerConnection(); 504 const pc2 = new RTCPeerConnection(); 505 t.add_cleanup(() => pc1.close()); 506 t.add_cleanup(() => pc2.close()); 507 const stream = await getNoiseStream({audio: true, video: true}); 508 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 509 const audio = stream.getAudioTracks()[0]; 510 pc1.addTrack(audio, stream); 511 const video = stream.getVideoTracks()[0]; 512 pc1.addTrack(video, stream); 513 514 let remoteStream = null; 515 pc2.ontrack = e => { remoteStream = e.streams[0]; } 516 await pc2.setRemoteDescription(await pc1.createOffer()); 517 assert_true(remoteStream != null); 518 let remoteTracks = remoteStream.getTracks(); 519 const removedTracks = []; 520 remoteStream.onremovetrack = e => { removedTracks.push(e.track.id); } 521 await pc2.setRemoteDescription({type: "rollback"}); 522 assert_equals(removedTracks.length, 2, 523 "Rollback should have removed two tracks"); 524 assert_true(removedTracks.includes(remoteTracks[0].id), 525 "First track should be removed"); 526 assert_true(removedTracks.includes(remoteTracks[1].id), 527 "Second track should be removed"); 528 529 }, "onremovetrack fires during remote rollback"); 530 531 promise_test(async t => { 532 const pc1 = new RTCPeerConnection(); 533 t.add_cleanup(() => pc1.close()); 534 const pc2 = new RTCPeerConnection(); 535 t.add_cleanup(() => pc2.close()); 536 537 const stream1 = await getNoiseStream({audio: true}); 538 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 539 pc1.addTrack(stream1.getTracks()[0], stream1); 540 541 const offer1 = await pc1.createOffer(); 542 543 const remoteStreams = []; 544 pc2.ontrack = e => { remoteStreams.push(e.streams[0]); } 545 546 await pc1.setLocalDescription(offer1); 547 await pc2.setRemoteDescription(pc1.pendingLocalDescription); 548 await pc2.setLocalDescription(await pc2.createAnswer()); 549 await pc1.setRemoteDescription(pc2.localDescription); 550 551 assert_equals(remoteStreams.length, 1, "Number of remote streams"); 552 assert_equals(remoteStreams[0].getTracks().length, 1, "Number of remote tracks"); 553 const track = remoteStreams[0].getTracks()[0]; 554 555 const stream2 = await getNoiseStream({audio: true}); 556 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 557 pc1.getTransceivers()[0].sender.setStreams(stream2); 558 559 const offer2 = await pc1.createOffer(); 560 await pc2.setRemoteDescription(offer2); 561 562 assert_equals(remoteStreams.length, 2); 563 assert_equals(remoteStreams[0].getTracks().length, 0); 564 assert_equals(remoteStreams[1].getTracks()[0].id, track.id); 565 await pc2.setRemoteDescription({type: "rollback"}); 566 assert_equals(remoteStreams.length, 3); 567 assert_equals(remoteStreams[0].id, remoteStreams[2].id); 568 assert_equals(remoteStreams[1].getTracks().length, 0); 569 assert_equals(remoteStreams[2].getTracks().length, 1); 570 assert_equals(remoteStreams[2].getTracks()[0].id, track.id); 571 572 }, "rollback of a remote offer with stream changes"); 573 574 promise_test(async t => { 575 const pc1 = new RTCPeerConnection(); 576 t.add_cleanup(() => pc1.close()); 577 const pc2 = new RTCPeerConnection(); 578 t.add_cleanup(() => pc2.close()); 579 pc2.addTransceiver('audio'); 580 const offer = await pc2.createOffer(); 581 await pc1.setRemoteDescription(offer); 582 const [transceiver] = pc1.getTransceivers(); 583 pc1.setRemoteDescription({type:'rollback'}); 584 pc1.removeTrack(transceiver.sender); 585 }, 'removeTrack() with a sender being rolled back does not crash or throw'); 586 587 promise_test(async t => { 588 const pc1 = new RTCPeerConnection(); 589 t.add_cleanup(() => pc1.close()); 590 const pc2 = new RTCPeerConnection(); 591 t.add_cleanup(() => pc2.close()); 592 pc1.addTransceiver('video'); 593 const channel = pc2.createDataChannel('dummy'); 594 await pc2.setLocalDescription(await pc2.createOffer()); 595 await pc2.setRemoteDescription(await pc1.createOffer()); 596 assert_equals(pc2.signalingState, 'have-remote-offer'); 597 await pc2.setLocalDescription(await pc2.createAnswer()); 598 await pc2.setLocalDescription(await pc2.createOffer()); 599 assert_equals(channel.readyState, 'connecting'); 600 }, 'Implicit rollback with only a datachannel works'); 601 602 </script>