|
| 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