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