tor-browser

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

videoFrame-copyTo-rgb.any.js (7928B)


      1 // META: global=window,dedicatedworker
      2 // META: script=/webcodecs/videoFrame-utils.js
      3 // META: script=/webcodecs/video-encoder-utils.js
      4 
      5 const smpte170m = {
      6  matrix: 'smpte170m',
      7  primaries: 'smpte170m',
      8  transfer: 'smpte170m',
      9  fullRange: false
     10 };
     11 const bt709 = {
     12  matrix: 'bt709',
     13  primaries: 'bt709',
     14  transfer: 'bt709',
     15  fullRange: false
     16 };
     17 
     18 function compareColors(actual, expected, tolerance, msg) {
     19  let channel = ['R', 'G', 'B', 'A'];
     20  for (let i = 0; i < 4; i++) {
     21    assert_approx_equals(
     22        actual[i], expected[i], tolerance,
     23        `${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`);
     24  }
     25 }
     26 
     27 function rgb2yuv(r, g, b) {
     28  let y = r * .299000 + g * .587000 + b * .114000
     29  let u = r * -.168736 + g * -.331264 + b * .500000 + 128
     30  let v = r * .500000 + g * -.418688 + b * -.081312 + 128
     31 
     32  y = Math.round(y);
     33  u = Math.round(u);
     34  v = Math.round(v);
     35  return {
     36    y, u, v
     37  }
     38 }
     39 
     40 function makeI420Frames(colorSpace) {
     41  const kYellow = {r: 0xFF, g: 0xFF, b: 0x00};
     42  const kRed = {r: 0xFF, g: 0x00, b: 0x00};
     43  const kBlue = {r: 0x00, g: 0x00, b: 0xFF};
     44  const kGreen = {r: 0x00, g: 0xFF, b: 0x00};
     45  const kPink = {r: 0xFF, g: 0x78, b: 0xFF};
     46  const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF};
     47  const kBlack = {r: 0x00, g: 0x00, b: 0x00};
     48  const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF};
     49 
     50  const result = [];
     51  const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4};
     52  const colors =
     53      [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink];
     54  const data = new Uint8Array(24);
     55  init.colorSpace = colorSpace;
     56  for (let color of colors) {
     57    color = rgb2yuv(color.r, color.g, color.b);
     58    data.fill(color.y, 0, 16);
     59    data.fill(color.u, 16, 20);
     60    data.fill(color.v, 20, 24);
     61    result.push(new VideoFrame(data, init));
     62  }
     63  return result;
     64 }
     65 
     66 function makeRGBXFrames(colorSpace) {
     67  const kYellow = 0xFFFF00;
     68  const kRed = 0xFF0000;
     69  const kBlue = 0x0000FF;
     70  const kGreen = 0x00FF00;
     71  const kBlack = 0x000000;
     72  const kWhite = 0xFFFFFF;
     73 
     74  const result = [];
     75  const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
     76  const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite];
     77  const data = new Uint32Array(16);
     78  init.colorSpace = colorSpace;
     79  for (let color of colors) {
     80    data.fill(color, 0, 16);
     81    result.push(new VideoFrame(data, init));
     82  }
     83  return result;
     84 }
     85 
     86 async function testFrame(frame, colorSpace, pixelFormat) {
     87  const width = frame.visibleRect.width;
     88  const height = frame.visibleRect.height;
     89  let frame_message = 'Frame: ' + JSON.stringify({
     90    format: frame.format,
     91    width: width,
     92    height: height,
     93    matrix: frame.colorSpace?.matrix,
     94    primaries: frame.colorSpace?.primaries,
     95    transfer: frame.colorSpace?.transfer,
     96  });
     97  const cnv = new OffscreenCanvas(width, height);
     98  const ctx =
     99      cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true});
    100 
    101  // Read VideoFrame pixels via copyTo()
    102  let imageData = ctx.createImageData(width, height);
    103  let copy_to_buf = imageData.data.buffer;
    104  let layout = null;
    105  try {
    106    const options = {
    107      rect: {x: 0, y: 0, width: width, height: height},
    108      format: pixelFormat,
    109      colorSpace: colorSpace
    110    };
    111    assert_equals(frame.allocationSize(options), copy_to_buf.byteLength);
    112    layout = await frame.copyTo(copy_to_buf, options);
    113  } catch (e) {
    114    assert_unreached(`copyTo() failure: ${e}`);
    115    return;
    116  }
    117  if (layout.length != 1) {
    118    assert_unreached('Conversion to RGB is not supported by the browser');
    119    return;
    120  }
    121 
    122  // Read VideoFrame pixels via drawImage()
    123  ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height);
    124  imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace});
    125  let get_image_buf = imageData.data.buffer;
    126 
    127  // Compare!
    128  const tolerance = 1;
    129  for (let i = 0; i < copy_to_buf.byteLength; i += 4) {
    130    if (pixelFormat.startsWith('BGR')) {
    131      // getImageData() always gives us RGB, we need to swap bytes before
    132      // comparing them with BGR.
    133      new Uint8Array(get_image_buf, i, 3).reverse();
    134    }
    135    compareColors(
    136        new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4),
    137        tolerance, frame_message + ` Mismatch at offset ${i}`);
    138  }
    139 }
    140 
    141 function test_4x4_I420_frames() {
    142  for (let colorSpace of ['srgb', 'display-p3']) {
    143    for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
    144      for (let frameColorSpace of [null, smpte170m, bt709]) {
    145        const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null";
    146        promise_test(async t => {
    147          for (let frame of makeI420Frames(frameColorSpace)) {
    148            await testFrame(frame, colorSpace, pixelFormat);
    149            frame.close();
    150          }
    151        }, `Convert 4x4 ${frameColorSpaceName} I420 frames to ${pixelFormat} / ${colorSpace}`);
    152      }
    153    }
    154  }
    155 }
    156 test_4x4_I420_frames();
    157 
    158 function test_4x4_RGB_frames() {
    159  for (let colorSpace of ['srgb', 'display-p3']) {
    160    for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
    161      for (let frameColorSpace of [null, smpte170m, bt709]) {
    162        const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null";
    163        promise_test(async t => {
    164          for (let frame of makeRGBXFrames(frameColorSpace)) {
    165            await testFrame(frame, colorSpace, pixelFormat);
    166            frame.close();
    167          }
    168        }, `Convert 4x4 ${frameColorSpaceName} RGBX frames to ${pixelFormat} / ${colorSpace}`);
    169      }
    170    }
    171  }
    172 }
    173 test_4x4_RGB_frames();
    174 
    175 
    176 function test_4color_canvas_frames() {
    177  for (let colorSpace of ['srgb', 'display-p3']) {
    178    for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
    179      promise_test(async t => {
    180        const frame = createFrame(32, 16);
    181        await testFrame(frame, colorSpace, pixelFormat);
    182        frame.close();
    183      }, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`);
    184    }
    185  }
    186 }
    187 test_4color_canvas_frames();
    188 
    189 promise_test(async t => {
    190  let pixelFormat = 'RGBA'
    191  const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4};
    192  const src_data = new Uint32Array(init.codedWidth * init.codedHeight);
    193  src_data.fill(0xFFFFFFFF);
    194  const offset = 5;
    195  const stride = 40;
    196  const dst_data = new Uint8Array(offset + stride * init.codedHeight);
    197  const options = {
    198    format: pixelFormat,
    199    layout: [
    200      {offset: offset, stride: stride},
    201    ]
    202  };
    203  const frame = new VideoFrame(src_data, init);
    204  await frame.copyTo(dst_data, options)
    205  assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset');
    206  for (let row = 0; row < init.codedHeight; ++row) {
    207    let width = init.codedWidth * 4;
    208    const row_data =
    209        dst_data.slice(offset + stride * row, offset + stride * row + width);
    210    const margin_data = dst_data.slice(
    211        offset + stride * row + width, offset + stride * (row + 1));
    212 
    213    assert_false(
    214        row_data.some(e => e != 0xFF),
    215        `unexpected data in row ${row} [${row_data}]`);
    216    assert_false(
    217        margin_data.some(e => e != 0),
    218        `unexpected margin in row ${row} [${margin_data}]`);
    219  }
    220 
    221  frame.close();
    222 }, `copyTo() with layout`);
    223 
    224 function test_unsupported_pixel_formats() {
    225  const kUnsupportedFormats = [
    226    'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A',
    227    'NV12'
    228  ];
    229 
    230  for (let pixelFormat of kUnsupportedFormats) {
    231    promise_test(async t => {
    232      const init =
    233          {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
    234      const data = new Uint32Array(16);
    235      const options = {format: pixelFormat};
    236      const frame = new VideoFrame(data, init);
    237      assert_throws_dom(
    238        'NotSupportedError', () => frame.allocationSize(options));
    239      await promise_rejects_dom(
    240          t, 'NotSupportedError', frame.copyTo(data, options))
    241      frame.close();
    242    }, `Unsupported format ${pixelFormat}`);
    243  }
    244 }
    245 test_unsupported_pixel_formats();