// 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 isRec709(colorSpace) {
return colorSpace.primaries === 'bt709' && colorSpace.transfer === 'bt709' &&
colorSpace.matrix === 'bt709' && colorSpace.fullRange === false;
}
function isSRGB(colorSpace) {
return colorSpace.primaries === 'bt709' &&
colorSpace.transfer === 'iec61966-2-1' && colorSpace.matrix === 'rgb' &&
colorSpace.fullRange === true;
}
function isRec601(colorSpace) {
return colorSpace.primaries === 'smpte170m' &&
(colorSpace.transfer === 'smpte170m' ||
colorSpace.transfer === 'bt709') &&
colorSpace.matrix === 'smpte170m' && colorSpace.fullRange === false;
}
function makePixelArray(byteLength) {
let data = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
data[i] = i;
}
return data;
}
function makeFrame(type, timestamp) {
let init = {
format: 'RGBA',
timestamp: timestamp,
codedWidth: FRAME_WIDTH,
codedHeight: FRAME_HEIGHT
};
switch (type) {
case 'I420': {
const yuvByteLength = 1.5 * FRAME_WIDTH * FRAME_HEIGHT;
let data = makePixelArray(yuvByteLength);
return new VideoFrame(data, {...init, format: 'I420'});
}
case 'RGBA': {
const rgbaByteLength = 4 * FRAME_WIDTH * FRAME_HEIGHT;
let data = makePixelArray(rgbaByteLength);
return new VideoFrame(data, {...init, format: 'RGBA'});
}
}
}
async function main(arg) {
const encoderConfig = {
codec: arg.codec,
hardwareAcceleration: arg.acceleration,
width: FRAME_WIDTH,
height: FRAME_HEIGHT,
};
TEST.log('Starting test with arguments: ' + JSON.stringify(arg));
let supported = false;
try {
supported = (await VideoEncoder.isConfigSupported(encoderConfig)).supported;
} catch (e) {
}
if (!supported) {
TEST.skip('Unsupported codec: ' + arg.codec);
return;
}
const frameDuration = 16666;
let inputFrames = [
// Use I420/BT.709 first since default macOS colorspace is sRGB.
makeFrame('I420', 0 * frameDuration),
makeFrame('I420', 1 * frameDuration),
makeFrame('RGBA', 2 * frameDuration),
makeFrame('RGBA', 3 * frameDuration),
];
let outputChunks = [];
let outputMetadata = [];
let errors = 0;
const init = {
output(chunk, metadata) {
outputChunks.push(chunk);
outputMetadata.push(metadata);
},
error(e) {
errors++;
TEST.log(e);
}
};
let encoder = new VideoEncoder(init);
encoder.configure(encoderConfig);
for (let frame of inputFrames) {
encoder.encode(frame);
await waitForNextFrame();
}
await encoder.flush();
encoder.close();
TEST.assert_eq(errors, 0, 'Encoding errors occurred during the test');
TEST.assert_eq(outputChunks.length, 4, 'Unexpected number of outputs');
TEST.assert_eq(
outputMetadata.length, 4, 'Unexpected number of output metadata');
// I420 passthrough should preserve default rec709 color space.
TEST.assert_eq(inputFrames[0].format, 'I420', 'inputs[0] is I420');
TEST.assert(isRec709(inputFrames[0].colorSpace), 'inputs[0] is rec709');
TEST.assert_eq(outputChunks[0].type, 'key', 'outputs[0] is key');
TEST.assert(
'decoderConfig' in outputMetadata[0], 'metadata[0] has decoderConfig');
TEST.assert(
isRec709(outputMetadata[0].decoderConfig.colorSpace),
'metadata[0] is rec709');
// Next output may or may not be a key frame w/ metadata (up to
// encoder). Corresponding input is still I420 rec709, so if metadata is
// given, we expect same colorSpace as for the previous frame.
TEST.assert_eq(inputFrames[1].format, 'I420', 'inputs[1] is I420');
TEST.assert(isRec709(inputFrames[1].colorSpace, 'inputs[1] is rec709'));
if ('decoderConfig' in outputMetadata[1]) {
TEST.assert(
isRec709(outputMetadata[1].decoderConfig.colorSpace),
'metadata[1] is rec709');
}
// Next output should be a key frame and have accompanying metadata
// because the corresponding input format changed to RGBA, which means
// we libyuv will convert to I420 w/ rec601 during encoding.
TEST.assert_eq(inputFrames[2].format, 'RGBA', 'inputs[2] is RGBA');
TEST.assert(isSRGB(inputFrames[2].colorSpace), 'inputs[2] is sRGB');
TEST.assert(outputChunks[2].type == 'key', 'outputs[2] is key');
TEST.assert(
'decoderConfig' in outputMetadata[2], 'metadata[2] has decoderConfig');
TEST.assert(
isRec601(outputMetadata[2].decoderConfig.colorSpace),
'metadata[2] is rec601');
// Next output may or may not be a key frame w/ metadata (up to
// encoder). Corresponding input is still RGBA sRGB, so if metadata is
// given, we expect same colorSpace as for the previous frame.
TEST.assert_eq(inputFrames[3].format, 'RGBA', 'inputs[3] is RGBA');
TEST.assert(isSRGB(inputFrames[3].colorSpace), 'inputs[3] is sRGB');
if ('decoderConfig' in outputMetadata[3]) {
TEST.assert(
isRec601(outputMetadata[3].decoderConfig.colorSpace),
'metadata[3] is rec601');
}
for (let frame of inputFrames) {
frame.close();
}
// Now decode the frames and ensure the encoder embedded the right color
// space information in the bitstream.
// VP8 doesn't have embedded color space information in the bitstream.
if (arg.codec == 'vp8') {
TEST.reportSuccess();
return;
}
let decodedFrames = [];
const decoderInit = {
output(frame) {
decodedFrames.push(frame);
},
error(e) {
errors++;
TEST.log(e);
}
};
let decoder = new VideoDecoder(decoderInit);
for (var i = 0; i < outputChunks.length; ++i) {
if ('decoderConfig' in outputMetadata[i]) {
let config = {...outputMetadata[i].decoderConfig};
// Removes the color space provided by the encoder so that color space
// information in the underlying bitstream is exposed during decode.
config.colorSpace = {};
config.hardwareAcceleration = arg.acceleration;
let support = await VideoDecoder.isConfigSupported(config);
if (!support.supported)
config.hardwareAcceleration = 'no-preference';
decoder.configure(config);
}
decoder.decode(outputChunks[i]);
await waitForNextFrame();
}
await decoder.flush();
decoder.close();
TEST.assert_eq(
errors, 0, 'Encoding errors occurred during the decoding test');
TEST.assert_eq(
decodedFrames.length, outputChunks.length,
'Unexpected number of decoded outputs');
let colorSpace = {};
for (var i = 0; i < decodedFrames.length; ++i) {
if ('decoderConfig' in outputMetadata[i]) {
colorSpace = outputMetadata[i].decoderConfig.colorSpace;
}
// It's acceptable to have no bitstream color space information.
if (decodedFrames[i].colorSpace.primaries != null) {
TEST.assert_eq(
decodedFrames[i].colorSpace.primaries, colorSpace.primaries,
`Frame ${i} color primaries mismatch`);
}
if (decodedFrames[i].colorSpace.matrix != null) {
if (decodedFrames[i].colorSpace.matrix != colorSpace.matrix) {
// Allow functionally equivalent matches.
TEST.assert(
colorSpace.matrix == 'smpte170m' &&
decodedFrames[i].colorSpace.matrix == 'bt470bg',
`Frame ${i} color matrix mismatch`);
} else {
TEST.assert_eq(
decodedFrames[i].colorSpace.matrix, colorSpace.matrix,
`Frame ${i} color matrix mismatch`);
}
}
if (decodedFrames[i].colorSpace.transfer != null) {
if (decodedFrames[i].colorSpace.transfer != colorSpace.transfer) {
// Allow functionally equivalent matches.
TEST.assert(
(colorSpace.transfer == 'smpte170m' &&
decodedFrames[i].colorSpace.transfer == 'bt709') ||
(colorSpace.transfer == 'bt709' &&
decodedFrames[i].colorSpace.transfer == 'smpte170m'),
`Frame ${i} color transfer mismatch`)
} else {
TEST.assert_eq(
decodedFrames[i].colorSpace.transfer, colorSpace.transfer,
`Frame ${i} color transfer mismatch`);
}
}
if (decodedFrames[i].colorSpace.fullRange != null) {
TEST.assert_eq(
decodedFrames[i].colorSpace.fullRange, colorSpace.fullRange,
`Frame ${i} color fullRange mismatch`);
}
decodedFrames[i].close();
}
TEST.reportSuccess();
}