// 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');