| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <style> | |
| body { | |
| color: white; | |
| background-color: black; | |
| } | |
| </style> | |
| </head> | |
| <body onload="main()"> | |
| <div id="buttons"></div> | |
| <table> | |
| <tr> | |
| <td>Image</td> | |
| <td id="video_header"></td> | |
| <td>Absolute Diff</td> | |
| <td>Different Pixels</td> | |
| </tr> | |
| <tr> | |
| <td><img src="blackwhite.png"></div> | |
| <td><video autoplay></video></div> | |
| <td><canvas id="diff"></canvas></td> | |
| <td><canvas id="mask"></canvas></td> | |
| </tr> | |
| </div> | |
| <p id="result"></p> | |
| <script> | |
| function log(str) { | |
| document.getElementById('result').textContent = str; | |
| console.log(str); | |
| } | |
| function loadVideo(name) { | |
| var videoElem = document.querySelector('video'); | |
| videoElem.src = 'blackwhite_' + name; | |
| document.getElementById('video_header').textContent = name; | |
| videoElem.addEventListener('ended', onVideoEnded); | |
| } | |
| function onVideoEnded(e) { | |
| document.title = verifyVideo() ? 'ENDED' : 'FAILED'; | |
| } | |
| function onVideoError(e) { | |
| document.title = 'ERROR'; | |
| document.getElementById('diff').style.visibility = 'hidden'; | |
| document.getElementById('mask').style.visibility = 'hidden'; | |
| log('Error playing video: ' + e.target.error.code + '.'); | |
| } | |
| function main() { | |
| // Programatically create buttons for each clip for manual testing. | |
| var buttonsElem = document.getElementById('buttons'); | |
| function createButton(name) { | |
| var buttonElem = document.createElement('button'); | |
| buttonElem.textContent = name; | |
| buttonElem.addEventListener('click', function() { | |
| loadVideo(name); | |
| }); | |
| buttonsElem.appendChild(buttonElem); | |
| } | |
| var VIDEOS = [ | |
| 'yuv420p.ogv', | |
| 'yuv422p.ogv', | |
| 'yuv444p.ogv', | |
| 'yuv420p.webm', | |
| 'yuv444p.webm', | |
| 'yuv420p.mp4', | |
| 'yuv420p_rec709.mp4', | |
| 'yuvj420p.mp4', | |
| 'yuv422p.mp4', | |
| 'yuv444p.mp4', | |
| 'yuv420p.avi' | |
| ]; | |
| for (var i = 0; i < VIDEOS.length; ++i) { | |
| createButton(VIDEOS[i]); | |
| } | |
| // Video event handlers. | |
| var videoElem = document.querySelector('video'); | |
| videoElem.addEventListener('error', onVideoError); | |
| // Check if a query parameter was provided for automated tests. | |
| if (window.location.search.length > 1) { | |
| loadVideo(window.location.search.substr(1)); | |
| } else { | |
| // If we're not an automated test, compute some pretty diffs. | |
| document.querySelector('video').addEventListener('ended', | |
| computeDiffs); | |
| } | |
| } | |
| function getCanvasPixels(canvas) { | |
| try { | |
| return canvas.getContext('2d') | |
| .getImageData(0, 0, canvas.width, canvas.height) | |
| .data; | |
| } catch(e) { | |
| var message = 'ERROR: ' + e; | |
| if (e.name == 'SecurityError') { | |
| message += ' Couldn\'t get image pixels, try running with ' + | |
| '--allow-file-access-from-files.'; | |
| } | |
| log(message); | |
| } | |
| } | |
| function verifyVideo() { | |
| var videoElem = document.querySelector('video'); | |
| var offscreen = document.createElement('canvas'); | |
| offscreen.width = videoElem.videoWidth; | |
| offscreen.height = videoElem.videoHeight; | |
| offscreen.getContext('2d').drawImage(videoElem, 0, 0, offscreen.width, | |
| offscreen.height); | |
| videoData = getCanvasPixels(offscreen); | |
| if (!videoData) | |
| return false; | |
| // Check the color of a givel pixel |x,y| in |imgData| against an | |
| // expected value, |expected|, with up to |allowedError| difference. | |
| function checkColor(imgData, x, y, stride, expected, allowedError) { | |
| for (var i = 0; i < 3; ++i) { | |
| var actual = imgData[(x + y * stride) * 4 + i]; | |
| if (Math.abs(actual - expected) > allowedError) { | |
| log('Color didn\'t match at (' + x + ', ' + y + '). Expected: ' + | |
| expected + ', actual: ' + actual); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Check one pixel in each quadrant (in the upper left, away from | |
| // boundaries and the text, to avoid compression artifacts). | |
| // Also allow a small error, for the same reason. | |
| // TODO(mtomasz): Once code.google.com/p/libyuv/issues/detail?id=324 is | |
| // fixed, the allowedError should be decreased to 1. | |
| var allowedError = 2; | |
| return checkColor(videoData, 30, 30, videoElem.videoWidth, 0xff, | |
| allowedError) && | |
| checkColor(videoData, 150, 30, videoElem.videoWidth, 0x00, | |
| allowedError) && | |
| checkColor(videoData, 30, 150, videoElem.videoWidth, 0x10, | |
| allowedError) && | |
| checkColor(videoData, 150, 150, videoElem.videoWidth, 0xef, | |
| allowedError); | |
| } | |
| // Compute a standard diff image, plus a high-contrast mask that shows | |
| // each differing pixel more visibly. | |
| function computeDiffs() { | |
| var diffElem = document.getElementById('diff'); | |
| var maskElem = document.getElementById('mask'); | |
| var videoElem = document.querySelector('video'); | |
| var imgElem = document.querySelector('img'); | |
| var width = imgElem.width; | |
| var height = imgElem.height; | |
| if (videoElem.videoWidth != width || videoElem.videoHeight != height) { | |
| log('ERROR: video dimensions don\'t match reference image ' + | |
| 'dimensions'); | |
| return; | |
| } | |
| // Make an offscreen canvas to dump reference image pixels into. | |
| var offscreen = document.createElement('canvas'); | |
| offscreen.width = width; | |
| offscreen.height = height; | |
| offscreen.getContext('2d').drawImage(imgElem, 0, 0, width, height); | |
| imgData = getCanvasPixels(offscreen); | |
| if (!imgData) | |
| return; | |
| // Scale and clear diff canvases. | |
| diffElem.width = maskElem.width = width; | |
| diffElem.height = maskElem.height = height; | |
| var diffCtx = diffElem.getContext('2d'); | |
| var maskCtx = maskElem.getContext('2d'); | |
| maskCtx.clearRect(0, 0, width, height); | |
| diffCtx.clearRect(0, 0, width, height); | |
| // Copy video pixels into diff. | |
| diffCtx.drawImage(videoElem, 0, 0, width, height); | |
| var diffIData = diffCtx.getImageData(0, 0, width, height); | |
| var diffData = diffIData.data; | |
| var maskIData = maskCtx.getImageData(0, 0, width, height); | |
| var maskData = maskIData.data; | |
| // Make diffs and collect stats. | |
| var meanSquaredError = 0; | |
| for (var i = 0; i < imgData.length; i += 4) { | |
| var difference = 0; | |
| for (var j = 0; j < 3; ++j) { | |
| diffData[i + j] = Math.abs(diffData[i + j] - imgData[i + j]); | |
| meanSquaredError += diffData[i + j] * diffData[i + j]; | |
| if (diffData[i + j] != 0) { | |
| difference += diffData[i + j]; | |
| } | |
| } | |
| if (difference > 0) { | |
| if (difference <= 3) { | |
| // If we're only off by a bit per channel or so, use darker red. | |
| maskData[i] = 128; | |
| } else { | |
| // Bright red to indicate a different pixel. | |
| maskData[i] = 255; | |
| } | |
| maskData[i+3] = 255; | |
| } | |
| } | |
| meanSquaredError /= width * height; | |
| log('Mean squared error: ' + meanSquaredError); | |
| diffCtx.putImageData(diffIData, 0, 0); | |
| maskCtx.putImageData(maskIData, 0, 0); | |
| document.getElementById('diff').style.visibility = 'visible'; | |
| document.getElementById('mask').style.visibility = 'visible'; | |
| } | |
| </script> | |
| </body> | |
| </html> |