Skip to content

Commit c00787c

Browse files
ChunMinChangmoz-wptsync-bot
authored andcommitted
Add WPT for VideoFrame.copyTo opaque/alpha format conversion
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 bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=2031524 gecko-commit: 7a165e40313faca15841dc77ab541eeb73b3db24 gecko-commit-git: 45768e45de1fec411e05dd556303d67fe44e9da5 gecko-reviewers: media-playback-reviewers, padenot
1 parent 6745634 commit c00787c

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// META: global=window,dedicatedworker
2+
// META: script=/webcodecs/video-encoder-utils.js
3+
4+
// Verify that VideoFrame.copyTo populates every row when converting between
5+
// opaque and alpha surface formats (BGRX<->BGRA, RGBX<->RGBA), including
6+
// when source and destination strides differ.
7+
8+
const SENTINEL = 0xDE;
9+
10+
function fillSolidFrame(format, width, height, pixel) {
11+
const bpp = 4;
12+
const data = new Uint8Array(width * height * bpp);
13+
for (let i = 0; i < data.length; i += bpp) {
14+
data[i] = pixel[0];
15+
data[i + 1] = pixel[1];
16+
data[i + 2] = pixel[2];
17+
data[i + 3] = pixel[3];
18+
}
19+
return new VideoFrame(data, {
20+
format,
21+
codedWidth: width,
22+
codedHeight: height,
23+
timestamp: 0,
24+
});
25+
}
26+
27+
async function testOpaqueCopyTo(srcFormat, dstFormat, width, height) {
28+
const pixel = [0x41, 0x42, 0x43, 0xFF];
29+
const frame = fillSolidFrame(srcFormat, width, height, pixel);
30+
const buf = new Uint8Array(frame.allocationSize({format: dstFormat}));
31+
buf.fill(SENTINEL);
32+
await frame.copyTo(buf, {format: dstFormat});
33+
frame.close();
34+
35+
for (let row = 0; row < height; row++) {
36+
for (let col = 0; col < width; col++) {
37+
const off = (row * width + col) * 4;
38+
assert_equals(buf[off], pixel[0], `row ${row} col ${col} byte 0`);
39+
assert_equals(buf[off + 1], pixel[1], `row ${row} col ${col} byte 1`);
40+
assert_equals(buf[off + 2], pixel[2], `row ${row} col ${col} byte 2`);
41+
assert_equals(buf[off + 3], 0xFF, `row ${row} col ${col} alpha`);
42+
}
43+
}
44+
}
45+
46+
promise_test(async t => {
47+
await testOpaqueCopyTo('BGRX', 'BGRA', 8, 4);
48+
}, 'BGRX to BGRA copyTo writes all rows (8x4)');
49+
50+
promise_test(async t => {
51+
await testOpaqueCopyTo('RGBX', 'RGBA', 8, 4);
52+
}, 'RGBX to RGBA copyTo writes all rows (8x4)');
53+
54+
promise_test(async t => {
55+
await testOpaqueCopyTo('BGRX', 'BGRA', 100, 100);
56+
}, 'BGRX to BGRA copyTo writes all rows (100x100)');
57+
58+
promise_test(async t => {
59+
await testOpaqueCopyTo('RGBX', 'RGBA', 100, 100);
60+
}, 'RGBX to RGBA copyTo writes all rows (100x100)');
61+
62+
promise_test(async t => {
63+
await testOpaqueCopyTo('BGRX', 'BGRA', 90, 50);
64+
}, 'BGRX to BGRA copyTo writes all rows (90x50, non-aligned width)');
65+
66+
promise_test(async t => {
67+
await testOpaqueCopyTo('BGRX', 'BGRA', 7, 3);
68+
}, 'BGRX to BGRA copyTo writes all rows (7x3, odd dimensions)');
69+
70+
promise_test(async t => {
71+
await testOpaqueCopyTo('BGRA', 'BGRX', 100, 100);
72+
}, 'BGRA to BGRX copyTo writes all rows (100x100)');
73+
74+
promise_test(async t => {
75+
await testOpaqueCopyTo('RGBA', 'RGBX', 100, 100);
76+
}, 'RGBA to RGBX copyTo writes all rows (100x100)');
77+
78+
promise_test(async t => {
79+
const width = 100;
80+
const height = 100;
81+
const frame = fillSolidFrame('BGRX', width, height, [0x41, 0x42, 0x43, 0xFF]);
82+
const buf = new Uint8Array(frame.allocationSize({format: 'BGRA'}));
83+
buf.fill(SENTINEL);
84+
await frame.copyTo(buf, {format: 'BGRA'});
85+
frame.close();
86+
let sentinelCount = 0;
87+
for (let i = 0; i < buf.length; i++) {
88+
if (buf[i] === SENTINEL) sentinelCount++;
89+
}
90+
assert_equals(sentinelCount, 0, 'No sentinel bytes should remain after copyTo');
91+
}, 'BGRX to BGRA copyTo overwrites all bytes (sentinel test)');
92+
93+
promise_test(async t => {
94+
const width = 100;
95+
const height = 100;
96+
const pixel = [0x41, 0x42, 0x43, 0xFF];
97+
const encoderConfig = {codec: 'vp8', width, height, bitrate: 5_000_000};
98+
99+
await checkEncoderSupport(t, encoderConfig);
100+
101+
const inputFrame = fillSolidFrame('RGBA', width, height, pixel);
102+
let decodedFrame = null;
103+
const decoder = new VideoDecoder({
104+
output(frame) { decodedFrame = frame; },
105+
error(e) { t.unreached_func('Decoder error: ' + e)(); },
106+
});
107+
let encodedChunk = null;
108+
let decoderConfig = null;
109+
const encoder = new VideoEncoder({
110+
output(chunk, meta) {
111+
encodedChunk = chunk;
112+
if (meta.decoderConfig) decoderConfig = meta.decoderConfig;
113+
},
114+
error(e) { t.unreached_func('Encoder error: ' + e)(); },
115+
});
116+
117+
encoder.configure(encoderConfig);
118+
encoder.encode(inputFrame);
119+
inputFrame.close();
120+
await encoder.flush();
121+
assert_not_equals(encodedChunk, null, 'Got encoded chunk');
122+
assert_not_equals(decoderConfig, null, 'Got decoder config');
123+
124+
decoder.configure(decoderConfig);
125+
decoder.decode(encodedChunk);
126+
await decoder.flush();
127+
assert_not_equals(decodedFrame, null, 'Got decoded frame');
128+
129+
const nativeFormat = decodedFrame.format;
130+
if (nativeFormat === 'BGRX' || nativeFormat === 'RGBX') {
131+
const dstFormat = nativeFormat === 'BGRX' ? 'BGRA' : 'RGBA';
132+
const allocSize = decodedFrame.allocationSize({format: dstFormat});
133+
const buf = new Uint8Array(allocSize);
134+
await decodedFrame.copyTo(buf, {format: dstFormat});
135+
136+
// The input is a solid color, so after encode/decode + format conversion
137+
// every output row should match row 0 within VP8 lossy tolerance.
138+
const stride = width * 4;
139+
const tolerance = 20;
140+
let badPixels = 0;
141+
for (let row = 1; row < height; row++) {
142+
for (let col = 0; col < width; col++) {
143+
const ref = col * 4;
144+
const off = row * stride + col * 4;
145+
if (Math.abs(buf[off] - buf[ref]) > tolerance ||
146+
Math.abs(buf[off + 1] - buf[ref + 1]) > tolerance ||
147+
Math.abs(buf[off + 2] - buf[ref + 2]) > tolerance ||
148+
Math.abs(buf[off + 3] - buf[ref + 3]) > tolerance) {
149+
badPixels++;
150+
}
151+
}
152+
}
153+
assert_equals(badPixels, 0,
154+
'All rows must match row 0 within tolerance (solid color input)');
155+
}
156+
157+
decodedFrame.close();
158+
encoder.close();
159+
decoder.close();
160+
}, 'Decoded frame opaque-to-alpha copyTo writes all rows');

0 commit comments

Comments
 (0)