blob: 5e596b7ff3708ff52265e24b7e4864567e226d58 [file] [log] [blame]
/**
* 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.");
}
}
}