blob: 5e596b7ff3708ff52265e24b7e4864567e226d58 [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,
andresp@webrtc.org0273fa92014-04-10 09:40:16 +000018// forceTurn, pcConstraints,
19// maxVideoBitrateKbps);
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +000020// test.run(onDone);
21// function onDone() {
22// test.getResults(); // return stats recorded during the loopback test.
23// }
24//
andresp@webrtc.org0273fa92014-04-10 09:40:16 +000025function LoopbackTest(
26 stream,
27 callDurationMs,
28 forceTurn,
29 pcConstraints,
30 maxVideoBitrateKbps) {
31
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +000032 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);
andresp@webrtc.org0273fa92014-04-10 09:40:16 +000097 var pc1 = new RTCPeerConnection(pcConfig, pcConstraints);
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +000098 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
andresp@webrtc.org0273fa92014-04-10 09:40:16 +0000110 var pc2 = new RTCPeerConnection(pcConfig, pcConstraints);
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +0000111 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
andresp@webrtc.org5a0218c2014-03-27 19:24:45 +0000134 function fail(msg) {
135 trace("Fail: " + msg);
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +0000136 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 &&
andresp@webrtc.org5a0218c2014-03-27 19:24:45 +0000204 currentDomain.search('webrtc.googlecode.com') === -1) {
205 failureCallback("Domain not authorized for turn server: " +
206 currentDomain);
andresp@webrtc.org44eb87e2014-03-17 14:23:22 +0000207 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}