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 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