diff options
Diffstat (limited to 'dom/canvas/test/captureStream_common.js')
-rw-r--r-- | dom/canvas/test/captureStream_common.js | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/dom/canvas/test/captureStream_common.js b/dom/canvas/test/captureStream_common.js new file mode 100644 index 0000000000..a3c66133e3 --- /dev/null +++ b/dom/canvas/test/captureStream_common.js @@ -0,0 +1,340 @@ +/* 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" }, + white: { data: [255, 255, 255, 255], name: "white" }, + 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(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(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(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(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(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(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. + */ + async waitForPixel( + 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; + } + if (test(this.getPixel(video, offsetX, offsetY, width, height))) { + return; + } + } + }, + + /* + * 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. + */ + async pixelMustBecome( + 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. + */ + async pixelMustNotBecome( + 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(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); +}; |