1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * Util base class to help test a captured canvas element. Initializes the
9 * output canvas (used for testing the color of video elements), and optionally
10 * overrides the default `createAndAppendElement` element |width| and |height|.
12 function CaptureStreamTestHelper(width, height) {
14 this.elemWidth = width;
17 this.elemHeight = height;
20 /* cout is used for `getPixel`; only needs to be big enough for one pixel */
21 this.cout = document.createElement("canvas");
26 CaptureStreamTestHelper.prototype = {
27 /* Predefined colors for use in the methods below. */
28 black: { data: [0, 0, 0, 255], name: "black" },
29 blackTransparent: { data: [0, 0, 0, 0], name: "blackTransparent" },
30 white: { data: [255, 255, 255, 255], name: "white" },
31 green: { data: [0, 255, 0, 255], name: "green" },
32 red: { data: [255, 0, 0, 255], name: "red" },
33 blue: { data: [0, 0, 255, 255], name: "blue" },
34 grey: { data: [128, 128, 128, 255], name: "grey" },
36 /* Default element size for createAndAppendElement() */
41 * Perform the drawing operation on each animation frame until stop is called
42 * on the returned object.
51 window.requestAnimationFrame(draw);
54 return { stop: () => (stop = true) };
57 /* Request a frame from the stream played by |video|. */
59 info("Requesting frame from " + video.id);
60 video.srcObject.requestFrame();
64 * Returns the pixel at (|offsetX|, |offsetY|) (from top left corner) of
65 * |video| as an array of the pixel's color channels: [R,G,B,A].
67 getPixel(video, offsetX = 0, offsetY = 0) {
68 // Avoids old values in case of a transparent image.
69 CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
71 var ctxout = this.cout.getContext("2d");
74 offsetX, // source x coordinate
75 offsetY, // source y coordinate
78 0, // destination x coordinate
79 0, // destination y coordinate
80 1, // destination width
82 ); // destination height
83 return ctxout.getImageData(0, 0, 1, 1).data;
87 * Returns true if px lies within the per-channel |threshold| of the
88 * referenced color for all channels. px is on the form of an array of color
89 * channels, [R,G,B,A]. Each channel is in the range [0, 255].
91 * Threshold defaults to 0 which is an exact match.
93 isPixel(px, refColor, threshold = 0) {
94 return px.every((ch, i) => Math.abs(ch - refColor.data[i]) <= threshold);
98 * Returns true if px lies further away than |threshold| of the
99 * referenced color for any channel. px is on the form of an array of color
100 * channels, [R,G,B,A]. Each channel is in the range [0, 255].
102 * Threshold defaults to 127 which should be far enough for most cases.
104 isPixelNot(px, refColor, threshold = 127) {
105 return px.some((ch, i) => Math.abs(ch - refColor.data[i]) > threshold);
109 * Behaves like isPixelNot but ignores the alpha channel.
111 isOpaquePixelNot(px, refColor, threshold) {
112 px[3] = refColor.data[3];
113 return this.isPixelNot(px, refColor, threshold);
117 * Returns a promise that resolves when the provided function |test|
118 * returns true, or rejects when the optional `cancel` promise resolves.
128 cancel = new Promise(() => {}),
132 cancel.then(e => (aborted = true));
136 new Promise(resolve =>
137 video.addEventListener("timeupdate", resolve, { once: true })
144 if (test(this.getPixel(video, offsetX, offsetY, width, height))) {
151 * Returns a promise that resolves when the top left pixel of |video| matches
152 * on all channels. Use |threshold| for fuzzy matching the color on each
153 * channel, in the range [0,255]. 0 means exact match, 255 accepts anything.
155 async pixelMustBecome(
158 { threshold = 0, infoString = "n/a", cancel = new Promise(() => {}) } = {}
161 "Waiting for video " +
164 refColor.data.join(",") +
171 var paintedFrames = video.mozPaintedFrames - 1;
172 await this.waitForPixel(
175 if (paintedFrames != video.mozPaintedFrames) {
178 video.mozPaintedFrames +
186 paintedFrames = video.mozPaintedFrames;
188 return this.isPixel(px, refColor, threshold);
198 ok(true, video.id + " " + infoString);
202 * Returns a promise that resolves after |time| ms of playback or when the
203 * top left pixel of |video| becomes |refColor|. The test is failed if the
204 * time is not reached, or if the cancel promise resolves.
206 async pixelMustNotBecome(
209 { threshold = 0, time = 5000, infoString = "n/a" } = {}
214 " to time out after " +
217 refColor.data.join(",") +
221 let timeout = new Promise(resolve => setTimeout(resolve, time));
222 let analysis = async () => {
223 await this.waitForPixel(
225 px => this.isPixel(px, refColor, threshold),
233 throw new Error("Got color " + refColor.name + ". " + infoString);
235 await Promise.race([timeout, analysis()]);
236 ok(true, video.id + " " + infoString);
239 /* Create an element of type |type| with id |id| and append it to the body. */
240 createAndAppendElement(type, id) {
241 var e = document.createElement(type);
243 e.width = this.elemWidth;
244 e.height = this.elemHeight;
245 if (type === "video") {
248 document.body.appendChild(e);
253 /* Sub class holding 2D-Canvas specific helpers. */
254 function CaptureStreamTestHelper2D(width, height) {
255 CaptureStreamTestHelper.call(this, width, height);
258 CaptureStreamTestHelper2D.prototype = Object.create(
259 CaptureStreamTestHelper.prototype
261 CaptureStreamTestHelper2D.prototype.constructor = CaptureStreamTestHelper2D;
263 /* Clear all drawn content on |canvas|. */
264 CaptureStreamTestHelper2D.prototype.clear = function (canvas) {
265 var ctx = canvas.getContext("2d");
266 ctx.clearRect(0, 0, canvas.width, canvas.height);
269 /* Draw the color |color| to the source canvas |canvas|. Format [R,G,B,A]. */
270 CaptureStreamTestHelper2D.prototype.drawColor = function (
276 width = canvas.width / 2,
277 height = canvas.height / 2,
280 var ctx = canvas.getContext("2d");
281 var rgba = color.data.slice(); // Copy to not overwrite the original array
282 rgba[3] = rgba[3] / 255.0; // Convert opacity to double in range [0,1]
283 info("Drawing color " + rgba.join(","));
284 ctx.fillStyle = "rgba(" + rgba.join(",") + ")";
286 // Only fill top left corner to test that output is not flipped or rotated.
287 ctx.fillRect(offsetX, offsetY, width, height);
290 /* Test that the given 2d canvas is NOT origin-clean. */
291 CaptureStreamTestHelper2D.prototype.testNotClean = function (canvas) {
292 var ctx = canvas.getContext("2d");
295 var data = ctx.getImageData(0, 0, 1, 1);
302 "Canvas '" + canvas.id + "' should not be origin-clean"
306 /* Sub class holding WebGL specific helpers. */
307 function CaptureStreamTestHelperWebGL(width, height) {
308 CaptureStreamTestHelper.call(this, width, height);
311 CaptureStreamTestHelperWebGL.prototype = Object.create(
312 CaptureStreamTestHelper.prototype
314 CaptureStreamTestHelperWebGL.prototype.constructor =
315 CaptureStreamTestHelperWebGL;
317 /* Set the (uniform) color location for future draw calls. */
318 CaptureStreamTestHelperWebGL.prototype.setFragmentColorLocation = function (
321 this.colorLocation = colorLocation;
324 /* Clear the given WebGL context with |color|. */
325 CaptureStreamTestHelperWebGL.prototype.clearColor = function (canvas, color) {
326 info("WebGL: clearColor(" + color.name + ")");
327 var gl = canvas.getContext("webgl");
328 var conv = color.data.map(i => i / 255.0);
329 gl.clearColor(conv[0], conv[1], conv[2], conv[3]);
330 gl.clear(gl.COLOR_BUFFER_BIT);
333 /* Set an already setFragmentColorLocation() to |color| and drawArrays() */
334 CaptureStreamTestHelperWebGL.prototype.drawColor = function (canvas, color) {
335 info("WebGL: drawArrays(" + color.name + ")");
336 var gl = canvas.getContext("webgl");
337 var conv = color.data.map(i => i / 255.0);
338 gl.uniform4f(this.colorLocation, conv[0], conv[1], conv[2], conv[3]);
339 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);