Home | History | Annotate | Download | only in media
      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