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