OLD | NEW |
| (Empty) |
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 } | |
OLD | NEW |