Skip to content

Commit

Permalink
Expose VideoEncoder to workers
Browse files Browse the repository at this point in the history
This CL mirrors the changes landed in
acbd2eae0763947f78244814c2bdd5628778813d, and exposes VideoEncoder to
workers.

The changes in this CL rely on the conclusions from the previous CL.
Notably, there is a need for a synchronous wait when getting the GPU
factories due to some threading issues, and there is no support for
logging from workers.

Bug: 1094169
Change-Id: I2da6cdb2afd7b80e342268cbbfcb00fea64ad37f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2578002
Commit-Queue: Thomas Guilbert <[email protected]>
Reviewed-by: Eugene Zemtsov <[email protected]>
Reviewed-by: Dan Sanders <[email protected]>
Cr-Commit-Position: refs/heads/master@{#835551}
  • Loading branch information
tguilbert-google authored and chromium-wpt-export-bot committed Dec 10, 2020
1 parent d68891c commit 2b4f117
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 281 deletions.
262 changes: 262 additions & 0 deletions webcodecs/video-encoder.any.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// 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 destroyed.
assert_not_equals(frame.timestamp, timestamp);
assert_throws_dom("InvalidStateError", () => frame.clone());

encoder.close();

return endAfterEventLoopTurn();
}, 'Test encoder consumes (destroys) 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.destroy();

encoder.configure(defaultConfig);

frame.destroy();
assert_throws_dom("OperationError", () => {
encoder.encode(frame)
});
}, 'Verify encoding destroyed frames throws.');
Loading

0 comments on commit 2b4f117

Please sign in to comment.