// 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');
}