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