tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };