1 <html> 2 <head> 3 <script type="text/javascript" src="webrtc_test_utilities.js"></script> 4 <script type="text/javascript" src="webrtc_test_audio.js"></script> 5 <script type="text/javascript"> 6 $ = function(id) { 7 return document.getElementById(id); 8 }; 9 10 var gFirstConnection = null; 11 var gSecondConnection = null; 12 var gTestWithoutMsid = false; 13 var gLocalStream = null; 14 var gSentTones = ''; 15 16 var gRemoteStreams = {}; 17 18 // Default transform functions, overridden by some test cases. 19 var transformSdp = function(sdp) { return sdp; }; 20 var transformRemoteSdp = function(sdp) { return sdp; }; 21 var onLocalDescriptionError = function(error) { failTest(error); }; 22 var onRemoteDescriptionError = function(error) { failTest(error); }; 23 24 // Temporary measure to be able to force iSAC 16K where needed, particularly 25 // on Android. This applies to every test which is why it's implemented like 26 // this. 27 var maybeForceIsac16K = function(sdp) { return sdp; }; 28 function forceIsac16KInSdp() { 29 maybeForceIsac16K = function(sdp) { 30 if (sdp.search('m=audio') == -1) 31 return sdp; 32 33 sdp = sdp.replace(/m=audio (\d+) RTP\/SAVPF.*\r\n/g, 34 'm=audio $1 RTP/SAVPF 103 126\r\n'); 35 sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:103 minptime=10'); 36 if (sdp.search('a=rtpmap:103 ISAC/16000') == -1) 37 failTest('Missing iSAC 16K codec on Android; cannot force codec.'); 38 39 return sdp; 40 }; 41 sendValueToTest('isac-forced'); 42 } 43 44 // When using external SDES, the crypto key is chosen by javascript. 45 var EXTERNAL_SDES_LINES = { 46 'audio': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + 47 'inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR', 48 'video': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + 49 'inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj', 50 'data': 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' + 51 'inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj' 52 }; 53 54 setAllEventsOccuredHandler(reportTestSuccess); 55 56 // Test that we can setup call with an audio and video track. 57 function call(constraints) { 58 createConnections(null); 59 navigator.webkitGetUserMedia(constraints, 60 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 61 waitForVideo('remote-view-1'); 62 waitForVideo('remote-view-2'); 63 } 64 65 // Test that we can setup a call with a video track and that the remote peer 66 // receives black frames if the local video track is disabled. 67 function callAndDisableLocalVideo(constraints) { 68 createConnections(null); 69 navigator.webkitGetUserMedia(constraints, 70 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 71 detectVideoPlaying('remote-view-1', 72 function () { 73 assertEquals(gLocalStream.getVideoTracks().length, 1); 74 gLocalStream.getVideoTracks()[0].enabled = false; 75 waitForBlackVideo('remote-view-1'); 76 }); 77 } 78 79 // Test that we can setup call with an audio and video track and check that 80 // the video resolution is as expected. 81 function callAndExpectResolution(constraints, 82 expected_width, 83 expected_height) { 84 createConnections(null); 85 navigator.webkitGetUserMedia(constraints, 86 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 87 waitForVideoWithResolution('remote-view-1', 88 expected_width, 89 expected_height); 90 waitForVideoWithResolution('remote-view-2', 91 expected_width, 92 expected_height); 93 } 94 95 96 // First calls without streams on any connections, and then adds a stream 97 // to peer connection 1 which gets sent to peer connection 2. We must wait 98 // for the first negotiation to complete before starting the second one, which 99 // is why we wait until the connection is stable before re-negotiating. 100 function callEmptyThenAddOneStreamAndRenegotiate(constraints) { 101 createConnections(null); 102 negotiate(); 103 waitForConnectionToStabilize(gFirstConnection, function() { 104 navigator.webkitGetUserMedia(constraints, 105 addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError); 106 // Only the first connection is sending here. 107 waitForVideo('remote-view-2'); 108 }); 109 } 110 111 // First makes a call between pc1 and pc2, and then makes a call between pc3 112 // and pc4. The stream sent from pc3 to pc4 is the stream received on pc1. 113 // The stream sent from pc4 to pc3 is cloned from the stream received on pc2 114 // to test that cloning of remote video tracks works as intended. 115 function callAndForwardRemoteStream(constraints) { 116 createConnections(null); 117 navigator.webkitGetUserMedia(constraints, 118 addStreamToBothConnectionsAndNegotiate, 119 printGetUserMediaError); 120 var gotRemoteStream1 = false; 121 var gotRemoteStream2 = false; 122 123 var onRemoteStream1 = function() { 124 gotRemoteStream1 = true; 125 maybeCallEstablished(); 126 } 127 128 var onRemoteStream2 = function() { 129 gotRemoteStream2 = true; 130 maybeCallEstablished(); 131 } 132 133 var maybeCallEstablished = function() { 134 if (gotRemoteStream1 && gotRemoteStream2) { 135 onCallEstablished(); 136 } 137 } 138 139 var onCallEstablished = function() { 140 thirdConnection = createConnection(null, 'remote-view-3'); 141 thirdConnection.addStream(gRemoteStreams['remote-view-1']); 142 143 fourthConnection = createConnection(null, 'remote-view-4'); 144 fourthConnection.addStream(gRemoteStreams['remote-view-2'].clone()); 145 146 negotiateBetween(thirdConnection, fourthConnection); 147 148 waitForVideo('remote-view-3'); 149 waitForVideo('remote-view-4'); 150 } 151 152 // Do the forwarding after we have received video. 153 detectVideoPlaying('remote-view-1', onRemoteStream1); 154 detectVideoPlaying('remote-view-2', onRemoteStream2); 155 } 156 157 // First makes a call between pc1 and pc2, and then construct a new media 158 // stream using the remote audio and video tracks, connect the new media 159 // stream to a video element. These operations should not crash Chrome. 160 function ConnectChromiumSinkToRemoteAudioTrack() { 161 createConnections(null); 162 navigator.webkitGetUserMedia({audio: true, video: true}, 163 addStreamToBothConnectionsAndNegotiate, 164 printGetUserMediaError); 165 166 detectVideoPlaying('remote-view-2', function() { 167 // Construct a new media stream with remote tracks. 168 var newStream = new webkitMediaStream(); 169 newStream.addTrack( 170 gSecondConnection.getRemoteStreams()[0].getAudioTracks()[0]); 171 newStream.addTrack( 172 gSecondConnection.getRemoteStreams()[0].getVideoTracks()[0]); 173 var videoElement = document.createElement('video'); 174 175 // No crash for this operation. 176 videoElement.src = URL.createObjectURL(newStream); 177 waitForVideo('remote-view-2'); 178 }); 179 } 180 181 // Test that we can setup call with an audio and video track and 182 // simulate that the remote peer don't support MSID. 183 function callWithoutMsidAndBundle() { 184 createConnections(null); 185 transformSdp = removeBundle; 186 transformRemoteSdp = removeMsid; 187 gTestWithoutMsid = true; 188 navigator.webkitGetUserMedia({audio: true, video: true}, 189 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 190 waitForVideo('remote-view-1'); 191 waitForVideo('remote-view-2'); 192 } 193 194 // Test that we can't setup a call with an unsupported video codec 195 function negotiateUnsupportedVideoCodec() { 196 createConnections(null); 197 transformSdp = removeVideoCodec; 198 199 onLocalDescriptionError = function(error) { 200 var expectedMsg = 'Failed to set local offer sdp:' + 201 ' Session error code: ERROR_CONTENT. Session error description:' + 202 ' Failed to set video receive codecs..'; 203 assertEquals(expectedMsg, error); 204 reportTestSuccess(); 205 }; 206 navigator.webkitGetUserMedia({audio: true, video: true}, 207 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 208 } 209 210 // Test that we can't setup a call if one peer does not support encryption 211 function negotiateNonCryptoCall() { 212 createConnections(null); 213 transformSdp = removeCrypto; 214 onLocalDescriptionError = function(error) { 215 var expectedMsg = 'Failed to set local offer sdp:' + 216 ' Called with SDP without DTLS fingerprint.'; 217 218 assertEquals(expectedMsg, error); 219 reportTestSuccess(); 220 }; 221 navigator.webkitGetUserMedia({audio: true, video: true}, 222 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 223 } 224 225 // Test that we can negotiate a call with an SDP offer that includes a 226 // b=AS:XX line to control audio and video bandwidth 227 function negotiateOfferWithBLine() { 228 createConnections(null); 229 transformSdp = addBandwithControl; 230 navigator.webkitGetUserMedia({audio: true, video: true}, 231 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 232 waitForVideo('remote-view-1'); 233 waitForVideo('remote-view-2'); 234 } 235 236 // Test that we can setup call with legacy settings. 237 function callWithLegacySdp() { 238 transformSdp = function(sdp) { 239 return removeBundle(useGice(useExternalSdes(sdp))); 240 }; 241 createConnections({ 242 'mandatory': {'RtpDataChannels': true, 'DtlsSrtpKeyAgreement': false} 243 }); 244 setupDataChannel({reliable: false}); 245 navigator.webkitGetUserMedia({audio: true, video: true}, 246 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 247 waitForVideo('remote-view-1'); 248 waitForVideo('remote-view-2'); 249 } 250 251 // Test only a data channel. 252 function callWithDataOnly() { 253 createConnections({optional:[{RtpDataChannels: true}]}); 254 setupDataChannel({reliable: false}); 255 negotiate(); 256 } 257 258 function callWithSctpDataOnly() { 259 createConnections({optional: [{DtlsSrtpKeyAgreement: true}]}); 260 setupSctpDataChannel({reliable: true}); 261 negotiate(); 262 } 263 264 // Test call with audio, video and a data channel. 265 function callWithDataAndMedia() { 266 createConnections({optional:[{RtpDataChannels: true}]}); 267 setupDataChannel({reliable: false}); 268 navigator.webkitGetUserMedia({audio: true, video: true}, 269 addStreamToBothConnectionsAndNegotiate, 270 printGetUserMediaError); 271 waitForVideo('remote-view-1'); 272 waitForVideo('remote-view-2'); 273 } 274 275 function callWithSctpDataAndMedia() { 276 createConnections({optional: [{DtlsSrtpKeyAgreement: true}]}); 277 setupSctpDataChannel({reliable: true}); 278 navigator.webkitGetUserMedia({audio: true, video: true}, 279 addStreamToBothConnectionsAndNegotiate, 280 printGetUserMediaError); 281 waitForVideo('remote-view-1'); 282 waitForVideo('remote-view-2'); 283 } 284 285 286 // Test call with a data channel and later add audio and video. 287 function callWithDataAndLaterAddMedia() { 288 createConnections({optional:[{RtpDataChannels: true}]}); 289 setupDataChannel({reliable: false}); 290 negotiate(); 291 292 // Set an event handler for when the data channel has been closed. 293 setAllEventsOccuredHandler(function() { 294 // When the video is flowing the test is done. 295 setAllEventsOccuredHandler(reportTestSuccess); 296 navigator.webkitGetUserMedia({audio: true, video: true}, 297 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 298 waitForVideo('remote-view-1'); 299 waitForVideo('remote-view-2'); 300 }); 301 } 302 303 // Test that we can setup call and send DTMF. 304 function callAndSendDtmf(tones) { 305 createConnections(null); 306 navigator.webkitGetUserMedia({audio: true, video: true}, 307 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 308 var onCallEstablished = function() { 309 // Send DTMF tones. 310 var localAudioTrack = gLocalStream.getAudioTracks()[0]; 311 var dtmfSender = gFirstConnection.createDTMFSender(localAudioTrack); 312 dtmfSender.ontonechange = onToneChange; 313 dtmfSender.insertDTMF(tones); 314 // Wait for the DTMF tones callback. 315 addExpectedEvent(); 316 var waitDtmf = setInterval(function() { 317 if (gSentTones == tones) { 318 clearInterval(waitDtmf); 319 eventOccured(); 320 } 321 }, 100); 322 } 323 324 // Do the DTMF test after we have received video. 325 detectVideoPlaying('remote-view-2', onCallEstablished); 326 } 327 328 function testCreateOfferOptions() { 329 createConnections(null); 330 var offerOptions = { 331 'offerToReceiveAudio': false, 332 'offerToReceiveVideo': true 333 }; 334 335 gFirstConnection.createOffer( 336 function(offer) { 337 assertEquals(-1, offer.sdp.search('m=audio')); 338 assertNotEquals(-1, offer.sdp.search('m=video')); 339 340 reportTestSuccess(); 341 }, 342 function(error) { failTest(error); }, 343 offerOptions); 344 } 345 346 function callAndEnsureAudioIsPlaying(beLenient, constraints) { 347 createConnections(null); 348 349 // Add the local stream to gFirstConnection to play one-way audio. 350 navigator.webkitGetUserMedia(constraints, 351 addStreamToTheFirstConnectionAndNegotiate, printGetUserMediaError); 352 353 var onCallEstablished = function() { 354 ensureAudioPlaying(gSecondConnection, beLenient); 355 }; 356 357 waitForConnectionToStabilize(gFirstConnection, onCallEstablished); 358 } 359 360 function enableRemoteVideo(peerConnection, enabled) { 361 remoteStream = peerConnection.getRemoteStreams()[0]; 362 remoteStream.getVideoTracks()[0].enabled = enabled; 363 } 364 365 function enableRemoteAudio(peerConnection, enabled) { 366 remoteStream = peerConnection.getRemoteStreams()[0]; 367 remoteStream.getAudioTracks()[0].enabled = enabled; 368 } 369 370 function enableLocalVideo(peerConnection, enabled) { 371 localStream = peerConnection.getLocalStreams()[0]; 372 localStream.getVideoTracks()[0].enabled = enabled; 373 } 374 375 function enableLocalAudio(peerConnection, enabled) { 376 localStream = peerConnection.getLocalStreams()[0]; 377 localStream.getAudioTracks()[0].enabled = enabled; 378 } 379 380 function callAndEnsureRemoteAudioTrackMutingWorks(beLenient) { 381 callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); 382 setAllEventsOccuredHandler(function() { 383 setAllEventsOccuredHandler(reportTestSuccess); 384 385 // Call is up, now mute the remote track and check we stop playing out 386 // audio (after a small delay, we don't expect it to happen instantly). 387 enableRemoteAudio(gSecondConnection, false); 388 ensureSilence(gSecondConnection); 389 }); 390 } 391 392 function callAndEnsureLocalAudioTrackMutingWorks(beLenient) { 393 callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); 394 setAllEventsOccuredHandler(function() { 395 setAllEventsOccuredHandler(reportTestSuccess); 396 397 // Call is up, now mute the local track of the sending side and ensure 398 // the receiving side stops receiving audio. 399 enableLocalAudio(gFirstConnection, false); 400 ensureSilence(gSecondConnection); 401 }); 402 } 403 404 function callAndEnsureAudioTrackUnmutingWorks(beLenient) { 405 callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); 406 setAllEventsOccuredHandler(function() { 407 setAllEventsOccuredHandler(reportTestSuccess); 408 409 // Mute, wait a while, unmute, verify audio gets back up. 410 // (Also, ensure video muting doesn't affect audio). 411 enableRemoteAudio(gSecondConnection, false); 412 enableRemoteVideo(gSecondConnection, false); 413 414 setTimeout(function() { 415 enableRemoteAudio(gSecondConnection, true); 416 }, 500); 417 418 setTimeout(function() { 419 ensureAudioPlaying(gSecondConnection, beLenient); 420 }, 1500); 421 }); 422 } 423 424 function callAndEnsureLocalVideoMutingDoesntMuteAudio(beLenient) { 425 callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); 426 setAllEventsOccuredHandler(function() { 427 setAllEventsOccuredHandler(reportTestSuccess); 428 enableLocalVideo(gFirstConnection, false); 429 ensureAudioPlaying(gSecondConnection, beLenient); 430 }); 431 } 432 433 function callAndEnsureRemoteVideoMutingDoesntMuteAudio(beLenient) { 434 callAndEnsureAudioIsPlaying(beLenient, {audio: true, video: true}); 435 setAllEventsOccuredHandler(function() { 436 setAllEventsOccuredHandler(reportTestSuccess); 437 enableRemoteVideo(gSecondConnection, false); 438 ensureAudioPlaying(gSecondConnection, beLenient); 439 }); 440 } 441 442 function callAndEnsureVideoTrackMutingWorks() { 443 createConnections(null); 444 navigator.webkitGetUserMedia({audio: true, video: true}, 445 addStreamToBothConnectionsAndNegotiate, printGetUserMediaError); 446 447 addExpectedEvent(); 448 detectVideoPlaying('remote-view-2', function() { 449 // Disable the receiver's remote media stream. Video should stop. 450 // (Also, ensure muting audio doesn't affect video). 451 enableRemoteVideo(gSecondConnection, false); 452 enableRemoteAudio(gSecondConnection, false); 453 454 detectVideoStopped('remote-view-2', function() { 455 // Video has stopped: unmute and succeed if it starts playing again. 456 enableRemoteVideo(gSecondConnection, true); 457 detectVideoPlaying('remote-view-2', eventOccured); 458 }) 459 }); 460 } 461 462 // Test call with a new Video MediaStream that has been created based on a 463 // stream generated by getUserMedia. 464 function callWithNewVideoMediaStream() { 465 createConnections(null); 466 navigator.webkitGetUserMedia({audio: true, video: true}, 467 createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); 468 waitForVideo('remote-view-1'); 469 waitForVideo('remote-view-2'); 470 } 471 472 // Test call with a new Video MediaStream that has been created based on a 473 // stream generated by getUserMedia. When Video is flowing, an audio track 474 // is added to the sent stream and the video track is removed. This 475 // is to test that adding and removing of remote tracks on an existing 476 // mediastream works. 477 function callWithNewVideoMediaStreamLaterSwitchToAudio() { 478 createConnections(null); 479 navigator.webkitGetUserMedia({audio: true, video: true}, 480 createNewVideoStreamAndAddToBothConnections, printGetUserMediaError); 481 482 waitForVideo('remote-view-1'); 483 waitForVideo('remote-view-2'); 484 485 // Set an event handler for when video is playing. 486 setAllEventsOccuredHandler(function() { 487 // Add an audio track to the local stream and remove the video track and 488 // then renegotiate. But first - setup the expectations. 489 local_stream = gFirstConnection.getLocalStreams()[0]; 490 491 remote_stream_1 = gFirstConnection.getRemoteStreams()[0]; 492 // Add an expected event that onaddtrack will be called on the remote 493 // mediastream received on gFirstConnection when the audio track is 494 // received. 495 addExpectedEvent(); 496 remote_stream_1.onaddtrack = function(){ 497 assertEquals(remote_stream_1.getAudioTracks()[0].id, 498 local_stream.getAudioTracks()[0].id); 499 eventOccured(); 500 } 501 502 // Add an expectation that the received video track is removed from 503 // gFirstConnection. 504 addExpectedEvent(); 505 remote_stream_1.onremovetrack = function() { 506 eventOccured(); 507 } 508 509 // Add an expected event that onaddtrack will be called on the remote 510 // mediastream received on gSecondConnection when the audio track is 511 // received. 512 remote_stream_2 = gSecondConnection.getRemoteStreams()[0]; 513 addExpectedEvent(); 514 remote_stream_2.onaddtrack = function() { 515 assertEquals(remote_stream_2.getAudioTracks()[0].id, 516 local_stream.getAudioTracks()[0].id); 517 eventOccured(); 518 } 519 520 // Add an expectation that the received video track is removed from 521 // gSecondConnection. 522 addExpectedEvent(); 523 remote_stream_2.onremovetrack = function() { 524 eventOccured(); 525 } 526 // When all the above events have occurred- the test pass. 527 setAllEventsOccuredHandler(reportTestSuccess); 528 529 local_stream.addTrack(gLocalStream.getAudioTracks()[0]); 530 local_stream.removeTrack(local_stream.getVideoTracks()[0]); 531 negotiate(); 532 }); 533 } 534 535 // This function is used for setting up a test that: 536 // 1. Creates a data channel on |gFirstConnection| and sends data to 537 // |gSecondConnection|. 538 // 2. When data is received on |gSecondConnection| a message 539 // is sent to |gFirstConnection|. 540 // 3. When data is received on |gFirstConnection|, the data 541 // channel is closed. The test passes when the state transition completes. 542 function setupDataChannel(params) { 543 var sendDataString = "send some text on a data channel." 544 firstDataChannel = gFirstConnection.createDataChannel( 545 "sendDataChannel", params); 546 assertEquals('connecting', firstDataChannel.readyState); 547 548 // When |firstDataChannel| transition to open state, send a text string. 549 firstDataChannel.onopen = function() { 550 assertEquals('open', firstDataChannel.readyState); 551 if (firstDataChannel.reliable) { 552 firstDataChannel.send(sendDataString); 553 } else { 554 sendDataRepeatedlyUntilClosed(firstDataChannel); 555 } 556 } 557 558 // When |firstDataChannel| receive a message, close the channel and 559 // initiate a new offer/answer exchange to complete the closure. 560 firstDataChannel.onmessage = function(event) { 561 assertEquals(event.data, sendDataString); 562 firstDataChannel.close(); 563 negotiate(); 564 } 565 566 // When |firstDataChannel| transition to closed state, the test pass. 567 addExpectedEvent(); 568 firstDataChannel.onclose = function() { 569 assertEquals('closed', firstDataChannel.readyState); 570 eventOccured(); 571 } 572 573 // Event handler for when |gSecondConnection| receive a new dataChannel. 574 gSecondConnection.ondatachannel = function (event) { 575 var secondDataChannel = event.channel; 576 577 // When |secondDataChannel| receive a message, send a message back. 578 secondDataChannel.onmessage = function(event) { 579 assertEquals(event.data, sendDataString); 580 console.log("gSecondConnection received data"); 581 if (secondDataChannel.reliable) { 582 // If we're reliable we will just send one message over the channel, 583 // and therefore channel one's message handler cannot have shut us 584 // down already. 585 assertEquals('open', secondDataChannel.readyState); 586 secondDataChannel.send(sendDataString); 587 } else { 588 // If unreliable, this could be one in a series of messages and it 589 // is possible we already replied (which will close our channel). 590 sendDataRepeatedlyUntilClosed(secondDataChannel); 591 } 592 } 593 } 594 595 // Sends |sendDataString| on |dataChannel| every 200ms as long as 596 // |dataChannel| is open. 597 function sendDataRepeatedlyUntilClosed(dataChannel) { 598 var sendTimer = setInterval(function() { 599 if (dataChannel.readyState == 'open') 600 dataChannel.send(sendDataString); 601 else 602 clearInterval(sendTimer); 603 }, 200); 604 } 605 } 606 607 // SCTP data channel setup is slightly different then RTP based 608 // channels. Due to a bug in libjingle, we can't send data immediately 609 // after channel becomes open. So for that reason in SCTP, 610 // we are sending data from second channel, when ondatachannel event is 611 // received. So data flow happens 2 -> 1 -> 2. 612 function setupSctpDataChannel(params) { 613 var sendDataString = "send some text on a data channel." 614 firstDataChannel = gFirstConnection.createDataChannel( 615 "sendDataChannel", params); 616 assertEquals('connecting', firstDataChannel.readyState); 617 618 // When |firstDataChannel| transition to open state, send a text string. 619 firstDataChannel.onopen = function() { 620 assertEquals('open', firstDataChannel.readyState); 621 } 622 623 // When |firstDataChannel| receive a message, send message back. 624 // initiate a new offer/answer exchange to complete the closure. 625 firstDataChannel.onmessage = function(event) { 626 assertEquals('open', firstDataChannel.readyState); 627 assertEquals(event.data, sendDataString); 628 firstDataChannel.send(sendDataString); 629 } 630 631 632 // Event handler for when |gSecondConnection| receive a new dataChannel. 633 gSecondConnection.ondatachannel = function (event) { 634 var secondDataChannel = event.channel; 635 secondDataChannel.onopen = function() { 636 secondDataChannel.send(sendDataString); 637 } 638 639 // When |secondDataChannel| receive a message, close the channel and 640 // initiate a new offer/answer exchange to complete the closure. 641 secondDataChannel.onmessage = function(event) { 642 assertEquals(event.data, sendDataString); 643 assertEquals('open', secondDataChannel.readyState); 644 secondDataChannel.close(); 645 negotiate(); 646 } 647 648 // When |secondDataChannel| transition to closed state, the test pass. 649 addExpectedEvent(); 650 secondDataChannel.onclose = function() { 651 assertEquals('closed', secondDataChannel.readyState); 652 eventOccured(); 653 } 654 } 655 } 656 657 // Test call with a stream that has been created by getUserMedia, clone 658 // the stream to a cloned stream, send them via the same peer connection. 659 function addTwoMediaStreamsToOneConnection() { 660 createConnections(null); 661 navigator.webkitGetUserMedia({audio: true, video: true}, 662 CloneStreamAndAddTwoStreamstoOneConnection, printGetUserMediaError); 663 } 664 665 function onToneChange(tone) { 666 gSentTones += tone.tone; 667 } 668 669 function createConnections(constraints) { 670 gFirstConnection = createConnection(constraints, 'remote-view-1'); 671 assertEquals('stable', gFirstConnection.signalingState); 672 673 gSecondConnection = createConnection(constraints, 'remote-view-2'); 674 assertEquals('stable', gSecondConnection.signalingState); 675 } 676 677 function createConnection(constraints, remoteView) { 678 var pc = new webkitRTCPeerConnection(null, constraints); 679 pc.onaddstream = function(event) { 680 onRemoteStream(event, remoteView); 681 } 682 return pc; 683 } 684 685 function displayAndRemember(localStream) { 686 var localStreamUrl = URL.createObjectURL(localStream); 687 $('local-view').src = localStreamUrl; 688 689 gLocalStream = localStream; 690 } 691 692 // Called if getUserMedia fails. 693 function printGetUserMediaError(error) { 694 var message = 'getUserMedia request unexpectedly failed:'; 695 if (error.constraintName) 696 message += ' could not satisfy constraint ' + error.constraintName; 697 else 698 message += ' devices not working/user denied access.'; 699 failTest(message); 700 } 701 702 // Called if getUserMedia succeeds and we want to send from both connections. 703 function addStreamToBothConnectionsAndNegotiate(localStream) { 704 displayAndRemember(localStream); 705 gFirstConnection.addStream(localStream); 706 gSecondConnection.addStream(localStream); 707 negotiate(); 708 } 709 710 // Called if getUserMedia succeeds when we want to send from one connection. 711 function addStreamToTheFirstConnectionAndNegotiate(localStream) { 712 displayAndRemember(localStream); 713 gFirstConnection.addStream(localStream); 714 negotiate(); 715 } 716 717 function verifyHasOneAudioAndVideoTrack(stream) { 718 assertEquals(1, stream.getAudioTracks().length); 719 assertEquals(1, stream.getVideoTracks().length); 720 } 721 722 // Called if getUserMedia succeeds, then clone the stream, send two streams 723 // from one peer connection. 724 function CloneStreamAndAddTwoStreamstoOneConnection(localStream) { 725 displayAndRemember(localStream); 726 727 var clonedStream = null; 728 if (typeof localStream.clone === "function") { 729 clonedStream = localStream.clone(); 730 } else { 731 clonedStream = new webkitMediaStream(localStream); 732 } 733 734 gFirstConnection.addStream(localStream); 735 gFirstConnection.addStream(clonedStream); 736 737 // Verify the local streams are correct. 738 assertEquals(2, gFirstConnection.getLocalStreams().length); 739 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[0]); 740 verifyHasOneAudioAndVideoTrack(gFirstConnection.getLocalStreams()[1]); 741 742 // The remote side should receive two streams. After that, verify the 743 // remote side has the correct number of streams and tracks. 744 addExpectedEvent(); 745 addExpectedEvent(); 746 gSecondConnection.onaddstream = function(event) { 747 eventOccured(); 748 } 749 setAllEventsOccuredHandler(function() { 750 // Negotiation complete, verify remote streams on the receiving side. 751 assertEquals(2, gSecondConnection.getRemoteStreams().length); 752 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[0]); 753 verifyHasOneAudioAndVideoTrack(gSecondConnection.getRemoteStreams()[1]); 754 755 reportTestSuccess(); 756 }); 757 758 negotiate(); 759 } 760 761 // Called if getUserMedia succeeds when we want to send a modified 762 // MediaStream. A new MediaStream is created and the video track from 763 // |localStream| is added. 764 function createNewVideoStreamAndAddToBothConnections(localStream) { 765 displayAndRemember(localStream); 766 var new_stream = new webkitMediaStream(); 767 new_stream.addTrack(localStream.getVideoTracks()[0]); 768 gFirstConnection.addStream(new_stream); 769 gSecondConnection.addStream(new_stream); 770 negotiate(); 771 } 772 773 function negotiate() { 774 negotiateBetween(gFirstConnection, gSecondConnection); 775 } 776 777 function negotiateBetween(caller, callee) { 778 console.log("Negotiating call..."); 779 // Not stable = negotiation is ongoing. The behavior of re-negotiating while 780 // a negotiation is ongoing is more or less undefined, so avoid this. 781 if (caller.signalingState != 'stable' || callee.signalingState != 'stable') 782 throw 'You can only negotiate when the connection is stable!'; 783 784 connectOnIceCandidate(caller, callee); 785 786 caller.createOffer( 787 function (offer) { 788 onOfferCreated(offer, caller, callee); 789 }); 790 } 791 792 function onOfferCreated(offer, caller, callee) { 793 offer.sdp = maybeForceIsac16K(transformSdp(offer.sdp)); 794 caller.setLocalDescription(offer, function() { 795 assertEquals('have-local-offer', caller.signalingState); 796 receiveOffer(offer.sdp, caller, callee); 797 }, onLocalDescriptionError); 798 } 799 800 function receiveOffer(offerSdp, caller, callee) { 801 console.log("Receiving offer..."); 802 offerSdp = transformRemoteSdp(offerSdp); 803 804 var parsedOffer = new RTCSessionDescription({ type: 'offer', 805 sdp: offerSdp }); 806 callee.setRemoteDescription(parsedOffer, function() {}, 807 onRemoteDescriptionError); 808 callee.createAnswer(function (answer) { 809 onAnswerCreated(answer, caller, callee); 810 }); 811 assertEquals('have-remote-offer', callee.signalingState); 812 } 813 814 function removeMsid(offerSdp) { 815 offerSdp = offerSdp.replace(/a=msid-semantic.*\r\n/g, ''); 816 offerSdp = offerSdp.replace('a=mid:audio\r\n', ''); 817 offerSdp = offerSdp.replace('a=mid:video\r\n', ''); 818 offerSdp = offerSdp.replace(/a=ssrc.*\r\n/g, ''); 819 return offerSdp; 820 } 821 822 function removeVideoCodec(offerSdp) { 823 offerSdp = offerSdp.replace('a=rtpmap:100 VP8/90000\r\n', 824 'a=rtpmap:100 XVP8/90000\r\n'); 825 return offerSdp; 826 } 827 828 function removeCrypto(offerSdp) { 829 offerSdp = offerSdp.replace(/a=crypto.*\r\n/g, 'a=Xcrypto\r\n'); 830 offerSdp = offerSdp.replace(/a=fingerprint.*\r\n/g, ''); 831 return offerSdp; 832 } 833 834 function addBandwithControl(offerSdp) { 835 offerSdp = offerSdp.replace('a=mid:audio\r\n', 'a=mid:audio\r\n'+ 836 'b=AS:16\r\n'); 837 offerSdp = offerSdp.replace('a=mid:video\r\n', 'a=mid:video\r\n'+ 838 'b=AS:512\r\n'); 839 return offerSdp; 840 } 841 842 function removeBundle(sdp) { 843 return sdp.replace(/a=group:BUNDLE .*\r\n/g, ''); 844 } 845 846 function useGice(sdp) { 847 sdp = sdp.replace(/t=.*\r\n/g, function(subString) { 848 return subString + 'a=ice-options:google-ice\r\n'; 849 }); 850 return sdp; 851 } 852 853 function useExternalSdes(sdp) { 854 // Remove current crypto specification. 855 sdp = sdp.replace(/a=crypto.*\r\n/g, ''); 856 sdp = sdp.replace(/a=fingerprint.*\r\n/g, ''); 857 // Add external crypto. This is not compatible with |removeMsid|. 858 sdp = sdp.replace(/a=mid:(\w+)\r\n/g, function(subString, group) { 859 return subString + EXTERNAL_SDES_LINES[group] + '\r\n'; 860 }); 861 return sdp; 862 } 863 864 function onAnswerCreated(answer, caller, callee) { 865 answer.sdp = maybeForceIsac16K(transformSdp(answer.sdp)); 866 callee.setLocalDescription(answer, 867 function () { 868 assertEquals('stable', callee.signalingState); 869 }, 870 onLocalDescriptionError); 871 receiveAnswer(answer.sdp, caller); 872 } 873 874 function receiveAnswer(answerSdp, caller) { 875 console.log("Receiving answer..."); 876 answerSdp = transformRemoteSdp(answerSdp); 877 var parsedAnswer = new RTCSessionDescription({ type: 'answer', 878 sdp: answerSdp }); 879 caller.setRemoteDescription(parsedAnswer, 880 function() { 881 assertEquals('stable', caller.signalingState); 882 }, 883 onRemoteDescriptionError); 884 } 885 886 function connectOnIceCandidate(caller, callee) { 887 caller.onicecandidate = function(event) { onIceCandidate(event, callee); } 888 callee.onicecandidate = function(event) { onIceCandidate(event, caller); } 889 } 890 891 function onIceCandidate(event, target) { 892 if (event.candidate) { 893 var candidate = new RTCIceCandidate(event.candidate); 894 target.addIceCandidate(candidate); 895 } 896 } 897 898 function onRemoteStream(e, target) { 899 console.log("Receiving remote stream..."); 900 if (gTestWithoutMsid && e.stream.id != "default") { 901 failTest('a default remote stream was expected but instead ' + 902 e.stream.id + ' was received.'); 903 } 904 gRemoteStreams[target] = e.stream; 905 var remoteStreamUrl = URL.createObjectURL(e.stream); 906 var remoteVideo = $(target); 907 remoteVideo.src = remoteStreamUrl; 908 } 909 910 </script> 911 </head> 912 <body> 913 <table border="0"> 914 <tr> 915 <td>Local Preview</td> 916 <td>Remote Stream for Connection 1</td> 917 <td>Remote Stream for Connection 2</td> 918 <td>Remote Stream for Connection 3</td> 919 <td>Remote Stream for Connection 4</td> 920 </tr> 921 <tr> 922 <td><video width="320" height="240" id="local-view" autoplay muted> 923 </video></td> 924 <td><video width="320" height="240" id="remote-view-1" autoplay> 925 </video></td> 926 <td><video width="320" height="240" id="remote-view-2" autoplay> 927 </video></td> 928 <td><video width="320" height="240" id="remote-view-3" autoplay> 929 </video></td> 930 <td><video width="320" height="240" id="remote-view-4" autoplay> 931 </video></td> 932 <!-- Canvases are named after their corresponding video elements. --> 933 <td><canvas width="320" height="240" id="remote-view-1-canvas" 934 style="display:none"></canvas></td> 935 <td><canvas width="320" height="240" id="remote-view-2-canvas" 936 style="display:none"></canvas></td> 937 <td><canvas width="320" height="240" id="remote-view-3-canvas" 938 style="display:none"></canvas></td> 939 <td><canvas width="320" height="240" id="remote-view-4-canvas" 940 style="display:none"></canvas></td> 941 </tr> 942 </table> 943 </body> 944 </html> 945