225 lines
7.1 KiB
JavaScript
225 lines
7.1 KiB
JavaScript
// META: timeout=long
|
|
// META: global=window,dedicatedworker
|
|
// META: script=/webcodecs/video-encoder-utils.js
|
|
// META: variant=?av1
|
|
// META: variant=?av1_444_high
|
|
// META: variant=?vp8
|
|
// META: variant=?vp9_p0
|
|
// META: variant=?vp9_p2
|
|
// META: variant=?vp9_444_p1
|
|
// META: variant=?vp9_444_p3
|
|
// META: variant=?h264_avc
|
|
// META: variant=?h264_annexb
|
|
// META: variant=?h265_hevc
|
|
// META: variant=?h265_annexb
|
|
|
|
var ENCODER_CONFIG = null;
|
|
promise_setup(async () => {
|
|
const config = {
|
|
'?av1': {
|
|
codec: 'av01.0.04M.08',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
},
|
|
'?av1_444_high': {
|
|
codec: 'av01.1.04M.08.0.000',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
outputPixelFormat: 'I444',
|
|
},
|
|
'?vp8': {
|
|
codec: 'vp8',
|
|
hasEmbeddedColorSpace: false,
|
|
hardwareAcceleration: 'prefer-software',
|
|
},
|
|
'?vp9_p0': {
|
|
codec: 'vp09.00.10.08',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
},
|
|
'?vp9_p2': {
|
|
codec: 'vp09.02.10.10',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
// TODO(https://github.com/w3c/webcodecs/issues/384):
|
|
// outputPixelFormat should be 'I420P10'
|
|
},
|
|
'?vp9_444_p1': {
|
|
codec: 'vp09.01.10.08.03',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
outputPixelFormat: 'I444',
|
|
},
|
|
'?vp9_444_p3': {
|
|
codec: 'vp09.03.10.10.03',
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
// TODO(https://github.com/w3c/webcodecs/issues/384):
|
|
// outputPixelFormat should be 'I444P10'
|
|
},
|
|
'?h264_avc': {
|
|
codec: 'avc1.42001E',
|
|
avc: {format: 'avc'},
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
},
|
|
'?h264_annexb': {
|
|
codec: 'avc1.42001E',
|
|
avc: {format: 'annexb'},
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-software',
|
|
},
|
|
'?h265_hevc': {
|
|
codec: 'hvc1.1.6.L123.00',
|
|
hevc: {format: 'hevc'},
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-hardware',
|
|
},
|
|
'?h265_annexb': {
|
|
codec: 'hvc1.1.6.L123.00',
|
|
hevc: {format: 'annexb'},
|
|
hasEmbeddedColorSpace: true,
|
|
hardwareAcceleration: 'prefer-hardware',
|
|
}
|
|
}[location.search];
|
|
config.width = 320;
|
|
config.height = 200;
|
|
config.bitrate = 1000000;
|
|
config.bitrateMode = "constant";
|
|
config.framerate = 30;
|
|
ENCODER_CONFIG = config;
|
|
});
|
|
|
|
async function runFullCycleTest(t, options) {
|
|
let encoder_config = { ...ENCODER_CONFIG };
|
|
if (options.realTimeLatencyMode) {
|
|
encoder_config.latencyMode = 'realtime';
|
|
}
|
|
let encoder_color_space = {};
|
|
const w = encoder_config.width;
|
|
const h = encoder_config.height;
|
|
let next_ts = 0
|
|
let frames_to_encode = 16;
|
|
let frames_encoded = 0;
|
|
let frames_decoded = 0;
|
|
|
|
await checkEncoderSupport(t, encoder_config);
|
|
let decoder = new VideoDecoder({
|
|
output(frame) {
|
|
t.add_cleanup(() => { frame.close() });
|
|
|
|
assert_equals(frame.visibleRect.width, w, "visibleRect.width");
|
|
assert_equals(frame.visibleRect.height, h, "visibleRect.height");
|
|
if (!options.realTimeLatencyMode) {
|
|
assert_equals(frame.timestamp, next_ts++, "decode timestamp");
|
|
}
|
|
|
|
if (ENCODER_CONFIG.outputPixelFormat) {
|
|
assert_equals(
|
|
frame.format, ENCODER_CONFIG.outputPixelFormat,
|
|
"decoded pixel format");
|
|
}
|
|
|
|
// The encoder is allowed to change the color space to satisfy the
|
|
// encoder when readback is needed to send the frame for encoding, but
|
|
// the decoder shouldn't change it after the fact.
|
|
assert_equals(
|
|
frame.colorSpace.primaries, encoder_color_space.primaries,
|
|
'colorSpace.primaries');
|
|
assert_equals(
|
|
frame.colorSpace.transfer, encoder_color_space.transfer,
|
|
'colorSpace.transfer');
|
|
assert_equals(
|
|
frame.colorSpace.matrix, encoder_color_space.matrix,
|
|
'colorSpace.matrix');
|
|
assert_equals(
|
|
frame.colorSpace.fullRange, encoder_color_space.fullRange,
|
|
'colorSpace.fullRange');
|
|
|
|
frames_decoded++;
|
|
assert_true(validateBlackDots(frame, frame.timestamp),
|
|
"frame doesn't match. ts: " + frame.timestamp);
|
|
},
|
|
error(e) {
|
|
assert_unreached(e.message);
|
|
}
|
|
});
|
|
|
|
let next_encode_ts = 0;
|
|
const encoder_init = {
|
|
output(chunk, metadata) {
|
|
let config = metadata.decoderConfig;
|
|
// Issue a configure if there's a new config, or on the
|
|
// first chunk if testing rate control
|
|
if (!options.rateControl && config ||
|
|
options.rateControl && chunk.timestamp == 0) {
|
|
config.hardwareAcceleration = encoder_config.hardwareAcceleration;
|
|
encoder_color_space = config.colorSpace;
|
|
|
|
// Removes the color space provided by the encoder so that color space
|
|
// information in the underlying bitstream is exposed during decode.
|
|
if (options.stripDecoderConfigColorSpace)
|
|
config.colorSpace = {};
|
|
|
|
decoder.configure(config);
|
|
}
|
|
decoder.decode(chunk);
|
|
frames_encoded++;
|
|
if (!options.realTimeLatencyMode) {
|
|
assert_equals(chunk.timestamp, next_encode_ts++, "encode timestamp");
|
|
}
|
|
},
|
|
error(e) {
|
|
assert_unreached(e.message);
|
|
}
|
|
};
|
|
|
|
let encoder = new VideoEncoder(encoder_init);
|
|
encoder.configure(encoder_config);
|
|
|
|
for (let i = 0; i < frames_to_encode; i++) {
|
|
let frame = createDottedFrame(w, h, i);
|
|
|
|
// Frames should have a valid color space when created from canvas.
|
|
assert_not_equals(frame.colorSpace.primaries, null, 'colorSpace.primaries');
|
|
assert_not_equals(frame.colorSpace.transfer, null, 'colorSpace.transfer');
|
|
assert_not_equals(frame.colorSpace.matrix, null, 'colorSpace.matrix');
|
|
assert_not_equals(frame.colorSpace.fullRange, null, 'colorSpace.fullRange');
|
|
|
|
let keyframe = (i % 5 == 0);
|
|
encoder.encode(frame, { keyFrame: keyframe });
|
|
if (i % 3 == 0 && options.rateControl) {
|
|
// reconfigure with a different rate
|
|
encoder_config.bitrate = encoder_config.bitrate * 0.9;
|
|
encoder.configure(encoder_config);
|
|
}
|
|
frame.close();
|
|
}
|
|
await encoder.flush();
|
|
await decoder.flush();
|
|
encoder.close();
|
|
decoder.close();
|
|
if (options.realTimeLatencyMode) {
|
|
assert_greater_than(frames_encoded, 0, "frames_encoded");
|
|
} else {
|
|
assert_equals(frames_encoded, frames_to_encode, "frames_encoded");
|
|
}
|
|
assert_equals(frames_decoded, frames_encoded, "frames_decoded");
|
|
}
|
|
|
|
promise_test(async t => {
|
|
return runFullCycleTest(t, {});
|
|
}, 'Encoding and decoding cycle');
|
|
|
|
promise_test(async t => {
|
|
return runFullCycleTest(t, {realTimeLatencyMode: true});
|
|
}, 'Encoding and decoding cycle with realtime latency mode');
|
|
|
|
promise_test(async t => {
|
|
if (ENCODER_CONFIG.hasEmbeddedColorSpace)
|
|
return runFullCycleTest(t, {stripDecoderConfigColorSpace: true});
|
|
}, 'Encoding and decoding cycle w/ stripped color space');
|
|
|
|
promise_test(async t => {
|
|
return runFullCycleTest(t, {rateControl: true});
|
|
}, 'Encoding and decoding cycle w/ rate control');
|