chromium/content/test/data/gpu/webcodecs/convert-to-rgb.js

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

function rgb2yuv(r, g, b) {
  let y = r * .299000 + g * .587000 + b * .114000
  let u = r * -.168736 + g * -.331264 + b * .500000 + 128
  let v = r * .500000 + g * -.418688 + b * -.081312 + 128

  y = Math.round(y);
  u = Math.round(u);
  v = Math.round(v);
  return {
    y, u, v
  }
}

function makeI420_frames() {
  const kYellow = {r: 0xFF, g: 0xFF, b: 0x00};
  const kRed = {r: 0xFF, g: 0x00, b: 0x00};
  const kBlue = {r: 0x00, g: 0x00, b: 0xFF};
  const kGreen = {r: 0x00, g: 0xFF, b: 0x00};
  const kPink = {r: 0xFF, g: 0x78, b: 0xFF};
  const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF};
  const kBlack = {r: 0x00, g: 0x00, b: 0x00};
  const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF};
  const smpte170m = {
    matrix: 'smpte170m',
    primaries: 'smpte170m',
    transfer: 'smpte170m',
    fullRange: false
  };
  const bt709 = {
    matrix: 'bt709',
    primaries: 'bt709',
    transfer: 'bt709',
    fullRange: false
  };

  const result = [];
  const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4};
  const colors =
      [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink];
  const data = new Uint8Array(24);
  for (let colorSpace of [null, smpte170m, bt709]) {
    init.colorSpace = colorSpace;
    result.push(new VideoFrame(data, init));
    for (let color of colors) {
      color = rgb2yuv(color.r, color.g, color.b);
      data.fill(color.y, 0, 16);
      data.fill(color.u, 16, 20);
      data.fill(color.v, 20, 24);
      result.push(new VideoFrame(data, init));
    }
  }
  return result;
}

async function test_frame(frame, colorSpace) {
  const width = frame.visibleRect.width;
  const height = frame.visibleRect.height;
  const frame_description = JSON.stringify({
    format: frame.format,
    width: width,
    height: height,
    codedHeight: frame.codedHeight,
    codedWidth: frame.codedWidth,
    displayHeight: frame.displayHeight,
    displayWidth: frame.displayWidth,
    matrix: frame.colorSpace?.matrix,
    primaries: frame.colorSpace?.primaries,
    transfer: frame.colorSpace?.transfer
  });
  TEST.log(`Test color: ${colorSpace} frame:${frame_description}`);
  const cnv = new OffscreenCanvas(width, height);
  const ctx =
      cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true});

  // Read VideoFrame pixels via copyTo()
  let imageData = ctx.createImageData(width, height);
  let copy_to_buf = imageData.data.buffer;
  let layout = null;
  try {
    const options = {
      rect: {x: 0, y: 0, width: width, height: height},
      format: 'RGBA',
      colorSpace: colorSpace
    };
    layout = await frame.copyTo(copy_to_buf, options);
  } catch (e) {
    TEST.reportFailure(`copyTo() failure: ${e}`);
    return;
  }
  if (layout.length != 1) {
    TEST.skip('Conversion to RGB is not supported by the browser');
    return;
  }

  // Read VideoFrame pixels via drawImage()
  ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height);
  imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace});
  let get_image_buf = imageData.data.buffer;

  // Compare!
  const tolerance = 1;
  for (let i = 0; i < copy_to_buf.byteLength; i += 4) {
    compareColors(
        new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4),
        tolerance, `Mismatch at offset ${i}`);
    if (TEST.finished) {
      break;
    }
  }
}

async function check_predefined_frames() {
  // Test frames constructed from an array buffer.
  // This should be a part of the WPT tests some day.
  for (let frame of makeI420_frames()) {
    await test_frame(frame, 'srgb');
    await test_frame(frame, 'display-p3');
    frame.close();
  }
}

async function main(arg) {
  let source_type = arg.source_type;
  TEST.log('Starting test with arguments: ' + JSON.stringify(arg));
  let source = await createFrameSource(source_type, FRAME_WIDTH, FRAME_HEIGHT);
  if (!source) {
    TEST.skip('Unsupported source: ' + source_type);
    return;
  }

  let frame = await source.getNextFrame();

  await test_frame(frame, 'srgb');
  await test_frame(frame, 'display-p3');
  frame.close();

  await check_predefined_frames();

  source.close();
  TEST.log('Test completed');
}