1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
content / test / data / gpu / webcodecs / encoding-rate-control.js [blame]
// 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');
}