<!DOCTYPE html> <html> <head><title>Loopback test</title></head> <body> <video id="localVideo" width="1280" height="720" autoplay muted></video> <video id="remoteVideo" width="1280" height="720" autoplay muted></video> <script src="ssim.js"></script> <script src="blackframe.js"></script> <script> var results = {}; var testProgress = 0; // Starts the test. function testCamera(resolution) { var test = new CameraTest(resolution); test.run(); } // Returns the results to caller. function getResults() { return results; } function setResults(stats) { results = stats; } // Calculates averages of array values. function average(array) { var count = array.length; var total = 0; for (var i = 0; i < count; i++) { total += parseInt(array[i]); } return Math.floor(total / count); } // Actual test object. function CameraTest(resolutionArray) { this.resolution = resolutionArray; this.localStream = null; this.remoteStream = null; this.results = {cameraType: '', cameraErrors: [], peerConnectionStats: [], frameStats: {numBlackFrames: 0, numFrozenFrames:0, numFrames: 0}}; 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"); this.localVideo.width = this.resolution[0].toString(); this.localVideo.height = this.resolution[1].toString(); this.remoteVideo.width = this.resolution[0].toString(); this.remoteVideo.height = this.resolution[1].toString(); } function resolutionMatchesIndependentOfRotation(aWidth, aHeight, bWidth, bHeight) { return (aWidth === bWidth && aHeight === bHeight) || (aWidth === bHeight && aHeight === bWidth); } CameraTest.prototype = { collectAndAnalyzeStats: function() { if (!resolutionMatchesIndependentOfRotation(this.localVideo.width, this.localVideo.height, this.resolution[0], this.resolution[1])) { this.reportError('resolution', 'Got resolution ' + this.resolution[0] + + 'x' + this.resolution[1] + ', expected resolution' + this.localVideo.width + 'x' + this.localVideo.height + ' or rotated version thereof'); } this.gatherStats(this.localPeerConnection, 100, 20000, this.reportTestDone.bind(this)); }, setup: function() { this.canvas = document.createElement('canvas'); this.canvas.width = localVideo.width; this.canvas.height = localVideo.height; this.context = this.canvas.getContext('2d'); this.remoteVideo.onloadedmetadata = this.collectAndAnalyzeStats.bind(this); this.localVideo.addEventListener('play', this.startCheckingVideoFrames.bind(this), false); }, startCheckingVideoFrames: function() { this.videoFrameChecker = setInterval(this.checkVideoFrame.bind(this), 20); }, run: function() { this.setup(); this.triggerGetUserMedia(this.resolution); }, triggerGetUserMedia: function(resolution) { var constraints = { audio: false, video: { mandatory: { minWidth: resolution[0], minHeight: resolution[1], maxWidth: resolution[0], maxHeight: resolution[1] } } }; try { this.doGetUserMedia(constraints, this.gotLocalStream.bind(this), this.onGetUserMediaError.bind(this)); } catch (exception) { console.log('Unexpected exception: ', exception); this.reportError('gUM', 'doGetUserMedia failed: ' + exception); } }, reportError: function(errorType, message) { this.results.cameraErrors.push([errorType, message]); console.log(message); }, doGetUserMedia: function(constraints, onSuccess, onFail) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; navigator.getUserMedia(constraints, onSuccess, onFail); }, gotLocalStream: function(stream) { this.localStream = stream; var 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.gotLocalDescription.bind(this)); this.localVideo.src = URL.createObjectURL(stream); this.results.cameraType = stream.getVideoTracks()[0].label; }, onGetUserMediaError: function(stream) { this.reportError('gUM', 'gUM call failed'); }, gatherStats: function(peerConnection, interval, durationMs, callback) { var startTime = new Date(); var pollFunction = setInterval(gatherOneReport.bind(this), interval); function gatherOneReport() { var elapsed = new Date() - startTime; if (elapsed > durationMs) { console.log('Done gathering stats.'); 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) { var reports = response.result(); for (var i = 0; i < reports.length; ++i) { var report = reports[i]; var incomingFps = this.getStatFromReport(report, 'googFrameRateInput'); if (incomingFps == null) { // Skip on null. continue; } var 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); testProgress = 1; }, processStats: function() { if (this.inFps != [] && this.outFps != []) { var minInFps = Math.min.apply(null, this.inFps); var maxInFps = Math.max.apply(null, this.inFps); var averageInFps = average(this.inFps); var minOutFps = Math.min.apply(null, this.outFps); var maxOutFps = Math.max.apply(null, this.outFps); var averageOutFps = average(this.outFps); this.results.peerConnectionStats = [minInFps, maxInFps, averageInFps, minOutFps, maxOutFps, averageOutFps]; } }, checkVideoFrame: function() { this.context.drawImage(this.localVideo, 0, 0, this.canvas.width, this.canvas.height); var imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height); if (isBlackFrame(imageData.data, imageData.data.length)) { this.results.frameStats.numBlackFrames++; } if (this.frameComparator.calculate(this.previousFrame, imageData.data) > this.identicalFrameSsimThreshold) { this.results.frameStats.numFrozenFrames++; } this.previousFrame = imageData.data; this.results.frameStats.numFrames++; }, isBlackFrame: function(data, length) { var accumulatedLuma = 0; for (var 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.src = URL.createObjectURL(event.stream); }, gotLocalDescription: function(description) { this.localPeerConnection.setLocalDescription(description); this.remotePeerConnection.setRemoteDescription(description); this.remotePeerConnection.createAnswer(this.gotRemoteDescription.bind( this)); }, gotRemoteDescription: function(description) { 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) { console.log("Something went wrong, here is the stack trace --> %s", error.stack); }; </script> </body> </html>