1<!DOCTYPE html> 2<html> 3<head><title>Loopback test</title></head> 4<body> 5 <video id="localVideo" width="1280" height="720" autoplay muted></video> 6 <video id="remoteVideo" width="1280" height="720" autoplay muted></video> 7<script src="ssim.js"></script> 8<script src="blackframe.js"></script> 9<script> 10 11 12var results = {}; 13var testProgress = 0; 14 15// Starts the test. 16function testCamera(resolution) { 17 var test = new CameraTest(resolution); 18 test.run(); 19} 20 21// Returns the results to caller. 22function getResults() { 23 return results; 24} 25 26function setResults(stats) { 27 results = stats; 28} 29 30// Calculates averages of array values. 31function average(array) { 32 var count = array.length; 33 var total = 0; 34 for (var i = 0; i < count; i++) { 35 total += parseInt(array[i]); 36 } 37 return Math.floor(total / count); 38} 39 40// Actual test object. 41function CameraTest(resolutionArray) { 42 this.resolution = resolutionArray; 43 this.localStream = null; 44 this.remoteStream = null; 45 this.results = {cameraType: '', cameraErrors: [], peerConnectionStats: [], 46 frameStats: {numBlackFrames: 0, numFrozenFrames:0, numFrames: 0}}; 47 48 this.inFps = []; 49 this.outFps = []; 50 // Variables associated with nearly-frozen frames detection. 51 this.previousFrame = []; 52 this.identicalFrameSsimThreshold = 0.985; 53 this.frameComparator = new Ssim(); 54 55 this.remoteVideo = document.getElementById("remoteVideo"); 56 this.localVideo = document.getElementById("localVideo"); 57 this.localVideo.width = this.resolution[0].toString(); 58 this.localVideo.height = this.resolution[1].toString(); 59 this.remoteVideo.width = this.resolution[0].toString(); 60 this.remoteVideo.height = this.resolution[1].toString(); 61} 62 63function resolutionMatchesIndependentOfRotation(aWidth, aHeight, 64 bWidth, bHeight) { 65 return (aWidth === bWidth && aHeight === bHeight) || 66 (aWidth === bHeight && aHeight === bWidth); 67} 68 69CameraTest.prototype = { 70 collectAndAnalyzeStats: function() { 71 if (!resolutionMatchesIndependentOfRotation(this.localVideo.width, 72 this.localVideo.height, this.resolution[0], this.resolution[1])) { 73 this.reportError('resolution', 'Got resolution ' + this.resolution[0] + 74 + 'x' + this.resolution[1] + ', expected resolution' + 75 this.localVideo.width + 'x' + this.localVideo.height + 76 ' or rotated version thereof'); 77 } 78 this.gatherStats(this.localPeerConnection, 100, 20000, 79 this.reportTestDone.bind(this)); 80 }, 81 82 setup: function() { 83 this.canvas = document.createElement('canvas'); 84 this.canvas.width = localVideo.width; 85 this.canvas.height = localVideo.height; 86 this.context = this.canvas.getContext('2d'); 87 this.remoteVideo.onloadedmetadata = this.collectAndAnalyzeStats.bind(this); 88 this.localVideo.addEventListener('play', 89 this.startCheckingVideoFrames.bind(this), false); 90 }, 91 92 startCheckingVideoFrames: function() { 93 this.videoFrameChecker = setInterval(this.checkVideoFrame.bind(this), 20); 94 }, 95 96 run: function() { 97 this.setup(); 98 this.triggerGetUserMedia(this.resolution); 99 }, 100 101 triggerGetUserMedia: function(resolution) { 102 var constraints = { 103 audio: false, 104 video: { 105 mandatory: { 106 minWidth: resolution[0], 107 minHeight: resolution[1], 108 maxWidth: resolution[0], 109 maxHeight: resolution[1] 110 } 111 } 112 }; 113 try { 114 this.doGetUserMedia(constraints, this.gotLocalStream.bind(this), 115 this.onGetUserMediaError.bind(this)); 116 } catch (exception) { 117 console.log('Unexpected exception: ', exception); 118 this.reportError('gUM', 'doGetUserMedia failed: ' + exception); 119 } 120 }, 121 122 reportError: function(errorType, message) { 123 this.results.cameraErrors.push([errorType, message]); 124 console.log(message); 125 }, 126 127 doGetUserMedia: function(constraints, onSuccess, onFail) { 128 navigator.getUserMedia = navigator.getUserMedia || 129 navigator.webkitGetUserMedia; 130 navigator.getUserMedia(constraints, onSuccess, onFail); 131 }, 132 133 gotLocalStream: function(stream) { 134 this.localStream = stream; 135 var servers = null; 136 137 this.localPeerConnection = new webkitRTCPeerConnection(servers); 138 this.localPeerConnection.onicecandidate = this.gotLocalIceCandidate.bind( 139 this); 140 141 this.remotePeerConnection = new webkitRTCPeerConnection(servers); 142 this.remotePeerConnection.onicecandidate = this.gotRemoteIceCandidate.bind( 143 this); 144 this.remotePeerConnection.onaddstream = this.gotRemoteStream.bind(this); 145 146 this.localPeerConnection.addStream(this.localStream); 147 this.localPeerConnection.createOffer(this.gotLocalDescription.bind(this)); 148 this.localVideo.src = URL.createObjectURL(stream); 149 150 this.results.cameraType = stream.getVideoTracks()[0].label; 151 }, 152 153 onGetUserMediaError: function(stream) { 154 this.reportError('gUM', 'gUM call failed'); 155 }, 156 157 gatherStats: function(peerConnection, interval, durationMs, callback) { 158 var startTime = new Date(); 159 var pollFunction = setInterval(gatherOneReport.bind(this), interval); 160 function gatherOneReport() { 161 var elapsed = new Date() - startTime; 162 if (elapsed > durationMs) { 163 console.log('Done gathering stats.'); 164 clearInterval(pollFunction); 165 callback(); 166 return; 167 } 168 peerConnection.getStats(this.gotStats.bind(this)); 169 } 170 }, 171 172 getStatFromReport: function(data, name) { 173 if (data.type = 'ssrc' && data.stat(name)) { 174 return data.stat(name); 175 } else { 176 return null; 177 } 178 }, 179 180 gotStats: function(response) { 181 var reports = response.result(); 182 for (var i = 0; i < reports.length; ++i) { 183 var report = reports[i]; 184 var incomingFps = this.getStatFromReport(report, 'googFrameRateInput'); 185 if (incomingFps == null) { 186 // Skip on null. 187 continue; 188 } 189 var outgoingFps = this.getStatFromReport(report, 'googFrameRateSent'); 190 // Save rates for later processing. 191 this.inFps.push(incomingFps) 192 this.outFps.push(outgoingFps); 193 } 194 }, 195 196 reportTestDone: function() { 197 this.processStats(); 198 199 clearInterval(this.videoFrameChecker); 200 201 setResults(this.results); 202 203 testProgress = 1; 204 }, 205 206 processStats: function() { 207 if (this.inFps != [] && this.outFps != []) { 208 var minInFps = Math.min.apply(null, this.inFps); 209 var maxInFps = Math.max.apply(null, this.inFps); 210 var averageInFps = average(this.inFps); 211 var minOutFps = Math.min.apply(null, this.outFps); 212 var maxOutFps = Math.max.apply(null, this.outFps); 213 var averageOutFps = average(this.outFps); 214 this.results.peerConnectionStats = [minInFps, maxInFps, averageInFps, 215 minOutFps, maxOutFps, averageOutFps]; 216 } 217 }, 218 219 checkVideoFrame: function() { 220 this.context.drawImage(this.localVideo, 0, 0, this.canvas.width, 221 this.canvas.height); 222 var imageData = this.context.getImageData(0, 0, this.canvas.width, 223 this.canvas.height); 224 225 if (isBlackFrame(imageData.data, imageData.data.length)) { 226 this.results.frameStats.numBlackFrames++; 227 } 228 229 if (this.frameComparator.calculate(this.previousFrame, imageData.data) > 230 this.identicalFrameSsimThreshold) { 231 this.results.frameStats.numFrozenFrames++; 232 } 233 234 this.previousFrame = imageData.data; 235 this.results.frameStats.numFrames++; 236 }, 237 238 isBlackFrame: function(data, length) { 239 var accumulatedLuma = 0; 240 for (var i = 4; i < length; i += 4) { 241 // Use Luma as in Rec. 709: Y′709 = 0.21R + 0.72G + 0.07B; 242 accumulatedLuma += (0.21 * data[i] + 0.72 * data[i + 1] 243 + 0.07 * data[i + 2]); 244 // Early termination if the average Luma so far is bright enough. 245 if (accumulatedLuma > (this.nonBlackPixelLumaThreshold * i / 4)) { 246 return false; 247 } 248 } 249 return true; 250 }, 251 252 gotRemoteStream: function(event) { 253 this.remoteVideo.src = URL.createObjectURL(event.stream); 254 }, 255 256 gotLocalDescription: function(description) { 257 this.localPeerConnection.setLocalDescription(description); 258 this.remotePeerConnection.setRemoteDescription(description); 259 this.remotePeerConnection.createAnswer(this.gotRemoteDescription.bind( 260 this)); 261 }, 262 263 gotRemoteDescription: function(description) { 264 this.remotePeerConnection.setLocalDescription(description); 265 this.localPeerConnection.setRemoteDescription(description); 266 }, 267 268 gotLocalIceCandidate: function(event) { 269 if (event.candidate) 270 this.remotePeerConnection.addIceCandidate( 271 new RTCIceCandidate(event.candidate)); 272 }, 273 274 gotRemoteIceCandidate: function(event) { 275 if (event.candidate) 276 this.localPeerConnection.addIceCandidate( 277 new RTCIceCandidate(event.candidate)); 278 }, 279} 280 281window.onerror = function (message, filename, lineno, colno, error) { 282 console.log("Something went wrong, here is the stack trace --> %s", 283 error.stack); 284}; 285</script> 286</body> 287</html> 288