Home | History | Annotate | Download | only in loopback_test
      1 /**
      2  * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
      3  *
      4  * Use of this source code is governed by a BSD-style license
      5  * that can be found in the LICENSE file in the root of the source
      6  * tree. An additional intellectual property rights grant can be found
      7  * in the file PATENTS.  All contributing project authors may
      8  * be found in the AUTHORS file in the root of the source tree.
      9  */
     10 
     11 // LoopbackTest establish a one way loopback call between 2 peer connections
     12 // while continuously monitoring bandwidth stats. The idea is to use this as
     13 // a base for other future tests and to keep track of more than just bandwidth
     14 // stats.
     15 //
     16 // Usage:
     17 //  var test = new LoopbackTest(stream, callDurationMs,
     18 //                              forceTurn, pcConstraints,
     19 //                              maxVideoBitrateKbps);
     20 //  test.run(onDone);
     21 //  function onDone() {
     22 //    test.getResults(); // return stats recorded during the loopback test.
     23 //  }
     24 //
     25 function LoopbackTest(
     26     stream,
     27     callDurationMs,
     28     forceTurn,
     29     pcConstraints,
     30     maxVideoBitrateKbps) {
     31 
     32   var pc1StatTracker;
     33   var pc2StatTracker;
     34 
     35   // In order to study effect of network (e.g. wifi) on peer connection one can
     36   // establish a loopback call and force it to go via a turn server. This way
     37   // the call won't switch to local addresses. That is achieved by filtering out
     38   // all non-relay ice candidades on both peers.
     39   function constrainTurnCandidates(pc) {
     40     var origAddIceCandidate = pc.addIceCandidate;
     41     pc.addIceCandidate = function (candidate, successCallback,
     42                                    failureCallback) {
     43       if (forceTurn && candidate.candidate.indexOf("typ relay ") == -1) {
     44         trace("Dropping non-turn candidate: " + candidate.candidate);
     45         successCallback();
     46         return;
     47       } else {
     48         origAddIceCandidate.call(this, candidate, successCallback,
     49                                  failureCallback);
     50       }
     51     }
     52   }
     53 
     54   // FEC makes it hard to study bwe estimation since there seems to be a spike
     55   // when it is enabled and disabled. Disable it for now. FEC issue tracked on:
     56   // https://code.google.com/p/webrtc/issues/detail?id=3050
     57   function constrainOfferToRemoveFec(pc) {
     58     var origCreateOffer = pc.createOffer;
     59     pc.createOffer = function (successCallback, failureCallback, options) {
     60       function filteredSuccessCallback(desc) {
     61         desc.sdp = desc.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g,
     62                                     '$1\r\n');
     63         desc.sdp = desc.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, '');
     64         desc.sdp = desc.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, '');
     65         successCallback(desc);
     66       }
     67       origCreateOffer.call(this, filteredSuccessCallback, failureCallback,
     68                            options);
     69     }
     70   }
     71 
     72   // Constraint max video bitrate by modifying the SDP when creating an answer.
     73   function constrainBitrateAnswer(pc) {
     74     var origCreateAnswer = pc.createAnswer;
     75     pc.createAnswer = function (successCallback, failureCallback, options) {
     76       function filteredSuccessCallback(desc) {
     77         if (maxVideoBitrateKbps) {
     78           desc.sdp = desc.sdp.replace(
     79               /a=mid:video\r\n/g,
     80               'a=mid:video\r\nb=AS:' + maxVideoBitrateKbps + '\r\n');
     81         }
     82         successCallback(desc);
     83       }
     84       origCreateAnswer.call(this, filteredSuccessCallback, failureCallback,
     85                             options);
     86     }
     87   }
     88 
     89   // Run the actual LoopbackTest.
     90   this.run = function(doneCallback) {
     91     if (forceTurn) requestTurn(start, fail);
     92     else start();
     93 
     94     function start(turnServer) {
     95       var pcConfig = forceTurn ? { iceServers: [turnServer] } : null;
     96       console.log(pcConfig);
     97       var pc1 = new RTCPeerConnection(pcConfig, pcConstraints);
     98       constrainTurnCandidates(pc1);
     99       constrainOfferToRemoveFec(pc1);
    100       pc1StatTracker = new StatTracker(pc1, 50);
    101       pc1StatTracker.recordStat("EstimatedSendBitrate",
    102                                 "bweforvideo", "googAvailableSendBandwidth");
    103       pc1StatTracker.recordStat("TransmitBitrate",
    104                                 "bweforvideo", "googTransmitBitrate");
    105       pc1StatTracker.recordStat("TargetEncodeBitrate",
    106                                 "bweforvideo", "googTargetEncBitrate");
    107       pc1StatTracker.recordStat("ActualEncodedBitrate",
    108                                 "bweforvideo", "googActualEncBitrate");
    109 
    110       var pc2 = new RTCPeerConnection(pcConfig, pcConstraints);
    111       constrainTurnCandidates(pc2);
    112       constrainBitrateAnswer(pc2);
    113       pc2StatTracker = new StatTracker(pc2, 50);
    114       pc2StatTracker.recordStat("REMB",
    115                                 "bweforvideo", "googAvailableReceiveBandwidth");
    116 
    117       pc1.addStream(stream);
    118       var call = new Call(pc1, pc2);
    119 
    120       call.start();
    121       setTimeout(function () {
    122           call.stop();
    123           pc1StatTracker.stop();
    124           pc2StatTracker.stop();
    125           success();
    126         }, callDurationMs);
    127     }
    128 
    129     function success() {
    130       trace("Success");
    131       doneCallback();
    132     }
    133 
    134     function fail(msg) {
    135       trace("Fail: " + msg);
    136       doneCallback();
    137     }
    138   }
    139 
    140   // Returns a google visualization datatable with the recorded samples during
    141   // the loopback test.
    142   this.getResults = function () {
    143     return mergeDataTable(pc1StatTracker.dataTable(),
    144                           pc2StatTracker.dataTable());
    145   }
    146 
    147   // Helper class to establish and manage a call between 2 peer connections.
    148   // Usage:
    149   //   var c = new Call(pc1, pc2);
    150   //   c.start();
    151   //   c.stop();
    152   //
    153   function Call(pc1, pc2) {
    154     pc1.onicecandidate = applyIceCandidate.bind(pc2);
    155     pc2.onicecandidate = applyIceCandidate.bind(pc1);
    156 
    157     function applyIceCandidate(e) {
    158       if (e.candidate) {
    159         this.addIceCandidate(new RTCIceCandidate(e.candidate),
    160                              onAddIceCandidateSuccess,
    161                              onAddIceCandidateError);
    162       }
    163     }
    164 
    165     function onAddIceCandidateSuccess() {}
    166     function onAddIceCandidateError(error) {
    167       trace("Failed to add Ice Candidate: " + error.toString());
    168     }
    169 
    170     this.start = function() {
    171       pc1.createOffer(gotDescription1, onCreateSessionDescriptionError);
    172 
    173       function onCreateSessionDescriptionError(error) {
    174         trace('Failed to create session description: ' + error.toString());
    175       }
    176 
    177       function gotDescription1(desc){
    178         trace("Offer: " + desc.sdp);
    179         pc1.setLocalDescription(desc);
    180         pc2.setRemoteDescription(desc);
    181         // Since the "remote" side has no media stream we need
    182         // to pass in the right constraints in order for it to
    183         // accept the incoming offer of audio and video.
    184         pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError);
    185       }
    186 
    187       function gotDescription2(desc){
    188         trace("Answer: " + desc.sdp);
    189         pc2.setLocalDescription(desc);
    190         pc1.setRemoteDescription(desc);
    191       }
    192     }
    193 
    194     this.stop = function() {
    195       pc1.close();
    196       pc2.close();
    197     }
    198   }
    199 
    200   // Request a turn server. This uses the same servers as apprtc.
    201   function requestTurn(successCallback, failureCallback) {
    202     var currentDomain = document.domain;
    203     if (currentDomain.search('localhost') === -1 &&
    204         currentDomain.search('webrtc.googlecode.com') === -1) {
    205       failureCallback("Domain not authorized for turn server: " +
    206                       currentDomain);
    207       return;
    208     }
    209 
    210     // Get a turn server from computeengineondemand.appspot.com.
    211     var turnUrl = 'https://computeengineondemand.appspot.com/' +
    212                   'turn?username=156547625762562&key=4080218913';
    213     var xmlhttp = new XMLHttpRequest();
    214     xmlhttp.onreadystatechange = onTurnResult;
    215     xmlhttp.open('GET', turnUrl, true);
    216     xmlhttp.send();
    217 
    218     function onTurnResult() {
    219       if (this.readyState !== 4) {
    220         return;
    221       }
    222 
    223       if (this.status === 200) {
    224         var turnServer = JSON.parse(xmlhttp.responseText);
    225         // Create turnUris using the polyfill (adapter.js).
    226         turnServer.uris = turnServer.uris.filter(
    227             function (e) { return e.search('transport=udp') != -1; }
    228         );
    229         var iceServers = createIceServers(turnServer.uris,
    230                                           turnServer.username,
    231                                           turnServer.password);
    232         if (iceServers !== null) {
    233           successCallback(iceServers);
    234           return;
    235         }
    236       }
    237       failureCallback("Failed to get a turn server.");
    238     }
    239   }
    240 }
    241