RTCPeerConnection-onnegotiationneeded.html (24004B)
1 <!doctype html> 2 <meta charset="utf-8"> 3 <title>Test RTCPeerConnection.prototype.onnegotiationneeded</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 // generateOffer 15 // generateAnswer 16 // generateAudioReceiveOnlyOffer 17 // test_never_resolve 18 19 // Listen to the negotiationneeded event on a peer connection 20 // Returns a promise that resolves when the first event is fired. 21 // The resolve result is a dictionary with event and nextPromise, 22 // which resolves when the next negotiationneeded event is fired. 23 // This allow us to promisify the event listening and assert whether 24 // an event is fired or not by testing whether a promise is resolved. 25 function awaitNegotiation(pc) { 26 if(pc.onnegotiationneeded) { 27 throw new Error('connection is already attached with onnegotiationneeded event handler'); 28 } 29 30 function waitNextNegotiation() { 31 return new Promise(resolve => { 32 pc.onnegotiationneeded = event => { 33 const nextPromise = waitNextNegotiation(); 34 resolve({ nextPromise, event }); 35 } 36 }); 37 } 38 39 return waitNextNegotiation(); 40 } 41 42 // Return a promise that rejects if the first promise is resolved before second promise. 43 // Also rejects when either promise rejects. 44 function assert_first_promise_fulfill_after_second(promise1, promise2, message) { 45 if(!message) { 46 message = 'first promise is resolved before second promise'; 47 } 48 49 return new Promise((resolve, reject) => { 50 let secondResolved = false; 51 52 promise1.then(() => { 53 if(secondResolved) { 54 resolve(); 55 } else { 56 assert_unreached(message); 57 } 58 }) 59 .catch(reject); 60 61 promise2.then(() => { 62 secondResolved = true; 63 }, reject); 64 }); 65 } 66 67 /* 68 4.7.3. Updating the Negotiation-Needed flag 69 70 To update the negotiation-needed flag 71 5. Set connection's [[needNegotiation]] slot to true. 72 6. Queue a task that runs the following steps: 73 3. Fire a simple event named negotiationneeded at connection. 74 75 To check if negotiation is needed 76 2. If connection has created any RTCDataChannels, and no m= section has 77 been negotiated yet for data, return "true". 78 79 6.1. RTCPeerConnection Interface Extensions 80 81 createDataChannel 82 14. If channel was the first RTCDataChannel created on connection, 83 update the negotiation-needed flag for connection. 84 */ 85 promise_test(t => { 86 const pc = new RTCPeerConnection(); 87 t.add_cleanup(() => pc.close()); 88 const negotiated = awaitNegotiation(pc); 89 90 pc.createDataChannel('test'); 91 return negotiated; 92 }, 'Creating first data channel should fire negotiationneeded event'); 93 94 test_never_resolve(t => { 95 const pc = new RTCPeerConnection(); 96 const negotiated = awaitNegotiation(pc); 97 98 pc.createDataChannel('foo'); 99 return negotiated 100 .then(({nextPromise}) => { 101 pc.createDataChannel('bar'); 102 return nextPromise; 103 }); 104 }, 'calling createDataChannel twice should fire negotiationneeded event once'); 105 106 /* 107 4.7.3. Updating the Negotiation-Needed flag 108 To check if negotiation is needed 109 3. For each transceiver t in connection's set of transceivers, perform 110 the following checks: 111 1. If t isn't stopped and isn't yet associated with an m= section 112 according to [JSEP] (section 3.4.1.), return "true". 113 114 5.1. RTCPeerConnection Interface Extensions 115 addTransceiver 116 9. Update the negotiation-needed flag for connection. 117 */ 118 promise_test(t => { 119 const pc = new RTCPeerConnection(); 120 t.add_cleanup(() => pc.close()); 121 const negotiated = awaitNegotiation(pc); 122 123 pc.addTransceiver('audio'); 124 return negotiated; 125 }, 'addTransceiver() should fire negotiationneeded event'); 126 127 /* 128 4.7.3. Updating the Negotiation-Needed flag 129 To update the negotiation-needed flag 130 4. If connection's [[needNegotiation]] slot is already true, abort these steps. 131 */ 132 test_never_resolve(t => { 133 const pc = new RTCPeerConnection(); 134 const negotiated = awaitNegotiation(pc); 135 136 pc.addTransceiver('audio'); 137 return negotiated 138 .then(({nextPromise}) => { 139 pc.addTransceiver('video'); 140 return nextPromise; 141 }); 142 }, 'Calling addTransceiver() twice should fire negotiationneeded event once'); 143 144 /* 145 4.7.3. Updating the Negotiation-Needed flag 146 To update the negotiation-needed flag 147 4. If connection's [[needNegotiation]] slot is already true, abort these steps. 148 */ 149 test_never_resolve(t => { 150 const pc = new RTCPeerConnection(); 151 const negotiated = awaitNegotiation(pc); 152 153 pc.createDataChannel('test'); 154 return negotiated 155 .then(({nextPromise}) => { 156 pc.addTransceiver('video'); 157 return nextPromise; 158 }); 159 }, 'Calling both addTransceiver() and createDataChannel() should fire negotiationneeded event once'); 160 161 /* 162 4.7.3. Updating the Negotiation-Needed flag 163 To update the negotiation-needed flag 164 2. If connection's signaling state is not "stable", abort these steps. 165 */ 166 test_never_resolve(t => { 167 const pc = new RTCPeerConnection(); 168 let negotiated; 169 170 return generateAudioReceiveOnlyOffer(pc) 171 .then(offer => { 172 pc.setLocalDescription(offer); 173 negotiated = awaitNegotiation(pc); 174 }) 175 .then(() => negotiated) 176 .then(({nextPromise}) => { 177 assert_equals(pc.signalingState, 'have-local-offer'); 178 pc.createDataChannel('test'); 179 return nextPromise; 180 }); 181 }, 'negotiationneeded event should not fire if signaling state is not stable'); 182 183 /* 184 4.4.1.6. Set the RTCSessionSessionDescription 185 2.2.10. If connection's signaling state is now stable, update the negotiation-needed 186 flag. If connection's [[NegotiationNeeded]] slot was true both before and after 187 this update, queue a task that runs the following steps: 188 2. If connection's [[NegotiationNeeded]] slot is false, abort these steps. 189 3. Fire a simple event named negotiationneeded at connection. 190 */ 191 promise_test(async t => { 192 const pc = new RTCPeerConnection(); 193 194 t.add_cleanup(() => pc.close()); 195 196 pc.addTransceiver('audio'); 197 await new Promise(resolve => pc.onnegotiationneeded = resolve); 198 199 const offer = await pc.createOffer(); 200 await pc.setLocalDescription(offer); 201 let fired = false; 202 pc.onnegotiationneeded = e => fired = true; 203 pc.createDataChannel('test'); 204 await pc.setRemoteDescription(await generateAnswer(offer)); 205 await undefined; 206 assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SRD success"); 207 await new Promise(resolve => pc.onnegotiationneeded = resolve); 208 }, 'negotiationneeded event should fire only after signaling state goes back to stable after setRemoteDescription'); 209 210 promise_test(async t => { 211 const pc = new RTCPeerConnection(); 212 t.add_cleanup(() => pc.close()); 213 214 pc.addTransceiver('audio'); 215 await new Promise(resolve => pc.onnegotiationneeded = resolve); 216 217 let fired = false; 218 pc.onnegotiationneeded = e => fired = true; 219 await pc.setRemoteDescription(await generateOffer()); 220 pc.createDataChannel('test'); 221 await pc.setLocalDescription(await pc.createAnswer()); 222 await undefined; 223 assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SLD success"); 224 225 await new Promise(resolve => pc.onnegotiationneeded = resolve); 226 }, 'negotiationneeded event should fire only after signaling state goes back to stable after setLocalDescription'); 227 228 promise_test(async t => { 229 const pc = new RTCPeerConnection(); 230 231 t.add_cleanup(() => pc.close()); 232 233 pc.addTransceiver('audio'); 234 await new Promise(resolve => pc.onnegotiationneeded = resolve); 235 236 const offer = await pc.createOffer(); 237 await pc.setLocalDescription(offer); 238 let fired = false; 239 pc.onnegotiationneeded = e => fired = true; 240 pc.createDataChannel('test'); 241 const p = pc.setRemoteDescription(await generateAnswer(offer)); 242 await new Promise(resolve => pc.onsignalingstatechange = resolve); 243 assert_false(fired, "negotiationneeded should not fire before signalingstatechange fires"); 244 await new Promise(resolve => pc.onnegotiationneeded = resolve); 245 await p; 246 }, 'negotiationneeded event should fire only after signalingstatechange event fires from setRemoteDescription'); 247 248 promise_test(async t => { 249 const pc = new RTCPeerConnection(); 250 t.add_cleanup(() => pc.close()); 251 252 pc.addTransceiver('audio'); 253 await new Promise(resolve => pc.onnegotiationneeded = resolve); 254 255 let fired = false; 256 pc.onnegotiationneeded = e => fired = true; 257 await pc.setRemoteDescription(await generateOffer()); 258 pc.createDataChannel('test'); 259 260 const p = pc.setLocalDescription(await pc.createAnswer()); 261 await new Promise(resolve => pc.onsignalingstatechange = resolve); 262 assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after returning to stable"); 263 await new Promise(resolve => pc.onnegotiationneeded = resolve); 264 await p; 265 }, 'negotiationneeded event should fire only after signalingstatechange event fires from setLocalDescription'); 266 267 /* 268 5.1. RTCPeerConnection Interface Extensions 269 270 addTrack 271 10. Update the negotiation-needed flag for connection. 272 */ 273 promise_test(async t => { 274 const pc = new RTCPeerConnection(); 275 t.add_cleanup(() => pc.close()); 276 277 const stream = await getNoiseStream({ audio: true }); 278 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 279 const [track] = stream.getTracks(); 280 pc.addTrack(track, stream); 281 282 await new Promise(resolve => pc.onnegotiationneeded = resolve); 283 }, 'addTrack should cause negotiationneeded to fire'); 284 285 /* 286 5.1. RTCPeerConnection Interface Extensions 287 288 removeTrack 289 12. Update the negotiation-needed flag for connection. 290 */ 291 promise_test(async t => { 292 const pc = new RTCPeerConnection(); 293 t.add_cleanup(() => pc.close()); 294 295 const stream = await getNoiseStream({ audio: true }); 296 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 297 const [track] = stream.getTracks(); 298 const sender = pc.addTrack(track, stream); 299 300 await new Promise(resolve => pc.onnegotiationneeded = resolve); 301 pc.onnegotiationneeded = t.step_func(() => { 302 assert_unreached('onnegotiationneeded misfired'); 303 }); 304 305 const offer = await pc.createOffer(); 306 await pc.setLocalDescription(offer); 307 308 const answer = await generateAnswer(offer); 309 await pc.setRemoteDescription(answer); 310 311 pc.removeTrack(sender); 312 await new Promise(resolve => pc.onnegotiationneeded = resolve) 313 }, 'removeTrack should cause negotiationneeded to fire on the caller'); 314 315 /* 316 5.1. RTCPeerConnection Interface Extensions 317 318 removeTrack 319 12. Update the negotiation-needed flag for connection. 320 */ 321 promise_test(async t => { 322 const caller = new RTCPeerConnection(); 323 t.add_cleanup(() => caller.close()); 324 caller.addTransceiver('audio', {direction:'recvonly'}); 325 const offer = await caller.createOffer(); 326 327 const callee = new RTCPeerConnection(); 328 t.add_cleanup(() => callee.close()); 329 330 const stream = await getNoiseStream({ audio: true }); 331 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 332 const [track] = stream.getTracks(); 333 const sender = callee.addTrack(track, stream); 334 335 await new Promise(resolve => callee.onnegotiationneeded = resolve); 336 callee.onnegotiationneeded = t.step_func(() => { 337 assert_unreached('onnegotiationneeded misfired'); 338 }); 339 340 await callee.setRemoteDescription(offer); 341 const answer = await callee.createAnswer(); 342 callee.setLocalDescription(answer); 343 344 callee.removeTrack(sender); 345 await new Promise(resolve => callee.onnegotiationneeded = resolve) 346 }, 'removeTrack should cause negotiationneeded to fire on the callee'); 347 348 /* 349 5.4. RTCRtpTransceiver Interface 350 351 setDirection 352 7. Update the negotiation-needed flag for connection. 353 */ 354 promise_test(async t => { 355 const pc = new RTCPeerConnection(); 356 t.add_cleanup(() => pc.close()); 357 358 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 359 const offer = await pc.createOffer(); 360 await pc.setLocalDescription(offer); 361 const answer = await generateAnswer(offer); 362 await pc.setRemoteDescription(answer); 363 transceiver.direction = 'recvonly'; 364 await new Promise(resolve => pc.onnegotiationneeded = resolve); 365 }, 'Updating the direction of the transceiver should cause negotiationneeded to fire'); 366 367 /* 368 5.2. RTCRtpSender Interface 369 370 setStreams 371 7. Update the negotiation-needed flag for connection. 372 */ 373 promise_test(async t => { 374 const pc = new RTCPeerConnection(); 375 t.add_cleanup(() => pc.close()); 376 377 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 378 const offer = await pc.createOffer(); 379 await pc.setLocalDescription(offer); 380 const answer = await generateAnswer(offer); 381 await pc.setRemoteDescription(answer); 382 383 const stream = new MediaStream(); 384 transceiver.sender.setStreams(stream); 385 await new Promise(resolve => pc.onnegotiationneeded = resolve); 386 }, 'Calling setStreams should cause negotiationneeded to fire'); 387 388 promise_test(async t => { 389 const pc = new RTCPeerConnection(); 390 t.add_cleanup(() => pc.close()); 391 392 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 393 const stream = new MediaStream(); 394 transceiver.sender.setStreams(stream); 395 await new Promise(resolve => pc.onnegotiationneeded = resolve); 396 397 const offer = await pc.createOffer(); 398 await pc.setLocalDescription(offer); 399 const answer = await generateAnswer(offer); 400 await pc.setRemoteDescription(answer); 401 402 const stream2 = new MediaStream(); 403 transceiver.sender.setStreams(stream2); 404 await new Promise(resolve => pc.onnegotiationneeded = resolve); 405 }, 'Calling setStreams with a different stream as before should cause negotiationneeded to fire'); 406 407 promise_test(async t => { 408 const pc = new RTCPeerConnection(); 409 t.add_cleanup(() => pc.close()); 410 411 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 412 const stream = new MediaStream(); 413 transceiver.sender.setStreams(stream); 414 await new Promise(resolve => pc.onnegotiationneeded = resolve); 415 416 const offer = await pc.createOffer(); 417 await pc.setLocalDescription(offer); 418 const answer = await generateAnswer(offer); 419 await pc.setRemoteDescription(answer); 420 421 const stream2 = new MediaStream(); 422 transceiver.sender.setStreams(stream, stream2); 423 await new Promise(resolve => pc.onnegotiationneeded = resolve); 424 }, 'Calling setStreams with an additional stream should cause negotiationneeded to fire'); 425 426 promise_test(async t => { 427 const pc = new RTCPeerConnection(); 428 t.add_cleanup(() => pc.close()); 429 430 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 431 const stream1 = new MediaStream(); 432 const stream2 = new MediaStream(); 433 transceiver.sender.setStreams(stream1, stream2); 434 await new Promise(resolve => pc.onnegotiationneeded = resolve); 435 436 const offer = await pc.createOffer(); 437 await pc.setLocalDescription(offer); 438 const answer = await generateAnswer(offer); 439 await pc.setRemoteDescription(answer); 440 441 transceiver.sender.setStreams(stream2); 442 await new Promise(resolve => pc.onnegotiationneeded = resolve); 443 }, 'Calling setStreams with a stream removed should cause negotiationneeded to fire'); 444 445 promise_test(async t => { 446 const pc = new RTCPeerConnection(); 447 t.add_cleanup(() => pc.close()); 448 449 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 450 const stream1 = new MediaStream(); 451 const stream2 = new MediaStream(); 452 transceiver.sender.setStreams(stream1, stream2); 453 await new Promise(resolve => pc.onnegotiationneeded = resolve); 454 455 const offer = await pc.createOffer(); 456 await pc.setLocalDescription(offer); 457 const answer = await generateAnswer(offer); 458 await pc.setRemoteDescription(answer); 459 460 transceiver.sender.setStreams(); 461 await new Promise(resolve => pc.onnegotiationneeded = resolve); 462 }, 'Calling setStreams with all streams removed should cause negotiationneeded to fire'); 463 464 promise_test(async t => { 465 const pc = new RTCPeerConnection(); 466 t.add_cleanup(() => pc.close()); 467 468 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 469 const stream = new MediaStream(); 470 transceiver.sender.setStreams(stream); 471 await new Promise(resolve => pc.onnegotiationneeded = resolve); 472 473 const offer = await pc.createOffer(); 474 await pc.setLocalDescription(offer); 475 const answer = await generateAnswer(offer); 476 await pc.setRemoteDescription(answer); 477 478 transceiver.sender.setStreams(stream); 479 const event = await Promise.race([ 480 new Promise(r => pc.onnegotiationneeded = r), 481 new Promise(r => t.step_timeout(r, 10)) 482 ]); 483 assert_equals(event, undefined, "No negotiationneeded event"); 484 }, 'Calling setStreams with the same stream as before should not cause negotiationneeded to fire'); 485 486 promise_test(async t => { 487 const pc = new RTCPeerConnection(); 488 t.add_cleanup(() => pc.close()); 489 490 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 491 const stream = new MediaStream(); 492 transceiver.sender.setStreams(stream); 493 await new Promise(resolve => pc.onnegotiationneeded = resolve); 494 495 const offer = await pc.createOffer(); 496 await pc.setLocalDescription(offer); 497 const answer = await generateAnswer(offer); 498 await pc.setRemoteDescription(answer); 499 500 transceiver.sender.setStreams(stream, stream); 501 const event = await Promise.race([ 502 new Promise(r => pc.onnegotiationneeded = r), 503 new Promise(r => t.step_timeout(r, 10)) 504 ]); 505 assert_equals(event, undefined, "No negotiationneeded event"); 506 }, 'Calling setStreams with duplicates of the same stream as before should not cause negotiationneeded to fire'); 507 508 promise_test(async t => { 509 const pc = new RTCPeerConnection(); 510 t.add_cleanup(() => pc.close()); 511 512 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 513 const stream1 = new MediaStream(); 514 const stream2 = new MediaStream(); 515 transceiver.sender.setStreams(stream1, stream2); 516 await new Promise(resolve => pc.onnegotiationneeded = resolve); 517 518 const offer = await pc.createOffer(); 519 await pc.setLocalDescription(offer); 520 const answer = await generateAnswer(offer); 521 await pc.setRemoteDescription(answer); 522 523 transceiver.sender.setStreams(stream2, stream1); 524 const event = await Promise.race([ 525 new Promise(r => pc.onnegotiationneeded = r), 526 new Promise(r => t.step_timeout(r, 10)) 527 ]); 528 assert_equals(event, undefined, "No negotiationneeded event"); 529 }, 'Calling setStreams with the same streams as before in a different order should not cause negotiationneeded to fire'); 530 531 promise_test(async t => { 532 const pc = new RTCPeerConnection(); 533 t.add_cleanup(() => pc.close()); 534 535 const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'}); 536 const stream1 = new MediaStream(); 537 const stream2 = new MediaStream(); 538 transceiver.sender.setStreams(stream1, stream2); 539 await new Promise(resolve => pc.onnegotiationneeded = resolve); 540 541 const offer = await pc.createOffer(); 542 await pc.setLocalDescription(offer); 543 const answer = await generateAnswer(offer); 544 await pc.setRemoteDescription(answer); 545 546 transceiver.sender.setStreams(stream1, stream2, stream1); 547 const event = await Promise.race([ 548 new Promise(r => pc.onnegotiationneeded = r), 549 new Promise(r => t.step_timeout(r, 10)) 550 ]); 551 assert_equals(event, undefined, "No negotiationneeded event"); 552 }, 'Calling setStreams with duplicates of the same streams as before should not cause negotiationneeded to fire'); 553 554 promise_test(async t => { 555 const pc1 = new RTCPeerConnection(); 556 t.add_cleanup(() => pc1.close()); 557 const pc2 = new RTCPeerConnection(); 558 t.add_cleanup(() => pc2.close()); 559 560 let negotiationCount = 0; 561 pc1.onnegotiationneeded = async () => { 562 negotiationCount++; 563 await pc1.setLocalDescription(await pc1.createOffer()); 564 await pc2.setRemoteDescription(pc1.localDescription); 565 await pc2.setLocalDescription(await pc2.createAnswer()); 566 await pc1.setRemoteDescription(pc2.localDescription); 567 } 568 569 pc1.addTransceiver("video"); 570 await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r()); 571 pc1.addTransceiver("audio"); 572 await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r()); 573 assert_equals(negotiationCount, 2); 574 }, 'Adding two transceivers, one at a time, results in the expected number of negotiationneeded events'); 575 576 /* 577 TODO 578 4.7.3. Updating the Negotiation-Needed flag 579 580 To update the negotiation-needed flag 581 3. If the result of checking if negotiation is needed is "false", 582 clear the negotiation-needed flag by setting connection's 583 [[needNegotiation]] slot to false, and abort these steps. 584 6. Queue a task that runs the following steps: 585 2. If connection's [[needNegotiation]] slot is false, abort these steps. 586 587 To check if negotiation is needed 588 3. For each transceiver t in connection's set of transceivers, perform 589 the following checks: 590 2. If t isn't stopped and is associated with an m= section according 591 to [JSEP] (section 3.4.1.), then perform the following checks: 592 1. If t's direction is "sendrecv" or "sendonly", and the 593 associated m= section in connection's currentLocalDescription 594 doesn't contain an "a=msid" line, return "true". 595 2. If connection's currentLocalDescription if of type "offer", 596 and the direction of the associated m= section in neither the 597 offer nor answer matches t's direction, return "true". 598 3. If connection's currentLocalDescription if of type "answer", 599 and the direction of the associated m= section in the answer 600 does not match t's direction intersected with the offered 601 direction (as described in [JSEP] (section 5.3.1.)), 602 return "true". 603 3. If t is stopped and is associated with an m= section according 604 to [JSEP] (section 3.4.1.), but the associated m= section is 605 not yet rejected in connection's currentLocalDescription or 606 currentRemoteDescription , return "true". 607 4. If all the preceding checks were performed and "true" was not returned, 608 nothing remains to be negotiated; return "false". 609 610 4.3.1. RTCPeerConnection Operation 611 612 When the RTCPeerConnection() constructor is invoked 613 7. Let connection have a [[needNegotiation]] internal slot, initialized to false. 614 615 5.4. RTCRtpTransceiver Interface 616 617 stop 618 11. Update the negotiation-needed flag for connection. 619 620 Untestable 621 4.7.3. Updating the Negotiation-Needed flag 622 1. If connection's [[isClosed]] slot is true, abort these steps. 623 6. Queue a task that runs the following steps: 624 1. If connection's [[isClosed]] slot is true, abort these steps. 625 */ 626 627 </script>