241 lines
8.3 KiB
JavaScript
241 lines
8.3 KiB
JavaScript
/**
|
|
* 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.");
|
|
}
|
|
}
|
|
}
|