WebCodecs video encoding is unavailable on Android (bug 1840508), so the encode/decode round-trip check gates on VideoEncoder.isConfigSupported and is expected to report PRECONDITION_FAILED there. Differential Revision: https://phabricator.services.mozilla.com/D294066
161 lines
5.4 KiB
JavaScript
161 lines
5.4 KiB
JavaScript
// META: global=window,dedicatedworker
|
|
// META: script=/webcodecs/video-encoder-utils.js
|
|
|
|
// Verify that VideoFrame.copyTo populates every row when converting between
|
|
// opaque and alpha surface formats (BGRX<->BGRA, RGBX<->RGBA), including
|
|
// when source and destination strides differ.
|
|
|
|
const SENTINEL = 0xDE;
|
|
|
|
function fillSolidFrame(format, width, height, pixel) {
|
|
const bpp = 4;
|
|
const data = new Uint8Array(width * height * bpp);
|
|
for (let i = 0; i < data.length; i += bpp) {
|
|
data[i] = pixel[0];
|
|
data[i + 1] = pixel[1];
|
|
data[i + 2] = pixel[2];
|
|
data[i + 3] = pixel[3];
|
|
}
|
|
return new VideoFrame(data, {
|
|
format,
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: 0,
|
|
});
|
|
}
|
|
|
|
async function testOpaqueCopyTo(srcFormat, dstFormat, width, height) {
|
|
const pixel = [0x41, 0x42, 0x43, 0xFF];
|
|
const frame = fillSolidFrame(srcFormat, width, height, pixel);
|
|
const buf = new Uint8Array(frame.allocationSize({format: dstFormat}));
|
|
buf.fill(SENTINEL);
|
|
await frame.copyTo(buf, {format: dstFormat});
|
|
frame.close();
|
|
|
|
for (let row = 0; row < height; row++) {
|
|
for (let col = 0; col < width; col++) {
|
|
const off = (row * width + col) * 4;
|
|
assert_equals(buf[off], pixel[0], `row ${row} col ${col} byte 0`);
|
|
assert_equals(buf[off + 1], pixel[1], `row ${row} col ${col} byte 1`);
|
|
assert_equals(buf[off + 2], pixel[2], `row ${row} col ${col} byte 2`);
|
|
assert_equals(buf[off + 3], 0xFF, `row ${row} col ${col} alpha`);
|
|
}
|
|
}
|
|
}
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('BGRX', 'BGRA', 8, 4);
|
|
}, 'BGRX to BGRA copyTo writes all rows (8x4)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('RGBX', 'RGBA', 8, 4);
|
|
}, 'RGBX to RGBA copyTo writes all rows (8x4)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('BGRX', 'BGRA', 100, 100);
|
|
}, 'BGRX to BGRA copyTo writes all rows (100x100)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('RGBX', 'RGBA', 100, 100);
|
|
}, 'RGBX to RGBA copyTo writes all rows (100x100)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('BGRX', 'BGRA', 90, 50);
|
|
}, 'BGRX to BGRA copyTo writes all rows (90x50, non-aligned width)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('BGRX', 'BGRA', 7, 3);
|
|
}, 'BGRX to BGRA copyTo writes all rows (7x3, odd dimensions)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('BGRA', 'BGRX', 100, 100);
|
|
}, 'BGRA to BGRX copyTo writes all rows (100x100)');
|
|
|
|
promise_test(async t => {
|
|
await testOpaqueCopyTo('RGBA', 'RGBX', 100, 100);
|
|
}, 'RGBA to RGBX copyTo writes all rows (100x100)');
|
|
|
|
promise_test(async t => {
|
|
const width = 100;
|
|
const height = 100;
|
|
const frame = fillSolidFrame('BGRX', width, height, [0x41, 0x42, 0x43, 0xFF]);
|
|
const buf = new Uint8Array(frame.allocationSize({format: 'BGRA'}));
|
|
buf.fill(SENTINEL);
|
|
await frame.copyTo(buf, {format: 'BGRA'});
|
|
frame.close();
|
|
let sentinelCount = 0;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
if (buf[i] === SENTINEL) sentinelCount++;
|
|
}
|
|
assert_equals(sentinelCount, 0, 'No sentinel bytes should remain after copyTo');
|
|
}, 'BGRX to BGRA copyTo overwrites all bytes (sentinel test)');
|
|
|
|
promise_test(async t => {
|
|
const width = 100;
|
|
const height = 100;
|
|
const pixel = [0x41, 0x42, 0x43, 0xFF];
|
|
const encoderConfig = {codec: 'vp8', width, height, bitrate: 5_000_000};
|
|
|
|
await checkEncoderSupport(t, encoderConfig);
|
|
|
|
const inputFrame = fillSolidFrame('RGBA', width, height, pixel);
|
|
let decodedFrame = null;
|
|
const decoder = new VideoDecoder({
|
|
output(frame) { decodedFrame = frame; },
|
|
error(e) { t.unreached_func('Decoder error: ' + e)(); },
|
|
});
|
|
let encodedChunk = null;
|
|
let decoderConfig = null;
|
|
const encoder = new VideoEncoder({
|
|
output(chunk, meta) {
|
|
encodedChunk = chunk;
|
|
if (meta.decoderConfig) decoderConfig = meta.decoderConfig;
|
|
},
|
|
error(e) { t.unreached_func('Encoder error: ' + e)(); },
|
|
});
|
|
|
|
encoder.configure(encoderConfig);
|
|
encoder.encode(inputFrame);
|
|
inputFrame.close();
|
|
await encoder.flush();
|
|
assert_not_equals(encodedChunk, null, 'Got encoded chunk');
|
|
assert_not_equals(decoderConfig, null, 'Got decoder config');
|
|
|
|
decoder.configure(decoderConfig);
|
|
decoder.decode(encodedChunk);
|
|
await decoder.flush();
|
|
assert_not_equals(decodedFrame, null, 'Got decoded frame');
|
|
|
|
const nativeFormat = decodedFrame.format;
|
|
if (nativeFormat === 'BGRX' || nativeFormat === 'RGBX') {
|
|
const dstFormat = nativeFormat === 'BGRX' ? 'BGRA' : 'RGBA';
|
|
const allocSize = decodedFrame.allocationSize({format: dstFormat});
|
|
const buf = new Uint8Array(allocSize);
|
|
await decodedFrame.copyTo(buf, {format: dstFormat});
|
|
|
|
// The input is a solid color, so after encode/decode + format conversion
|
|
// every output row should match row 0 within VP8 lossy tolerance.
|
|
const stride = width * 4;
|
|
const tolerance = 20;
|
|
let badPixels = 0;
|
|
for (let row = 1; row < height; row++) {
|
|
for (let col = 0; col < width; col++) {
|
|
const ref = col * 4;
|
|
const off = row * stride + col * 4;
|
|
if (Math.abs(buf[off] - buf[ref]) > tolerance ||
|
|
Math.abs(buf[off + 1] - buf[ref + 1]) > tolerance ||
|
|
Math.abs(buf[off + 2] - buf[ref + 2]) > tolerance ||
|
|
Math.abs(buf[off + 3] - buf[ref + 3]) > tolerance) {
|
|
badPixels++;
|
|
}
|
|
}
|
|
}
|
|
assert_equals(badPixels, 0,
|
|
'All rows must match row 0 within tolerance (solid color input)');
|
|
}
|
|
|
|
decodedFrame.close();
|
|
encoder.close();
|
|
decoder.close();
|
|
}, 'Decoded frame opaque-to-alpha copyTo writes all rows');
|