| <!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> |