blob: 68bc1902674841ee451d975930b057a7982c2b88 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head><title>Loopback test</title></head>
<body>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay muted></video>
<script src="third_party/blackframe.js"></script>
<script src="third_party/munge_sdp.js"></script>
<script src="third_party/ssim.js"></script>
<script>
let isVideoInputFound = false;
let scriptReady = false;
let isTestDone = false;
let enumerateDevicesError = '';
const globalErrors = [];
let results = {};
// Check if a video input device exists
function checkVideoInput() {
navigator.mediaDevices.enumerateDevices()
.then(findVideoInput)
.catch(gotEnumerateDevicesError);
return isVideoInputFound;
}
function findVideoInput(devices) {
isVideoInputFound = devices.some((dev) => dev.kind == 'videoinput');
}
function gotEnumerateDevicesError(error) {
console.log('navigator.mediaDevices.enumerateDevices error: ', error);
enumerateDevicesError = error.toString();
}
// Starts the test.
function testWebRtcLoopbackCall(videoCodec, durationSec) {
const durationMs = 1000 * durationSec;
const test = new WebRtcLoopbackCallTest(videoCodec, durationMs);
test.run();
}
// Returns the results to caller.
function getResults() {
return results;
}
function setResults(stats) {
results = stats;
}
// Calculates averages of array values.
function average(array) {
const count = array.length;
let total = 0;
for (let i = 0; i < count; i++) {
total += parseInt(array[i]);
}
return Math.floor(total / count);
}
// Actual test object.
function WebRtcLoopbackCallTest(videoCodec, durationMs) {
this.videoCodec = videoCodec;
this.durationMs = durationMs;
this.localStream = null;
this.remoteStream = null;
this.results = {
cameraType: '',
peerConnectionStats: {minInFps: 0, maxInFps: 0, averageInFps: 0,
minOutFps: 0, maxOutFps: 0, averageOutFps: 0},
frameStats: {totalFrames: 0, blackFrames: 0, frozenFrames: 0},
errors: globalErrors,
};
this.inFps = [];
this.outFps = [];
// Variables associated with nearly-frozen frames detection.
this.previousFrame = [];
this.identicalFrameSsimThreshold = 0.985;
this.frameComparator = new Ssim();
this.remoteVideo = document.getElementById('remoteVideo');
this.localVideo = document.getElementById('localVideo');
}
WebRtcLoopbackCallTest.prototype = {
collectAndAnalyzeStats: function() {
this.gatherStats(this.localPeerConnection, 100, this.durationMs,
this.reportTestDone.bind(this));
},
setup: function() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.remoteVideo.onloadedmetadata = this.collectAndAnalyzeStats.bind(this);
this.remoteVideo.addEventListener('play',
this.startCheckingVideoFrames.bind(this), false);
},
startCheckingVideoFrames: function() {
// TODO(phoglund): replace with MediaRecorder. setInterval isn't at all
// reliable, so the number of captured frames can probably vary wildly
// over the 20 second execution time.
this.videoFrameChecker = setInterval(this.checkVideoFrame.bind(this), 20);
},
run: function() {
this.setup();
this.triggerGetUserMedia();
},
triggerGetUserMedia: function() {
const constraints = {audio: false, video: true};
navigator.mediaDevices.getUserMedia(constraints)
.then(this.gotLocalStream.bind(this))
.catch(this.onGetUserMediaError.bind(this));
},
reportError: function(message) {
globalErrors.push(message);
},
gotLocalStream: function(stream) {
this.localStream = stream;
let servers = null;
this.localPeerConnection = new webkitRTCPeerConnection(servers);
this.localPeerConnection.onicecandidate = this.gotLocalIceCandidate.bind(
this);
this.remotePeerConnection = new webkitRTCPeerConnection(servers);
this.remotePeerConnection.onicecandidate = this.gotRemoteIceCandidate.bind(
this);
this.remotePeerConnection.onaddstream = this.gotRemoteStream.bind(this);
this.localPeerConnection.addStream(this.localStream);
this.localPeerConnection.createOffer(this.gotOffer.bind(this),
function(error) {});
this.localVideo.srcObject = stream;
this.results.cameraType = stream.getVideoTracks()[0].label;
},
onGetUserMediaError: function(error) {
this.reportError('getUserMedia failed: ' + error.toString());
},
gatherStats: function(peerConnection, interval, durationMs, callback) {
const startTime = new Date();
const pollFunction = setInterval(gatherOneReport.bind(this), interval);
function gatherOneReport() {
const elapsed = new Date() - startTime;
if (elapsed > durationMs) {
clearInterval(pollFunction);
callback();
return;
}
peerConnection.getStats(this.gotStats.bind(this));
}
},
getStatFromReport: function(data, name) {
if (data.type = 'ssrc' && data.stat(name)) {
return data.stat(name);
} else {
return null;
}
},
gotStats: function(response) {
const reports = response.result();
for (let i = 0; i < reports.length; ++i) {
const report = reports[i];
const incomingFps = this.getStatFromReport(report, 'googFrameRateInput');
if (incomingFps == null) {
// Skip on null.
continue;
}
const outgoingFps = this.getStatFromReport(report, 'googFrameRateSent');
// Save rates for later processing.
this.inFps.push(incomingFps);
this.outFps.push(outgoingFps);
}
},
reportTestDone: function() {
this.processStats();
clearInterval(this.videoFrameChecker);
setResults(this.results);
// Stop camera devices
this.localStream.getTracks().forEach((track) => track.stop());
this.localVideo.srcObject = null;
this.remoteVideo.srcObject = null;
document.body.removeChild(this.localVideo);
document.body.removeChild(this.remoteVideo);
isTestDone = true;
},
processStats: function() {
if (this.inFps != [] && this.outFps != []) {
stats = this.results.peerConnectionStats;
stats.minInFps = Math.min.apply(null, this.inFps);
stats.maxInFps = Math.max.apply(null, this.inFps);
stats.averageInFps = average(this.inFps);
stats.minOutFps = Math.min.apply(null, this.outFps);
stats.maxOutFps = Math.max.apply(null, this.outFps);
stats.averageOutFps = average(this.outFps);
}
},
checkVideoFrame: function() {
this.context.drawImage(this.remoteVideo, 0, 0, this.canvas.width,
this.canvas.height);
const imageData = this.context.getImageData(0, 0, this.canvas.width,
this.canvas.height);
if (isBlackFrame(imageData.data, imageData.data.length)) {
this.results.frameStats.blackFrames++;
}
if (this.frameComparator.calculate(this.previousFrame, imageData.data) >
this.identicalFrameSsimThreshold) {
this.results.frameStats.frozenFrames++;
}
this.previousFrame = imageData.data;
this.results.frameStats.totalFrames++;
},
isBlackFrame: function(data, length) {
let accumulatedLuma = 0;
for (let i = 4; i < length; i += 4) {
// Use Luma as in Rec. 709: Y′709 = 0.21R + 0.72G + 0.07B;
accumulatedLuma += (0.21 * data[i] + 0.72 * data[i + 1]
+ 0.07 * data[i + 2]);
// Early termination if the average Luma so far is bright enough.
if (accumulatedLuma > (this.nonBlackPixelLumaThreshold * i / 4)) {
return false;
}
}
return true;
},
gotRemoteStream: function(event) {
this.remoteVideo.srcObject = event.stream;
},
gotOffer: function(description) {
description.sdp =
setSdpDefaultVideoCodec(description.sdp, this.videoCodec);
this.localPeerConnection.setLocalDescription(description);
this.remotePeerConnection.setRemoteDescription(description);
this.remotePeerConnection.createAnswer(this.gotAnswer.bind(
this), function(error) {});
},
gotAnswer: function(description) {
const selectedCodec =
getSdpDefaultVideoCodec(description.sdp);
if (selectedCodec != this.videoCodec) {
this.reportError('Expected codec ' + this.videoCodec + ', but WebRTC ' +
'selected ' + selectedCodec);
}
this.remotePeerConnection.setLocalDescription(description);
this.localPeerConnection.setRemoteDescription(description);
},
gotLocalIceCandidate: function(event) {
if (event.candidate) {
this.remotePeerConnection.addIceCandidate(
new RTCIceCandidate(event.candidate));
}
},
gotRemoteIceCandidate: function(event) {
if (event.candidate) {
this.localPeerConnection.addIceCandidate(
new RTCIceCandidate(event.candidate));
}
},
};
window.onerror = function(message, filename, lineno, colno, error) {
globalErrors.push('exception-in-test-page: ' + error.stack);
};
// Used by munge_sdp.js.
function failure(location, msg) {
globalErrors.push('failed-to-munge: ' + msg + ' in ' + location);
}
scriptReady = true;
</script>
</body>
</html>