captureStream_common.js (9984B)
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/. */ 4 5 "use strict"; 6 7 /* 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|. 11 */ 12 function CaptureStreamTestHelper(width, height) { 13 if (width) { 14 this.elemWidth = width; 15 } 16 if (height) { 17 this.elemHeight = height; 18 } 19 20 /* cout is used for `getPixel`; only needs to be big enough for one pixel */ 21 this.cout = document.createElement("canvas"); 22 this.cout.width = 1; 23 this.cout.height = 1; 24 } 25 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" }, 35 36 /* Default element size for createAndAppendElement() */ 37 elemWidth: 100, 38 elemHeight: 100, 39 40 /* 41 * Perform the drawing operation on each animation frame until stop is called 42 * on the returned object. 43 */ 44 startDrawing(f) { 45 var stop = false; 46 var draw = () => { 47 if (stop) { 48 return; 49 } 50 f(); 51 window.requestAnimationFrame(draw); 52 }; 53 draw(); 54 return { stop: () => (stop = true) }; 55 }, 56 57 /* Request a frame from the stream played by |video|. */ 58 requestFrame(video) { 59 info("Requesting frame from " + video.id); 60 video.srcObject.requestFrame(); 61 }, 62 63 /* 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]. 66 */ 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); 70 71 var ctxout = this.cout.getContext("2d"); 72 ctxout.drawImage( 73 video, 74 offsetX, // source x coordinate 75 offsetY, // source y coordinate 76 1, // source width 77 1, // source height 78 0, // destination x coordinate 79 0, // destination y coordinate 80 1, // destination width 81 1 82 ); // destination height 83 return ctxout.getImageData(0, 0, 1, 1).data; 84 }, 85 86 /* 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]. 90 * 91 * Threshold defaults to 0 which is an exact match. 92 */ 93 isPixel(px, refColor, threshold = 0) { 94 return px.every((ch, i) => Math.abs(ch - refColor.data[i]) <= threshold); 95 }, 96 97 /* 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]. 101 * 102 * Threshold defaults to 127 which should be far enough for most cases. 103 */ 104 isPixelNot(px, refColor, threshold = 127) { 105 return px.some((ch, i) => Math.abs(ch - refColor.data[i]) > threshold); 106 }, 107 108 /* 109 * Behaves like isPixelNot but ignores the alpha channel. 110 */ 111 isOpaquePixelNot(px, refColor, threshold) { 112 px[3] = refColor.data[3]; 113 return this.isPixelNot(px, refColor, threshold); 114 }, 115 116 /* 117 * Returns a promise that resolves when the provided function |test| 118 * returns true, or rejects when the optional `cancel` promise resolves. 119 */ 120 async waitForPixel( 121 video, 122 test, 123 { 124 offsetX = 0, 125 offsetY = 0, 126 width = 0, 127 height = 0, 128 cancel = new Promise(() => {}), 129 } = {} 130 ) { 131 let aborted = false; 132 cancel.then(e => (aborted = true)); 133 134 while (true) { 135 await Promise.race([ 136 new Promise(resolve => 137 video.addEventListener("timeupdate", resolve, { once: true }) 138 ), 139 cancel, 140 ]); 141 if (aborted) { 142 throw await cancel; 143 } 144 if (test(this.getPixel(video, offsetX, offsetY, width, height))) { 145 return; 146 } 147 } 148 }, 149 150 /* 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. 154 */ 155 async pixelMustBecome( 156 video, 157 refColor, 158 { threshold = 0, infoString = "n/a", cancel = new Promise(() => {}) } = {} 159 ) { 160 info( 161 "Waiting for video " + 162 video.id + 163 " to match [" + 164 refColor.data.join(",") + 165 "] - " + 166 refColor.name + 167 " (" + 168 infoString + 169 ")" 170 ); 171 var paintedFrames = video.mozPaintedFrames - 1; 172 await this.waitForPixel( 173 video, 174 px => { 175 if (paintedFrames != video.mozPaintedFrames) { 176 info( 177 "Frame: " + 178 video.mozPaintedFrames + 179 " IsPixel ref=" + 180 refColor.data + 181 " threshold=" + 182 threshold + 183 " value=" + 184 px 185 ); 186 paintedFrames = video.mozPaintedFrames; 187 } 188 return this.isPixel(px, refColor, threshold); 189 }, 190 { 191 offsetX: 0, 192 offsetY: 0, 193 width: 0, 194 height: 0, 195 cancel, 196 } 197 ); 198 ok(true, video.id + " " + infoString); 199 }, 200 201 /* 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. 205 */ 206 async pixelMustNotBecome( 207 video, 208 refColor, 209 { threshold = 0, time = 5000, infoString = "n/a" } = {} 210 ) { 211 info( 212 "Waiting for " + 213 video.id + 214 " to time out after " + 215 time + 216 "ms against [" + 217 refColor.data.join(",") + 218 "] - " + 219 refColor.name 220 ); 221 let timeout = new Promise(resolve => setTimeout(resolve, time)); 222 let analysis = async () => { 223 await this.waitForPixel( 224 video, 225 px => this.isPixel(px, refColor, threshold), 226 { 227 offsetX: 0, 228 offsetY: 0, 229 width: 0, 230 height: 0, 231 } 232 ); 233 throw new Error("Got color " + refColor.name + ". " + infoString); 234 }; 235 await Promise.race([timeout, analysis()]); 236 ok(true, video.id + " " + infoString); 237 }, 238 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); 242 e.id = id; 243 e.width = this.elemWidth; 244 e.height = this.elemHeight; 245 if (type === "video") { 246 e.autoplay = true; 247 } 248 document.body.appendChild(e); 249 return e; 250 }, 251 }; 252 253 /* Sub class holding 2D-Canvas specific helpers. */ 254 function CaptureStreamTestHelper2D(width, height) { 255 CaptureStreamTestHelper.call(this, width, height); 256 } 257 258 CaptureStreamTestHelper2D.prototype = Object.create( 259 CaptureStreamTestHelper.prototype 260 ); 261 CaptureStreamTestHelper2D.prototype.constructor = CaptureStreamTestHelper2D; 262 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); 267 }; 268 269 /* Draw the color |color| to the source canvas |canvas|. Format [R,G,B,A]. */ 270 CaptureStreamTestHelper2D.prototype.drawColor = function ( 271 canvas, 272 color, 273 { 274 offsetX = 0, 275 offsetY = 0, 276 width = canvas.width / 2, 277 height = canvas.height / 2, 278 } = {} 279 ) { 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(",") + ")"; 285 286 // Only fill top left corner to test that output is not flipped or rotated. 287 ctx.fillRect(offsetX, offsetY, width, height); 288 }; 289 290 /* Test that the given 2d canvas is NOT origin-clean. */ 291 CaptureStreamTestHelper2D.prototype.testNotClean = function (canvas) { 292 var ctx = canvas.getContext("2d"); 293 var error = "OK"; 294 try { 295 var data = ctx.getImageData(0, 0, 1, 1); 296 } catch (e) { 297 error = e.name; 298 } 299 is( 300 error, 301 "SecurityError", 302 "Canvas '" + canvas.id + "' should not be origin-clean" 303 ); 304 }; 305 306 /* Sub class holding WebGL specific helpers. */ 307 function CaptureStreamTestHelperWebGL(width, height) { 308 CaptureStreamTestHelper.call(this, width, height); 309 } 310 311 CaptureStreamTestHelperWebGL.prototype = Object.create( 312 CaptureStreamTestHelper.prototype 313 ); 314 CaptureStreamTestHelperWebGL.prototype.constructor = 315 CaptureStreamTestHelperWebGL; 316 317 /* Set the (uniform) color location for future draw calls. */ 318 CaptureStreamTestHelperWebGL.prototype.setFragmentColorLocation = function ( 319 colorLocation 320 ) { 321 this.colorLocation = colorLocation; 322 }; 323 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); 331 }; 332 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); 340 };