| /** |
| * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| // LoopbackTest establish a one way loopback call between 2 peer connections |
| // while continuously monitoring bandwidth stats. The idea is to use this as |
| // a base for other future tests and to keep track of more than just bandwidth |
| // stats. |
| // |
| // Usage: |
| // var test = new LoopbackTest(stream, callDurationMs, |
| // forceTurn, pcConstraints, |
| // maxVideoBitrateKbps); |
| // test.run(onDone); |
| // function onDone() { |
| // test.getResults(); // return stats recorded during the loopback test. |
| // } |
| // |
| function LoopbackTest( |
| stream, |
| callDurationMs, |
| forceTurn, |
| pcConstraints, |
| maxVideoBitrateKbps) { |
| |
| var pc1StatTracker; |
| var pc2StatTracker; |
| |
| // In order to study effect of network (e.g. wifi) on peer connection one can |
| // establish a loopback call and force it to go via a turn server. This way |
| // the call won't switch to local addresses. That is achieved by filtering out |
| // all non-relay ice candidades on both peers. |
| function constrainTurnCandidates(pc) { |
| var origAddIceCandidate = pc.addIceCandidate; |
| pc.addIceCandidate = function (candidate, successCallback, |
| failureCallback) { |
| if (forceTurn && candidate.candidate.indexOf("typ relay ") == -1) { |
| trace("Dropping non-turn candidate: " + candidate.candidate); |
| successCallback(); |
| return; |
| } else { |
| origAddIceCandidate.call(this, candidate, successCallback, |
| failureCallback); |
| } |
| } |
| } |
| |
| // FEC makes it hard to study bwe estimation since there seems to be a spike |
| // when it is enabled and disabled. Disable it for now. FEC issue tracked on: |
| // https://code.google.com/p/webrtc/issues/detail?id=3050 |
| function constrainOfferToRemoveFec(pc) { |
| var origCreateOffer = pc.createOffer; |
| pc.createOffer = function (successCallback, failureCallback, options) { |
| function filteredSuccessCallback(desc) { |
| desc.sdp = desc.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g, |
| '$1\r\n'); |
| desc.sdp = desc.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, ''); |
| desc.sdp = desc.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, ''); |
| successCallback(desc); |
| } |
| origCreateOffer.call(this, filteredSuccessCallback, failureCallback, |
| options); |
| } |
| } |
| |
| // Constraint max video bitrate by modifying the SDP when creating an answer. |
| function constrainBitrateAnswer(pc) { |
| var origCreateAnswer = pc.createAnswer; |
| pc.createAnswer = function (successCallback, failureCallback, options) { |
| function filteredSuccessCallback(desc) { |
| if (maxVideoBitrateKbps) { |
| desc.sdp = desc.sdp.replace( |
| /a=mid:video\r\n/g, |
| 'a=mid:video\r\nb=AS:' + maxVideoBitrateKbps + '\r\n'); |
| } |
| successCallback(desc); |
| } |
| origCreateAnswer.call(this, filteredSuccessCallback, failureCallback, |
| options); |
| } |
| } |
| |
| // Run the actual LoopbackTest. |
| this.run = function(doneCallback) { |
| if (forceTurn) requestTurn(start, fail); |
| else start(); |
| |
| function start(turnServer) { |
| var pcConfig = forceTurn ? { iceServers: [turnServer] } : null; |
| console.log(pcConfig); |
| var pc1 = new RTCPeerConnection(pcConfig, pcConstraints); |
| constrainTurnCandidates(pc1); |
| constrainOfferToRemoveFec(pc1); |
| pc1StatTracker = new StatTracker(pc1, 50); |
| pc1StatTracker.recordStat("EstimatedSendBitrate", |
| "bweforvideo", "googAvailableSendBandwidth"); |
| pc1StatTracker.recordStat("TransmitBitrate", |
| "bweforvideo", "googTransmitBitrate"); |
| pc1StatTracker.recordStat("TargetEncodeBitrate", |
| "bweforvideo", "googTargetEncBitrate"); |
| pc1StatTracker.recordStat("ActualEncodedBitrate", |
| "bweforvideo", "googActualEncBitrate"); |
| |
| var pc2 = new RTCPeerConnection(pcConfig, pcConstraints); |
| constrainTurnCandidates(pc2); |
| constrainBitrateAnswer(pc2); |
| pc2StatTracker = new StatTracker(pc2, 50); |
| pc2StatTracker.recordStat("REMB", |
| "bweforvideo", "googAvailableReceiveBandwidth"); |
| |
| pc1.addStream(stream); |
| var call = new Call(pc1, pc2); |
| |
| call.start(); |
| setTimeout(function () { |
| call.stop(); |
| pc1StatTracker.stop(); |
| pc2StatTracker.stop(); |
| success(); |
| }, callDurationMs); |
| } |
| |
| function success() { |
| trace("Success"); |
| doneCallback(); |
| } |
| |
| function fail(msg) { |
| trace("Fail: " + msg); |
| doneCallback(); |
| } |
| } |
| |
| // Returns a google visualization datatable with the recorded samples during |
| // the loopback test. |
| this.getResults = function () { |
| return mergeDataTable(pc1StatTracker.dataTable(), |
| pc2StatTracker.dataTable()); |
| } |
| |
| // Helper class to establish and manage a call between 2 peer connections. |
| // Usage: |
| // var c = new Call(pc1, pc2); |
| // c.start(); |
| // c.stop(); |
| // |
| function Call(pc1, pc2) { |
| pc1.onicecandidate = applyIceCandidate.bind(pc2); |
| pc2.onicecandidate = applyIceCandidate.bind(pc1); |
| |
| function applyIceCandidate(e) { |
| if (e.candidate) { |
| this.addIceCandidate(new RTCIceCandidate(e.candidate), |
| onAddIceCandidateSuccess, |
| onAddIceCandidateError); |
| } |
| } |
| |
| function onAddIceCandidateSuccess() {} |
| function onAddIceCandidateError(error) { |
| trace("Failed to add Ice Candidate: " + error.toString()); |
| } |
| |
| this.start = function() { |
| pc1.createOffer(gotDescription1, onCreateSessionDescriptionError); |
| |
| function onCreateSessionDescriptionError(error) { |
| trace('Failed to create session description: ' + error.toString()); |
| } |
| |
| function gotDescription1(desc){ |
| trace("Offer: " + desc.sdp); |
| pc1.setLocalDescription(desc); |
| pc2.setRemoteDescription(desc); |
| // Since the "remote" side has no media stream we need |
| // to pass in the right constraints in order for it to |
| // accept the incoming offer of audio and video. |
| pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError); |
| } |
| |
| function gotDescription2(desc){ |
| trace("Answer: " + desc.sdp); |
| pc2.setLocalDescription(desc); |
| pc1.setRemoteDescription(desc); |
| } |
| } |
| |
| this.stop = function() { |
| pc1.close(); |
| pc2.close(); |
| } |
| } |
| |
| // Request a turn server. This uses the same servers as apprtc. |
| function requestTurn(successCallback, failureCallback) { |
| var currentDomain = document.domain; |
| if (currentDomain.search('localhost') === -1 && |
| currentDomain.search('webrtc.googlecode.com') === -1) { |
| failureCallback("Domain not authorized for turn server: " + |
| currentDomain); |
| return; |
| } |
| |
| // Get a turn server from computeengineondemand.appspot.com. |
| var turnUrl = 'https://computeengineondemand.appspot.com/' + |
| 'turn?username=156547625762562&key=4080218913'; |
| var xmlhttp = new XMLHttpRequest(); |
| xmlhttp.onreadystatechange = onTurnResult; |
| xmlhttp.open('GET', turnUrl, true); |
| xmlhttp.send(); |
| |
| function onTurnResult() { |
| if (this.readyState !== 4) { |
| return; |
| } |
| |
| if (this.status === 200) { |
| var turnServer = JSON.parse(xmlhttp.responseText); |
| // Create turnUris using the polyfill (adapter.js). |
| turnServer.uris = turnServer.uris.filter( |
| function (e) { return e.search('transport=udp') != -1; } |
| ); |
| var iceServers = createIceServers(turnServer.uris, |
| turnServer.username, |
| turnServer.password); |
| if (iceServers !== null) { |
| successCallback(iceServers); |
| return; |
| } |
| } |
| failureCallback("Failed to get a turn server."); |
| } |
| } |
| } |