|  | /** | 
|  | * 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."); | 
|  | } | 
|  | } | 
|  | } |