| <!DOCTYPE html> |
| <html> |
| <title>Test the VideoTrackReader API.</title> |
| <body> |
| <img id='frame_image' src="pattern.png"> |
| </body> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/common/media.js"></script> |
| <script> |
| |
| const defaultConfig = { |
| codec: 'vp8', |
| framerate: 25, |
| width: 640, |
| height: 480 |
| }; |
| |
| async function generateBitmap(width, height) { |
| return createImageBitmap(document.getElementById('frame_image'), |
| { resizeWidth: width, |
| resizeHeight: height }); |
| } |
| |
| async function createVideoFrame(width, height, timestamp) { |
| let bitmap = await generateBitmap(width, height); |
| return new VideoFrame(bitmap, { timestamp: timestamp }); |
| } |
| |
| // Calls done after giving async output/error callbacks a final chance to run. |
| async function asyncDone(test) { |
| test.step_timeout(test.done.bind(test), 0); |
| } |
| |
| async_test(async (t) => { |
| // VideoEncoderInit lacks required fields. |
| assert_throws_js(TypeError, () => { new VideoEncoder({}); }); |
| |
| // VideoEncoderInit has required fields. |
| let encoder = new VideoEncoder({ |
| output(chunk) { t.unreached_func("Unexpected output").call(); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| assert_equals(encoder.state, "unconfigured"); |
| |
| encoder.close(); |
| |
| asyncDone(t); |
| }, 'Test VideoEncoder construction'); |
| |
| async_test(async (t) => { |
| let encoder = new VideoEncoder({ |
| output(chunk) { t.unreached_func("Unexpected output").call(); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| const requiredConfigPairs = defaultConfig; |
| let incrementalConfig = {}; |
| |
| for (let key in requiredConfigPairs) { |
| // Configure should fail while required keys are missing. |
| assert_throws_js(TypeError, () => { encoder.configure(incrementalConfig); }); |
| incrementalConfig[key] = requiredConfigPairs[key]; |
| assert_equals(encoder.state, "unconfigured"); |
| } |
| |
| // Configure should pass once incrementalConfig meets all requirements. |
| encoder.configure(incrementalConfig); |
| assert_equals(encoder.state, "configured"); |
| |
| // We should be able to reconfigure the encoder. |
| encoder.configure(incrementalConfig); |
| assert_equals(encoder.state, "configured"); |
| |
| let config = incrementalConfig; |
| |
| // Bogus codec rejected. |
| config.codec = 'bogus'; |
| assert_throws_js(TypeError, () => { encoder.configure(config); }); |
| |
| // Audio codec rejected. |
| config.codec = 'vorbis'; |
| assert_throws_js(TypeError, () => { encoder.configure(config); }); |
| |
| // Ambiguous codec rejected. |
| config.codec = 'vp9'; |
| assert_throws_js(TypeError, () => { encoder.configure(config); }); |
| |
| // Codec with mime type rejected. |
| config.codec = 'video/webm; codecs="vp9"'; |
| assert_throws_js(TypeError, () => { encoder.configure(config); }); |
| |
| // The failed configures should not affect the current config. |
| assert_equals(encoder.state, "configured"); |
| |
| // Test we can configure after a reset. |
| encoder.reset() |
| assert_equals(encoder.state, "unconfigured"); |
| |
| encoder.configure(defaultConfig); |
| assert_equals(encoder.state, "configured"); |
| |
| encoder.close(); |
| |
| asyncDone(t); |
| }, 'Test VideoEncoder.configure()'); |
| |
| async_test(async (t) => { |
| let encoder = new VideoEncoder({ |
| output(chunk) { t.unreached_func("Unexpected output").call(); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| let videoFrame = await createVideoFrame(640, 480, 0); |
| |
| assert_throws_dom('InvalidStateError', |
| () => { encoder.encode(videoFrame); }, |
| 'first encode'); |
| |
| // Once more for good measure. |
| assert_throws_dom('InvalidStateError', |
| () => { encoder.encode(videoFrame); }, |
| 'second encode'); |
| |
| encoder.close(); |
| |
| asyncDone(t); |
| }, 'Test encode() before configure() throws InvalidStateError.'); |
| |
| async_test(async (t) => { |
| let output_chunks = []; |
| let encoder = new VideoEncoder({ |
| output(chunk) { output_chunks.push(chunk); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| // 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); |
| |
| encoder.close(); |
| |
| asyncDone(t); |
| }, 'Test successful configure(), encode(), and flush()'); |
| |
| async_test(async (t) => { |
| let output_chunks = []; |
| let encoder = new VideoEncoder({ |
| output(chunk) { output_chunks.push(chunk); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| // 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. |
| config.codec = 'bogus'; |
| assert_throws_js(TypeError, () => encoder.configure(config)); |
| |
| encoder.encode(frame4.clone()); |
| |
| await encoder.flush(); |
| |
| assert_equals(output_chunks[0].timestamp, frame3.timestamp); |
| assert_equals(output_chunks[1].timestamp, frame4.timestamp); |
| |
| encoder.close(); |
| |
| asyncDone(t); |
| }, 'Test successful encode() after re-configure().'); |
| |
| async_test(async (t) => { |
| let output_chunks = []; |
| let encoder = new VideoEncoder({ |
| output(chunk) { output_chunks.push(chunk); }, |
| error(error) { t.unreached_func("Unexpected error:" + error).call(); }, |
| }); |
| |
| let timestamp = 33333; |
| let frame = await createVideoFrame(640, 480, timestamp); |
| |
| t.step(() => { |
| // No encodes yet. |
| assert_equals(encoder.encodeQueueSize, 0); |
| |
| encoder.configure(defaultConfig); |
| |
| encoder.encode(frame); |
| |
| assert_not_equals(frame.timestamp, timestamp); |
| assert_throws_dom("InvalidStateError", () => frame.clone()); |
| |
| encoder.close(); |
| }); |
| |
| asyncDone(t); |
| }, 'Test encoder consumes (destroys) frames.'); |
| |
| promise_test(async t => { |
| let encoder = new VideoEncoder({ |
| output(frame) { |
| t.step(() => { |
| throw "unexpected output"; |
| }); |
| }, |
| error(e) { |
| t.step(() => { |
| throw "unexpected error"; |
| }); |
| } |
| }); |
| |
| encoder.close(); |
| |
| assert_equals(encoder.state, "closed") |
| |
| let frame = await createVideoFrame(640, 480, 0); |
| |
| assert_throws_dom("InvalidStateError", |
| () => encoder.configure(defaultConfig), |
| "configure"); |
| assert_throws_dom("InvalidStateError", |
| () => encoder.encode(frame), |
| "encode"); |
| assert_throws_dom("InvalidStateError", |
| () => encoder.reset(), |
| "reset"); |
| assert_throws_dom("InvalidStateError", |
| () => encoder.close(), |
| "close"); |
| return promise_rejects_dom(t, 'InvalidStateError', encoder.flush(), 'flush'); |
| }, 'Closed encoder'); |
| |
| promise_test(async t => { |
| let encoder = new VideoEncoder({ |
| output(frame) { |
| t.step(() => { |
| throw "unexpected output"; |
| }); |
| }, |
| error(e) { |
| t.step(() => { |
| throw "unexpected error"; |
| }); |
| } |
| }); |
| |
| assert_equals(encoder.state, "unconfigured"); |
| |
| let frame = await createVideoFrame(640, 480, 0); |
| |
| // Resetting an unconfigured encoder is a no-op. |
| encoder.reset(); |
| assert_equals(encoder.state, "unconfigured"); |
| |
| assert_throws_dom("InvalidStateError", |
| () => encoder.encode(frame), |
| "encode"); |
| return promise_rejects_dom(t, 'InvalidStateError', encoder.flush(), 'flush'); |
| }, 'Unconfigured encoder'); |
| |
| </script> |
| </html> |