chromium/content/test/data/gpu/webcodecs/encoding-rate-control.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 createRandomGenerator(seed) {
  return function(max) {
    seed = ((1 + seed) * 7 + 13) & 0x7fffffff;
    return seed % max;
  }
}

// Draw pseudorandom animation on the canvas
function createDrawingFunction(cnv, changes_per_frame) {
  var ctx = cnv.getContext('2d');
  let width = cnv.width;
  let height = cnv.height;
  let rnd = createRandomGenerator(425533);
  const w = 35;
  const h = 30;
  const white_noise_img = ctx.createImageData(w, h);
  self.crypto.getRandomValues(white_noise_img.data);

  return function() {
    let x = 0, y = 0;
    // Paint gradient filled ellipses
    for (let i = 0; i < changes_per_frame; i++) {
      x = rnd(width);
      y = rnd(height);

      let a =
          'rgba(' + rnd(255) + ',' + rnd(255) + ',' + rnd(255) + ',' + 1 + ')';
      let b =
          'rgba(' + rnd(255) + ',' + rnd(255) + ',' + rnd(255) + ',' + 1 + ')';
      let c =
          'rgba(' + rnd(255) + ',' + rnd(255) + ',' + rnd(255) + ',' + 1 + ')';
      let gradient = ctx.createLinearGradient(x, y, x + w, y + h);
      gradient.addColorStop(0, a);
      gradient.addColorStop(0.5, b);
      gradient.addColorStop(1, c);

      ctx.fillStyle = gradient;
      ctx.beginPath();
      ctx.ellipse(x, y, w / 2, h / 2, 0.0, 0.0, 2 * Math.PI);
      ctx.fill();
    }

    // Add a bit of white noise
    ctx.putImageData(white_noise_img, x, y);
  }
}

async function main(arg) {
  const width = 1280;
  const height = 720;
  const seconds = 5;
  const fps = 30;
  const frames_to_encode = fps * seconds;
  let errors = 0;
  let chunks = [];

  const encoder_config = {
    codec: arg.codec,
    hardwareAcceleration: arg.acceleration,
    width: width,
    height: height,
    bitrate: arg.bitrate,
    bitrateMode: arg.bitrate_mode,
    framerate: fps
  };

  let supported = false;
  try {
    supported =
        (await VideoEncoder.isConfigSupported(encoder_config)).supported;
  } catch (e) {
  }
  if (!supported) {
    TEST.skip('Unsupported codec: ' + arg.codec);
    return;
  }

  // Start drawing canvas animation that will be source of the frames
  // for the test.
  let cnv = document.getElementById('src');
  cnv.width = width;
  cnv.height = height;
  const changes_per_frame = 25;
  const draw = createDrawingFunction(cnv, changes_per_frame);

  const init = {
    output(chunk, metadata) {
      chunks.push(chunk);
    },
    error(e) {
      errors++;
      TEST.log(e);
    }
  };

  let encoder = new VideoEncoder(init);
  encoder.configure(encoder_config);

  // Take frames from the canvas and encoder them with a given bitrate
  for (let i = 0; i < frames_to_encode; i++) {
    const time_us = i / fps * 1_000_000;
    draw();
    let frame = new VideoFrame(cnv, {timestamp: time_us});
    encoder.encode(frame, {keyFrame: false});
    frame.close();
    await waitForNextFrame();
  }

  await encoder.flush();
  TEST.log('Encoding completed');
  encoder.close();

  TEST.assert(errors == 0, 'Encoding errors occurred during the test');
  TEST.assert(
      chunks.length == frames_to_encode,
      'Output count mismatch: ' + chunks.length);

  // Calculate total size of the encoded video
  let total_encoded_size = 0;
  for (let chunk of chunks) {
    total_encoded_size += chunk.byteLength;
  }

  const actual_bitrate = total_encoded_size / seconds * 8 /* bits */;
  const bitrate_mismatch = Math.abs(actual_bitrate - arg.bitrate) / arg.bitrate;

  // Check how far off expected encoded size from the ideal, CBR is
  // supposed to be more strict that VBR.
  let tolerance = (arg.bitrate_mode == 'constant') ? 0.15 : 0.30;
  TEST.assert(
      bitrate_mismatch < tolerance,
      `Bitrate is too far off. Expected ${arg.bitrate}` +
          ` Actual bitrate: ${actual_bitrate}` +
          ` Tolerance: ${tolerance * arg.bitrate}` +
          ` Mismatch: ${bitrate_mismatch}`);

  TEST.log('Test completed');
}