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//
25function 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