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