diff options
Diffstat (limited to 'testing/web-platform/tests/webcodecs')
5 files changed, 417 insertions, 8 deletions
diff --git a/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js b/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js index 559ff3203b..a70bf28f80 100644 --- a/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js +++ b/testing/web-platform/tests/webcodecs/audio-encoder-config.https.any.js @@ -272,6 +272,8 @@ const validConfigs = [ numberOfChannels: 2, opus: { complexity: 5, + signal: 'music', + application: 'audio', frameDuration: 20000, packetlossperc: 10, useinbandfec: true, @@ -283,6 +285,8 @@ const validConfigs = [ numberOfChannels: 2, opus: { format: 'opus', + signal: 'voice', + application: 'lowdelay', complexity: 10, frameDuration: 60000, packetlossperc: 20, // Irrelevant without useinbandfec, but still valid. diff --git a/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js b/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js index 8f1724bc85..c1ebafc4a3 100644 --- a/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js +++ b/testing/web-platform/tests/webcodecs/video-encoder-flush.https.any.js @@ -2,16 +2,36 @@ // META: script=/common/media.js // META: script=/webcodecs/utils.js // META: script=/webcodecs/video-encoder-utils.js +// META: variant=?vp8 +// META: variant=?h264_avc + +const VP8_CONFIG = { + codec: 'vp8', + width: 640, + height: 480, + displayWidth: 800, + displayHeight: 600, +}; + +const H264_AVC_CONFIG = { + codec: 'avc1.42001e', // Baseline + width: 640, + height: 480, + displayWidth: 800, + displayHeight: 600, + avc: {format: 'avc'}, +}; + +let CONFIG = null; +promise_setup(async () => { + CONFIG = { + '?vp8': VP8_CONFIG, + '?h264_avc': H264_AVC_CONFIG, + }[location.search]; +}); promise_test(async t => { let codecInit = getDefaultCodecInit(t); - let encoderConfig = { - codec: 'vp8', - width: 640, - height: 480, - displayWidth: 800, - displayHeight: 600, - }; let outputs = 0; let firstOutput = new Promise(resolve => { @@ -24,7 +44,7 @@ promise_test(async t => { }); let encoder = new VideoEncoder(codecInit); - encoder.configure(encoderConfig); + encoder.configure(CONFIG); let frame1 = createFrame(640, 480, 0); let frame2 = createFrame(640, 480, 33333); @@ -45,3 +65,48 @@ promise_test(async t => { assert_equals(outputs, 1, 'outputs'); }, 'Test reset during flush'); + +promise_test(async t => { + let frame1 = createFrame(640, 480, 0); + let frame2 = createFrame(640, 480, 33333); + t.add_cleanup(() => { + frame1.close(); + frame2.close(); + }); + + const callbacks = {}; + const encoder = createVideoEncoder(t, callbacks); + + let flushInCallbackDone; + let outputs = 0; + let firstOutput = new Promise(resolve => { + callbacks.output = (chunk, metadata) => { + encoder.reset(); + + callbacks.output = (chunk, metadata) => { + outputs++; + }; + + encoder.configure(CONFIG); + encoder.encode(frame2); + flushInCallbackDone = encoder.flush(); + + resolve(); + }; + }); + + encoder.configure(CONFIG); + encoder.encode(frame1); + const flushDone = encoder.flush(); + + // Wait for the first output, then reset. + await firstOutput; + + // Flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + + // Wait for the second flush and check the output count. + await flushInCallbackDone; + + assert_equals(outputs, 1, 'outputs'); +}, 'Test new flush after reset in a flush callback'); diff --git a/testing/web-platform/tests/webcodecs/video-encoder-utils.js b/testing/web-platform/tests/webcodecs/video-encoder-utils.js index 0838260d31..916f995156 100644 --- a/testing/web-platform/tests/webcodecs/video-encoder-utils.js +++ b/testing/web-platform/tests/webcodecs/video-encoder-utils.js @@ -101,3 +101,22 @@ function createDottedFrame(width, height, dots, ts) { putBlackDots(ctx, width, height, dots); return new VideoFrame(cnv, { timestamp: ts, duration }); } + +function createVideoEncoder(t, callbacks) { + return new VideoEncoder({ + output(chunk, metadata) { + if (callbacks && callbacks.output) { + t.step(() => callbacks.output(chunk, metadata)); + } else { + t.unreached_func('unexpected output()'); + } + }, + error(e) { + if (callbacks && callbacks.error) { + t.step(() => callbacks.error(e)); + } else { + t.unreached_func('unexpected error()'); + } + } + }); +} diff --git a/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js b/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js index a3acb82ab2..1c3b8f120d 100644 --- a/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js +++ b/testing/web-platform/tests/webcodecs/videoDecoder-codec-specific.https.any.js @@ -556,6 +556,75 @@ promise_test(async t => { const callbacks = {}; const decoder = createVideoDecoder(t, callbacks); + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + const flushDone = decoder.flush(); + + let flushDoneInCallback; + let outputs = 0; + await new Promise(resolve => { + callbacks.output = frame => { + decoder.reset(); + frame.close(); + + callbacks.output = frame => { + outputs++; + frame.close(); + }; + callbacks.error = e => { + t.unreached_func('unexpected error()'); + }; + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + flushDoneInCallback = decoder.flush(); + + resolve(); + }; + }); + + // First flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + // Wait for the second flush and check the output count. + await flushDoneInCallback; + assert_equals(outputs, 1, 'outputs'); +}, 'Test new flush after reset in a flush callback'); + +promise_test(async t => { + await checkImplements(); + const callbacks = {}; + const decoder = createVideoDecoder(t, callbacks); + + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + const flushDone = decoder.flush(); + let flushDoneInCallback; + + await new Promise(resolve => { + callbacks.output = frame => { + decoder.reset(); + frame.close(); + + callbacks.output = frame => { frame.close(); }; + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + decoder.decode(createCorruptChunk(1)); + flushDoneInCallback = decoder.flush(); + + resolve(); + }; + }); + + // First flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + // Wait for the second flush and check the error in the rejected promise. + await promise_rejects_dom(t, 'EncodingError', flushDoneInCallback); +}, 'Test decoding a corrupt frame after reset in a flush callback'); + +promise_test(async t => { + await checkImplements(); + const callbacks = {}; + const decoder = createVideoDecoder(t, callbacks); + decoder.configure({...CONFIG, optimizeForLatency: true}); decoder.decode(CHUNKS[0]); diff --git a/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js b/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js new file mode 100644 index 0000000000..442efc4b0f --- /dev/null +++ b/testing/web-platform/tests/webcodecs/videoFrame-copyTo-rgb.any.js @@ -0,0 +1,252 @@ +// META: global=window,dedicatedworker +// META: script=/webcodecs/videoFrame-utils.js +// META: script=/webcodecs/video-encoder-utils.js + +function compareColors(actual, expected, tolerance, msg) { + let channel = ['R', 'G', 'B', 'A']; + for (let i = 0; i < 4; i++) { + assert_approx_equals( + actual[i], expected[i], tolerance, + `${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`); + } +} + +function rgb2yuv(r, g, b) { + let y = r * .299000 + g * .587000 + b * .114000 + let u = r * -.168736 + g * -.331264 + b * .500000 + 128 + let v = r * .500000 + g * -.418688 + b * -.081312 + 128 + + y = Math.round(y); + u = Math.round(u); + v = Math.round(v); + return { + y, u, v + } +} + +function makeI420Frames() { + const kYellow = {r: 0xFF, g: 0xFF, b: 0x00}; + const kRed = {r: 0xFF, g: 0x00, b: 0x00}; + const kBlue = {r: 0x00, g: 0x00, b: 0xFF}; + const kGreen = {r: 0x00, g: 0xFF, b: 0x00}; + const kPink = {r: 0xFF, g: 0x78, b: 0xFF}; + const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF}; + const kBlack = {r: 0x00, g: 0x00, b: 0x00}; + const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF}; + const smpte170m = { + matrix: 'smpte170m', + primaries: 'smpte170m', + transfer: 'smpte170m', + fullRange: false + }; + const bt709 = { + matrix: 'bt709', + primaries: 'bt709', + transfer: 'bt709', + fullRange: false + }; + + const result = []; + const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const colors = + [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink]; + const data = new Uint8Array(24); + for (let colorSpace of [null, smpte170m, bt709]) { + init.colorSpace = colorSpace; + result.push(new VideoFrame(data, init)); + for (let color of colors) { + color = rgb2yuv(color.r, color.g, color.b); + data.fill(color.y, 0, 16); + data.fill(color.u, 16, 20); + data.fill(color.v, 20, 24); + result.push(new VideoFrame(data, init)); + } + } + return result; +} + +function makeRGBXFrames() { + const kYellow = 0xFFFF00; + const kRed = 0xFF0000; + const kBlue = 0x0000FF; + const kGreen = 0x00FF00; + const kBlack = 0x000000; + const kWhite = 0xFFFFFF; + const smpte170m = { + matrix: 'smpte170m', + primaries: 'smpte170m', + transfer: 'smpte170m', + fullRange: false + }; + const bt709 = { + matrix: 'bt709', + primaries: 'bt709', + transfer: 'bt709', + fullRange: false + }; + + const result = []; + const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite]; + const data = new Uint32Array(16); + for (let colorSpace of [null, smpte170m, bt709]) { + init.colorSpace = colorSpace; + for (let color of colors) { + data.fill(color, 0, 16); + result.push(new VideoFrame(data, init)); + } + } + return result; +} + +async function testFrame(frame, colorSpace, pixelFormat) { + const width = frame.visibleRect.width; + const height = frame.visibleRect.height; + let frame_message = 'Frame: ' + JSON.stringify({ + format: frame.format, + width: width, + height: height, + matrix: frame.colorSpace?.matrix, + primaries: frame.colorSpace?.primaries, + transfer: frame.colorSpace?.transfer, + }); + const cnv = new OffscreenCanvas(width, height); + const ctx = + cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true}); + + // Read VideoFrame pixels via copyTo() + let imageData = ctx.createImageData(width, height); + let copy_to_buf = imageData.data.buffer; + let layout = null; + try { + const options = { + rect: {x: 0, y: 0, width: width, height: height}, + format: pixelFormat, + colorSpace: colorSpace + }; + layout = await frame.copyTo(copy_to_buf, options); + } catch (e) { + assert_unreached(`copyTo() failure: ${e}`); + return; + } + if (layout.length != 1) { + assert_unreached('Conversion to RGB is not supported by the browser'); + return; + } + + // Read VideoFrame pixels via drawImage() + ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height); + imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace}); + let get_image_buf = imageData.data.buffer; + + // Compare! + const tolerance = 1; + for (let i = 0; i < copy_to_buf.byteLength; i += 4) { + if (pixelFormat.startsWith('BGR')) { + // getImageData() always gives us RGB, we need to swap bytes before + // comparing them with BGR. + new Uint8Array(get_image_buf, i, 3).reverse(); + } + compareColors( + new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4), + tolerance, frame_message + ` Mismatch at offset ${i}`); + } +} + +function test_4x4_I420_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + for (let frame of makeI420Frames()) { + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + } + }, `Convert 4x4 I420 frames to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4x4_I420_frames(); + +function test_4x4_RGB_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + for (let frame of makeRGBXFrames()) { + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + } + }, `Convert 4x4 RGBX frames to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4x4_RGB_frames(); + + +function test_4color_canvas_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + const frame = createFrame(32, 16); + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + }, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4color_canvas_frames(); + +promise_test(async t => { + let pixelFormat = 'RGBA' + const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const src_data = new Uint32Array(init.codedWidth * init.codedHeight); + src_data.fill(0xFFFFFFFF); + const offset = 5; + const stride = 40; + const dst_data = new Uint8Array(offset + stride * init.codedHeight); + const options = { + format: pixelFormat, + layout: [ + {offset: offset, stride: stride}, + ] + }; + const frame = new VideoFrame(src_data, init); + await frame.copyTo(dst_data, options) + assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset'); + for (let row = 0; row < init.codedHeight; ++row) { + let width = init.codedWidth * 4; + const row_data = + dst_data.slice(offset + stride * row, offset + stride * row + width); + const margin_data = dst_data.slice( + offset + stride * row + width, offset + stride * (row + 1)); + + assert_false( + row_data.some(e => e != 0xFF), + `unexpected data in row ${row} [${row_data}]`); + assert_false( + margin_data.some(e => e != 0), + `unexpected margin in row ${row} [${margin_data}]`); + } + + frame.close(); +}, `copyTo() with layout`); + +function test_unsupported_pixel_formats() { + const kUnsupportedFormats = [ + 'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A', + 'NV12' + ]; + + for (let pixelFormat of kUnsupportedFormats) { + promise_test(async t => { + const init = + {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const data = new Uint32Array(16); + const options = {format: pixelFormat}; + const frame = new VideoFrame(data, init); + await promise_rejects_dom( + t, 'NotSupportedError', frame.copyTo(data, options)) + frame.close(); + }, `Unsupported format ${pixelFormat}`); + } +} +test_unsupported_pixel_formats(); |