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