Tool to establish a loopback call via apprtc turn server.

For now the test keeps track of video bandwidth estimation and plots it
using google visualization libraries after the test is concluded.
There is also scripts to run a test and record the tcpdump.

BUG=3037
R=hta@webrtc.org, phoglund@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/9729004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@5707 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/webrtc/tools/loopback_test/loopback_test.js b/webrtc/tools/loopback_test/loopback_test.js
new file mode 100644
index 0000000..b99204d
--- /dev/null
+++ b/webrtc/tools/loopback_test/loopback_test.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS.  All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+// LoopbackTest establish a one way loopback call between 2 peer connections
+// while continuously monitoring bandwidth stats. The idea is to use this as
+// a base for other future tests and to keep track of more than just bandwidth
+// stats.
+//
+// Usage:
+//  var test = new LoopbackTest(stream, callDurationMs,
+//                              forceTurn, maxVideoBitrateKbps);
+//  test.run(onDone);
+//  function onDone() {
+//    test.getResults(); // return stats recorded during the loopback test.
+//  }
+//
+function LoopbackTest(stream, callDurationMs, forceTurn, maxVideoBitrateKbps) {
+  var pc1StatTracker;
+  var pc2StatTracker;
+
+  // In order to study effect of network (e.g. wifi) on peer connection one can
+  // establish a loopback call and force it to go via a turn server. This way
+  // the call won't switch to local addresses. That is achieved by filtering out
+  // all non-relay ice candidades on both peers.
+  function constrainTurnCandidates(pc) {
+    var origAddIceCandidate = pc.addIceCandidate;
+    pc.addIceCandidate = function (candidate, successCallback,
+                                   failureCallback) {
+      if (forceTurn && candidate.candidate.indexOf("typ relay ") == -1) {
+        trace("Dropping non-turn candidate: " + candidate.candidate);
+        successCallback();
+        return;
+      } else {
+        origAddIceCandidate.call(this, candidate, successCallback,
+                                 failureCallback);
+      }
+    }
+  }
+
+  // FEC makes it hard to study bwe estimation since there seems to be a spike
+  // when it is enabled and disabled. Disable it for now. FEC issue tracked on:
+  // https://code.google.com/p/webrtc/issues/detail?id=3050
+  function constrainOfferToRemoveFec(pc) {
+    var origCreateOffer = pc.createOffer;
+    pc.createOffer = function (successCallback, failureCallback, options) {
+      function filteredSuccessCallback(desc) {
+        desc.sdp = desc.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g,
+                                    '$1\r\n');
+        desc.sdp = desc.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, '');
+        desc.sdp = desc.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, '');
+        successCallback(desc);
+      }
+      origCreateOffer.call(this, filteredSuccessCallback, failureCallback,
+                           options);
+    }
+  }
+
+  // Constraint max video bitrate by modifying the SDP when creating an answer.
+  function constrainBitrateAnswer(pc) {
+    var origCreateAnswer = pc.createAnswer;
+    pc.createAnswer = function (successCallback, failureCallback, options) {
+      function filteredSuccessCallback(desc) {
+        if (maxVideoBitrateKbps) {
+          desc.sdp = desc.sdp.replace(
+              /a=mid:video\r\n/g,
+              'a=mid:video\r\nb=AS:' + maxVideoBitrateKbps + '\r\n');
+        }
+        successCallback(desc);
+      }
+      origCreateAnswer.call(this, filteredSuccessCallback, failureCallback,
+                            options);
+    }
+  }
+
+  // Run the actual LoopbackTest.
+  this.run = function(doneCallback) {
+    if (forceTurn) requestTurn(start, fail);
+    else start();
+
+    function start(turnServer) {
+      var pcConfig = forceTurn ? { iceServers: [turnServer] } : null;
+      console.log(pcConfig);
+      var pc1 = new RTCPeerConnection(pcConfig);
+      constrainTurnCandidates(pc1);
+      constrainOfferToRemoveFec(pc1);
+      pc1StatTracker = new StatTracker(pc1, 50);
+      pc1StatTracker.recordStat("EstimatedSendBitrate",
+                                "bweforvideo", "googAvailableSendBandwidth");
+      pc1StatTracker.recordStat("TransmitBitrate",
+                                "bweforvideo", "googTransmitBitrate");
+      pc1StatTracker.recordStat("TargetEncodeBitrate",
+                                "bweforvideo", "googTargetEncBitrate");
+      pc1StatTracker.recordStat("ActualEncodedBitrate",
+                                "bweforvideo", "googActualEncBitrate");
+
+      var pc2 = new RTCPeerConnection(pcConfig);
+      constrainTurnCandidates(pc2);
+      constrainBitrateAnswer(pc2);
+      pc2StatTracker = new StatTracker(pc2, 50);
+      pc2StatTracker.recordStat("REMB",
+                                "bweforvideo", "googAvailableReceiveBandwidth");
+
+      pc1.addStream(stream);
+      var call = new Call(pc1, pc2);
+
+      call.start();
+      setTimeout(function () {
+          call.stop();
+          pc1StatTracker.stop();
+          pc2StatTracker.stop();
+          success();
+        }, callDurationMs);
+    }
+
+    function success() {
+      trace("Success");
+      doneCallback();
+    }
+
+    function fail() {
+      trace("Fail");
+      doneCallback();
+    }
+  }
+
+  // Returns a google visualization datatable with the recorded samples during
+  // the loopback test.
+  this.getResults = function () {
+    return mergeDataTable(pc1StatTracker.dataTable(),
+                          pc2StatTracker.dataTable());
+  }
+
+  // Helper class to establish and manage a call between 2 peer connections.
+  // Usage:
+  //   var c = new Call(pc1, pc2);
+  //   c.start();
+  //   c.stop();
+  //
+  function Call(pc1, pc2) {
+    pc1.onicecandidate = applyIceCandidate.bind(pc2);
+    pc2.onicecandidate = applyIceCandidate.bind(pc1);
+
+    function applyIceCandidate(e) {
+      if (e.candidate) {
+        this.addIceCandidate(new RTCIceCandidate(e.candidate),
+                             onAddIceCandidateSuccess,
+                             onAddIceCandidateError);
+      }
+    }
+
+    function onAddIceCandidateSuccess() {}
+    function onAddIceCandidateError(error) {
+      trace("Failed to add Ice Candidate: " + error.toString());
+    }
+
+    this.start = function() {
+      pc1.createOffer(gotDescription1, onCreateSessionDescriptionError);
+
+      function onCreateSessionDescriptionError(error) {
+        trace('Failed to create session description: ' + error.toString());
+      }
+
+      function gotDescription1(desc){
+        trace("Offer: " + desc.sdp);
+        pc1.setLocalDescription(desc);
+        pc2.setRemoteDescription(desc);
+        // Since the "remote" side has no media stream we need
+        // to pass in the right constraints in order for it to
+        // accept the incoming offer of audio and video.
+        pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError);
+      }
+
+      function gotDescription2(desc){
+        trace("Answer: " + desc.sdp);
+        pc2.setLocalDescription(desc);
+        pc1.setRemoteDescription(desc);
+      }
+    }
+
+    this.stop = function() {
+      pc1.close();
+      pc2.close();
+    }
+  }
+
+  // Request a turn server. This uses the same servers as apprtc.
+  function requestTurn(successCallback, failureCallback) {
+    var currentDomain = document.domain;
+    if (currentDomain.search('localhost') === -1 &&
+        currentDomain.search('apprtc') === -1) {
+      onerror("not authorized domain");
+      return;
+    }
+
+    // Get a turn server from computeengineondemand.appspot.com.
+    var turnUrl = 'https://computeengineondemand.appspot.com/' +
+                  'turn?username=156547625762562&key=4080218913';
+    var xmlhttp = new XMLHttpRequest();
+    xmlhttp.onreadystatechange = onTurnResult;
+    xmlhttp.open('GET', turnUrl, true);
+    xmlhttp.send();
+
+    function onTurnResult() {
+      if (this.readyState !== 4) {
+        return;
+      }
+
+      if (this.status === 200) {
+        var turnServer = JSON.parse(xmlhttp.responseText);
+        // Create turnUris using the polyfill (adapter.js).
+        turnServer.uris = turnServer.uris.filter(
+            function (e) { return e.search('transport=udp') != -1; }
+        );
+        var iceServers = createIceServers(turnServer.uris,
+                                          turnServer.username,
+                                          turnServer.password);
+        if (iceServers !== null) {
+          successCallback(iceServers);
+          return;
+        }
+      }
+      failureCallback("Failed to get a turn server.");
+    }
+  }
+}