fune/dom/canvas/test/captureStream_common.js
Andreas Pehrson f421080879 Bug 1380346 - Let CaptureStreamTestHelper2D.drawColor draw squares wherever you want. r=jib
It was supporting a simpler case of only drawing in the upper left corner of
the input canvas. This supports that by default still, but also allows the
caller to exactly specify coordinates and size of the rectangle to draw.

MozReview-Commit-ID: GVQh0HqejqU

--HG--
extra : rebase_source : fb48fd1681f0545c53b5cb49b2791f42270ca83c
2017-09-14 19:00:20 +02:00

280 lines
9.9 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/*
* Util base class to help test a captured canvas element. Initializes the
* output canvas (used for testing the color of video elements), and optionally
* overrides the default `createAndAppendElement` element |width| and |height|.
*/
function CaptureStreamTestHelper(width, height) {
if (width) {
this.elemWidth = width;
}
if (height) {
this.elemHeight = height;
}
/* cout is used for `getPixel`; only needs to be big enough for one pixel */
this.cout = document.createElement('canvas');
this.cout.width = 1;
this.cout.height = 1;
}
CaptureStreamTestHelper.prototype = {
/* Predefined colors for use in the methods below. */
black: { data: [0, 0, 0, 255], name: "black" },
blackTransparent: { data: [0, 0, 0, 0], name: "blackTransparent" },
green: { data: [0, 255, 0, 255], name: "green" },
red: { data: [255, 0, 0, 255], name: "red" },
blue: { data: [0, 0, 255, 255], name: "blue"},
grey: { data: [128, 128, 128, 255], name: "grey" },
/* Default element size for createAndAppendElement() */
elemWidth: 100,
elemHeight: 100,
/*
* Perform the drawing operation on each animation frame until stop is called
* on the returned object.
*/
startDrawing: function (f) {
var stop = false;
var draw = () => {
if (stop) { return; }
f();
window.requestAnimationFrame(draw);
};
draw();
return { stop: () => stop = true };
},
/* Request a frame from the stream played by |video|. */
requestFrame: function (video) {
info("Requesting frame from " + video.id);
video.srcObject.requestFrame();
},
/*
* Returns the pixel at (|offsetX|, |offsetY|) (from top left corner) of
* |video| as an array of the pixel's color channels: [R,G,B,A].
*/
getPixel: function (video, offsetX = 0, offsetY = 0) {
// Avoids old values in case of a transparent image.
CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
var ctxout = this.cout.getContext('2d');
ctxout.drawImage(video,
offsetX, // source x coordinate
offsetY, // source y coordinate
1, // source width
1, // source height
0, // destination x coordinate
0, // destination y coordinate
1, // destination width
1); // destination height
return ctxout.getImageData(0, 0, 1, 1).data;
},
/*
* Returns true if px lies within the per-channel |threshold| of the
* referenced color for all channels. px is on the form of an array of color
* channels, [R,G,B,A]. Each channel is in the range [0, 255].
*
* Threshold defaults to 0 which is an exact match.
*/
isPixel: function (px, refColor, threshold = 0) {
return px.every((ch, i) => Math.abs(ch - refColor.data[i]) <= threshold);
},
/*
* Returns true if px lies further away than |threshold| of the
* referenced color for any channel. px is on the form of an array of color
* channels, [R,G,B,A]. Each channel is in the range [0, 255].
*
* Threshold defaults to 127 which should be far enough for most cases.
*/
isPixelNot: function (px, refColor, threshold = 127) {
return px.some((ch, i) => Math.abs(ch - refColor.data[i]) > threshold);
},
/*
* Behaves like isPixelNot but ignores the alpha channel.
*/
isOpaquePixelNot: function(px, refColor, threshold) {
px[3] = refColor.data[3];
return this.isPixelNot(px, refColor, threshold);
},
/*
* Returns a promise that resolves when the provided function |test|
* returns true, or rejects when the optional `cancel` promise resolves.
*/
waitForPixel: async function (video, test, {
offsetX = 0, offsetY = 0,
width = 0, height = 0,
cancel = new Promise(() => {}),
} = {}) {
let aborted = false;
cancel.then(e => aborted = true);
while (true) {
await Promise.race([
new Promise(resolve => video.addEventListener("timeupdate", resolve, { once: true })),
cancel,
]);
if (aborted) {
throw await cancel;
}
try {
if (test(this.getPixel(video, offsetX, offsetY, width, height))) {
return;
}
} catch (e) {
info("Waiting for pixel but no video available: " + e + "\n" + e.stack);
}
}
},
/*
* Returns a promise that resolves when the top left pixel of |video| matches
* on all channels. Use |threshold| for fuzzy matching the color on each
* channel, in the range [0,255]. 0 means exact match, 255 accepts anything.
*/
pixelMustBecome: async function (video, refColor, {
threshold = 0, infoString = "n/a",
cancel = new Promise(() => {}),
} = {}) {
info("Waiting for video " + video.id + " to match [" +
refColor.data.join(',') + "] - " + refColor.name +
" (" + infoString + ")");
var paintedFrames = video.mozPaintedFrames-1;
await this.waitForPixel(video, px => {
if (paintedFrames != video.mozPaintedFrames) {
info("Frame: " + video.mozPaintedFrames +
" IsPixel ref=" + refColor.data +
" threshold=" + threshold +
" value=" + px);
paintedFrames = video.mozPaintedFrames;
}
return this.isPixel(px, refColor, threshold);
}, {
offsetX: 0, offsetY: 0,
width: 0, height: 0,
cancel,
});
ok(true, video.id + " " + infoString);
},
/*
* Returns a promise that resolves after |time| ms of playback or when the
* top left pixel of |video| becomes |refColor|. The test is failed if the
* time is not reached, or if the cancel promise resolves.
*/
pixelMustNotBecome: async function (video, refColor, {
threshold = 0, time = 5000,
infoString = "n/a",
} = {}) {
info("Waiting for " + video.id + " to time out after " + time +
"ms against [" + refColor.data.join(',') + "] - " + refColor.name);
let timeout = new Promise(resolve => setTimeout(resolve, time));
let analysis = async () => {
await this.waitForPixel(video, px => this.isPixel(px, refColor, threshold), {
offsetX: 0, offsetY: 0, width: 0, height: 0,
});
throw new Error("Got color " + refColor.name + ". " + infoString);
};
await Promise.race([timeout, analysis()]);
ok(true, video.id + " " + infoString);
},
/* Create an element of type |type| with id |id| and append it to the body. */
createAndAppendElement: function (type, id) {
var e = document.createElement(type);
e.id = id;
e.width = this.elemWidth;
e.height = this.elemHeight;
if (type === 'video') {
e.autoplay = true;
}
document.body.appendChild(e);
return e;
},
}
/* Sub class holding 2D-Canvas specific helpers. */
function CaptureStreamTestHelper2D(width, height) {
CaptureStreamTestHelper.call(this, width, height);
}
CaptureStreamTestHelper2D.prototype = Object.create(CaptureStreamTestHelper.prototype);
CaptureStreamTestHelper2D.prototype.constructor = CaptureStreamTestHelper2D;
/* Clear all drawn content on |canvas|. */
CaptureStreamTestHelper2D.prototype.clear = function(canvas) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
/* Draw the color |color| to the source canvas |canvas|. Format [R,G,B,A]. */
CaptureStreamTestHelper2D.prototype.drawColor = function(canvas, color,
{ offsetX = 0,
offsetY = 0,
width = canvas.width / 2,
height = canvas.height / 2,
} = {}) {
var ctx = canvas.getContext('2d');
var rgba = color.data.slice(); // Copy to not overwrite the original array
rgba[3] = rgba[3] / 255.0; // Convert opacity to double in range [0,1]
info("Drawing color " + rgba.join(','));
ctx.fillStyle = "rgba(" + rgba.join(',') + ")";
// Only fill top left corner to test that output is not flipped or rotated.
ctx.fillRect(offsetX, offsetY, width, height);
};
/* Test that the given 2d canvas is NOT origin-clean. */
CaptureStreamTestHelper2D.prototype.testNotClean = function(canvas) {
var ctx = canvas.getContext('2d');
var error = "OK";
try {
var data = ctx.getImageData(0, 0, 1, 1);
} catch(e) {
error = e.name;
}
is(error, "SecurityError",
"Canvas '" + canvas.id + "' should not be origin-clean");
};
/* Sub class holding WebGL specific helpers. */
function CaptureStreamTestHelperWebGL(width, height) {
CaptureStreamTestHelper.call(this, width, height);
}
CaptureStreamTestHelperWebGL.prototype = Object.create(CaptureStreamTestHelper.prototype);
CaptureStreamTestHelperWebGL.prototype.constructor = CaptureStreamTestHelperWebGL;
/* Set the (uniform) color location for future draw calls. */
CaptureStreamTestHelperWebGL.prototype.setFragmentColorLocation = function(colorLocation) {
this.colorLocation = colorLocation;
};
/* Clear the given WebGL context with |color|. */
CaptureStreamTestHelperWebGL.prototype.clearColor = function(canvas, color) {
info("WebGL: clearColor(" + color.name + ")");
var gl = canvas.getContext('webgl');
var conv = color.data.map(i => i / 255.0);
gl.clearColor(conv[0], conv[1], conv[2], conv[3]);
gl.clear(gl.COLOR_BUFFER_BIT);
};
/* Set an already setFragmentColorLocation() to |color| and drawArrays() */
CaptureStreamTestHelperWebGL.prototype.drawColor = function(canvas, color) {
info("WebGL: drawArrays(" + color.name + ")");
var gl = canvas.getContext('webgl');
var conv = color.data.map(i => i / 255.0);
gl.uniform4f(this.colorLocation, conv[0], conv[1], conv[2], conv[3]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};