// META: global=window,dedicatedworker // META: script=/common/media.js // META: script=/webcodecs/utils.js const defaultConfig = { codec: 'vp8', framerate: 25, width: 640, height: 480 }; async function generateBitmap(width, height) { const src = "pattern.png"; var size = { resizeWidth: width, resizeHeight: height }; return fetch(src) .then(response => response.blob()) .then(blob => createImageBitmap(blob, size)); } async function createVideoFrame(width, height, timestamp) { let bitmap = await generateBitmap(width, height); return new VideoFrame(bitmap, { timestamp: timestamp }); } promise_test(t => { // VideoEncoderInit lacks required fields. assert_throws_js(TypeError, () => { new VideoEncoder({}); }); // VideoEncoderInit has required fields. let encoder = new VideoEncoder(getDefaultCodecInit(t)); assert_equals(encoder.state, "unconfigured"); encoder.close(); return endAfterEventLoopTurn(); }, 'Test VideoEncoder construction'); promise_test(t => { let encoder = new VideoEncoder(getDefaultCodecInit(t)); let badCodecsList = [ '', // Empty codec 'bogus', // Non exsitent codec 'vorbis', // Audio codec 'vp9', // Ambiguous codec 'video/webm; codecs="vp9"' // Codec with mime type ] testConfigurations(encoder, defaultConfig, badCodecsList); return endAfterEventLoopTurn(); }, 'Test VideoEncoder.configure()'); promise_test(async t => { let output_chunks = []; let codecInit = getDefaultCodecInit(t); codecInit.output = chunk => output_chunks.push(chunk); let encoder = new VideoEncoder(codecInit); // No encodes yet. assert_equals(encoder.encodeQueueSize, 0); encoder.configure(defaultConfig); // Still no encodes. assert_equals(encoder.encodeQueueSize, 0); let frame1 = await createVideoFrame(640, 480, 0); let frame2 = await createVideoFrame(640, 480, 33333); encoder.encode(frame1.clone()); encoder.encode(frame2.clone()); // Could be 0, 1, or 2. We can't guarantee this check runs before the UA has // processed the encodes. assert_true(encoder.encodeQueueSize >= 0 && encoder.encodeQueueSize <= 2) await encoder.flush(); // We can guarantee that all encodes are processed after a flush. assert_equals(encoder.encodeQueueSize, 0); assert_equals(output_chunks.length, 2); assert_equals(output_chunks[0].timestamp, frame1.timestamp); assert_equals(output_chunks[1].timestamp, frame2.timestamp); }, 'Test successful configure(), encode(), and flush()'); promise_test(async t => { let callbacks_before_reset = 0; let callbacks_after_reset = 0; let codecInit = getDefaultCodecInit(t); codecInit.output = chunk => { if (chunk.timestamp % 2 == 0) { // pre-reset frames have even timestamp callbacks_before_reset++; } else { // after-reset frames have odd timestamp callbacks_after_reset++; } } let encoder = new VideoEncoder(codecInit); encoder.configure(defaultConfig); await encoder.flush(); let frames = []; for (let i = 0; i < 200; i++) { let frame = await createVideoFrame(640, 480, i * 40_000); frames.push(frame); } for (frame of frames) encoder.encode(frame); // Wait for the first frame to be encoded await t.step_wait(() => callbacks_before_reset > 0, "Encoded outputs started coming", 10000, 1); let saved_callbacks_before_reset = callbacks_before_reset; assert_greater_than(callbacks_before_reset, 0); assert_less_than_equal(callbacks_before_reset, frames.length); encoder.reset(); assert_equals(encoder.encodeQueueSize, 0); let newConfig = { ...defaultConfig }; newConfig.width = 800; newConfig.height = 600; encoder.configure(newConfig); for (let i = frames.length; i < frames.length + 5; i++) { let frame = await createVideoFrame(800, 600, i * 40_000 + 1); encoder.encode(frame); } await encoder.flush(); assert_equals(callbacks_after_reset, 5); assert_equals(saved_callbacks_before_reset, callbacks_before_reset); assert_equals(encoder.encodeQueueSize, 0); }, 'Test successful reset() and re-confiugre()'); promise_test(async t => { let output_chunks = []; let codecInit = getDefaultCodecInit(t); codecInit.output = chunk => output_chunks.push(chunk); let encoder = new VideoEncoder(codecInit); // No encodes yet. assert_equals(encoder.encodeQueueSize, 0); let config = defaultConfig; encoder.configure(config); let frame1 = await createVideoFrame(640, 480, 0); let frame2 = await createVideoFrame(640, 480, 33333); encoder.encode(frame1.clone()); encoder.configure(config); encoder.encode(frame2.clone()); await encoder.flush(); // We can guarantee that all encodes are processed after a flush. assert_equals(encoder.encodeQueueSize, 0); // The first frame may have been dropped when reconfiguring. // This shouldn't happen, and should be fixed/called out in the spec, but // this is preptively added to prevent flakiness. // TODO: Remove these checks when implementations handle this correctly. assert_true(output_chunks.length == 1 || output_chunks.length == 2); if (output_chunks.length == 1) { // If we only have one chunk frame, make sure we droped the frame that was // in flight when we reconfigured. assert_equals(output_chunks[0].timestamp, frame2.timestamp); } else { assert_equals(output_chunks[0].timestamp, frame1.timestamp); assert_equals(output_chunks[1].timestamp, frame2.timestamp); } output_chunks = []; let frame3 = await createVideoFrame(640, 480, 66666); let frame4 = await createVideoFrame(640, 480, 100000); encoder.encode(frame3.clone()); // Verify that a failed call to configure does not change the encoder's state. let badConfig = { ...defaultConfig }; badConfig.codec = 'bogus'; assert_throws_js(TypeError, () => encoder.configure(badConfig)); encoder.encode(frame4.clone()); await encoder.flush(); assert_equals(output_chunks[0].timestamp, frame3.timestamp); assert_equals(output_chunks[1].timestamp, frame4.timestamp); }, 'Test successful encode() after re-configure().'); promise_test(async t => { let output_chunks = []; let codecInit = getDefaultCodecInit(t); codecInit.output = chunk => output_chunks.push(chunk); let encoder = new VideoEncoder(codecInit); let timestamp = 33333; let frame = await createVideoFrame(640, 480, timestamp); encoder.configure(defaultConfig); assert_equals(encoder.state, "configured"); encoder.encode(frame); // |frame| is not longer valid since it has been closed. assert_not_equals(frame.timestamp, timestamp); assert_throws_dom("InvalidStateError", () => frame.clone()); encoder.close(); return endAfterEventLoopTurn(); }, 'Test encoder consumes (closes) frames.'); promise_test(async t => { let encoder = new VideoEncoder(getDefaultCodecInit(t)); let frame = await createVideoFrame(640, 480, 0); return testClosedCodec(t, encoder, defaultConfig, frame); }, 'Verify closed VideoEncoder operations'); promise_test(async t => { let encoder = new VideoEncoder(getDefaultCodecInit(t)); let frame = await createVideoFrame(640, 480, 0); return testUnconfiguredCodec(t, encoder, frame); }, 'Verify unconfigured VideoEncoder operations'); promise_test(async t => { let encoder = new VideoEncoder(getDefaultCodecInit(t)); let frame = await createVideoFrame(640, 480, 0); frame.close(); encoder.configure(defaultConfig); assert_throws_dom("OperationError", () => { encoder.encode(frame) }); }, 'Verify encoding closed frames throws.');