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