<!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="gbrp.png"></div>
<td><video autoplay></video></div>
<td><canvas id="diff"></canvas></td>
<td><canvas id="mask"></canvas></td>
</tr>
<p id="result"></p>
<script>
function log(str) {
document.getElementById('result').textContent = str;
console.log(str);
}
function loadVideo(name) {
const videoElem = document.querySelector('video');
document.getElementById('video_header').textContent = name;
videoElem.src = 'gbrp-' + name;
}
function onVideoFrame(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() {
// Programmatically create buttons for each clip for manual testing.
const buttonsElem = document.getElementById('buttons');
function createButton(name) {
const buttonElem = document.createElement('button');
buttonElem.textContent = name;
buttonElem.addEventListener('click', function () {
loadVideo(name);
});
buttonsElem.appendChild(buttonElem);
}
const VIDEOS = ['av1.mp4', 'vp9.mp4', 'h264.mp4'];
const videoElem = document.querySelector('video');
if (videoElem.canPlayType('video/mp4;codecs="hvc1.4.10.L60.9e.8"')) {
VIDEOS.push('h265.mp4');
}
for (let i = 0; i < VIDEOS.length; ++i) {
createButton(VIDEOS[i]);
}
// Check if a query parameter was provided for automated tests.
if (window.location.search.length > 1) {
videoElem.addEventListener('error', onVideoError);
videoElem.requestVideoFrameCallback(onVideoFrame);
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) {
let 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() {
const videoElem = document.querySelector('video');
const 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 given pixel |x,y| in |imgData| against an
// expected RGB value, |expectedR, expectedG, expectedB|, with up to |allowedError| difference.
function checkColor(
imgData,
x,
y,
stride,
expectedR,
expectedG,
expectedB,
allowedError,
) {
const actualR = imgData[(x + y * stride) * 4];
const actualG = imgData[(x + y * stride) * 4 + 1];
const actualB = imgData[(x + y * stride) * 4 + 2];
if (
Math.abs(actualR - expectedR) > allowedError ||
Math.abs(actualG - expectedG) > allowedError ||
Math.abs(actualB - expectedB) > allowedError
) {
log(
'Color didn\'t match at (' +
x +
', ' +
y +
'). Expected: (' +
expectedR +
', ' +
expectedG +
', ' +
expectedB +
')' +
', actual: (' +
actualR +
', ' +
actualG +
', ' +
actualB +
').',
);
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.
const allowedError = 2;
return (
checkColor(
videoData,
20,
20,
videoElem.videoWidth,
0x00,
0x00,
0x00,
allowedError,
) &&
checkColor(
videoData,
60,
20,
videoElem.videoWidth,
0xff,
0x00,
0x00,
allowedError,
) &&
checkColor(
videoData,
100,
20,
videoElem.videoWidth,
0xff,
0x00,
0xff,
allowedError,
) &&
checkColor(
videoData,
140,
20,
videoElem.videoWidth,
0x00,
0x00,
0xff,
allowedError,
) &&
checkColor(
videoData,
180,
20,
videoElem.videoWidth,
0xff,
0xff,
0x00,
allowedError,
) &&
checkColor(
videoData,
220,
20,
videoElem.videoWidth,
0x00,
0xff,
0x00,
allowedError,
) &&
checkColor(
videoData,
260,
20,
videoElem.videoWidth,
0x00,
0xff,
0xff,
allowedError,
) &&
checkColor(
videoData,
300,
20,
videoElem.videoWidth,
0xff,
0xff,
0xff,
allowedError,
)
);
}
// Compute a standard diff image, plus a high-contrast mask that shows
// each differing pixel more visibly.
function computeDiffs() {
const diffElem = document.getElementById('diff');
const maskElem = document.getElementById('mask');
const videoElem = document.querySelector('video');
const imgElem = document.querySelector('img');
const width = imgElem.width;
const 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.
const 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;
const diffCtx = diffElem.getContext('2d');
const 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);
const diffIData = diffCtx.getImageData(0, 0, width, height);
const diffData = diffIData.data;
const maskIData = maskCtx.getImageData(0, 0, width, height);
const maskData = maskIData.data;
// Make diffs and collect stats.
let meanSquaredError = 0;
for (let i = 0; i < imgData.length; i += 4) {
let difference = 0;
for (let 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>