Automatic update from web-platform-tests Ensure VideoEncoder readback preserves visible_rect and natural_size When texture-backed VideoFrames (e.g. from canvas) undergo readback via CopyRGBATextureToVideoFrame, the newly allocated destination frame has a visible_rect equal to the entire coded_size. VideoEncoder's metadata_fix_lambda copied timestamp and metadata but did not wrap the result frame with the source frame's visible_rect and natural_size. As a result, encoders would downscale the entire coded size rather than encoding the cropped sub-rectangle specified in visibleRect. This CL wraps the readback result_frame in VideoEncoder::StartReadback using media::VideoFrame::WrapVideoFrame to preserve visible_rect and natural_size. Fixed: 543284189 Change-Id: Id92a329b80595f64aff3196a86f1aafbc895dd0e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8235378 Reviewed-by: Eugene Zemtsov <eugene@chromium.org> Commit-Queue: Dale Curtis <dalecurtis@chromium.org> Cr-Commit-Position: refs/heads/main@{#1676956} -- wpt-commits: bf81dbf296e18bdc166c8a7912fc82c8b94e640e wpt-pr: 61845
269 lines
8.0 KiB
HTML
269 lines
8.0 KiB
HTML
<title>Test Encode VideoFrame from CanvasImageSource.</title>
|
|
<img src="four-colors.png"/>
|
|
<svg width="320" height="240" xmlns="http://www.w3.org/2000/svg">
|
|
<image href="four-colors.png" height="320" width="240"/>
|
|
</svg>
|
|
<canvas id=""></canvas>
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="/webcodecs/utils.js"></script>
|
|
<script>
|
|
promise_test(() => {
|
|
return new Promise(resolve => onload = resolve);
|
|
}, "Wait for onload event to get access to image data");
|
|
|
|
promise_test(async t => {
|
|
const img = document.querySelector('img');
|
|
const frame = new VideoFrame(img, {timestamp: 0});
|
|
assert_equals(frame.codedWidth, img.width);
|
|
assert_equals(frame.codedHeight, img.height);
|
|
|
|
let outputs = 0;
|
|
const codecInit = getDefaultCodecInit(t);
|
|
codecInit.output = (chunk, metadata) => {
|
|
outputs += 1;
|
|
}
|
|
|
|
const encoder = new VideoEncoder(codecInit);
|
|
encoder.configure({
|
|
codec: 'vp8',
|
|
width: frame.codedWidth,
|
|
height: frame.codedHeight,
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
encoder.close();
|
|
|
|
assert_equals(outputs, 1, 'outputs');
|
|
}, 'Test encode ImageElement-constructed VideoFrame');
|
|
|
|
promise_test(async t => {
|
|
const svg = document.querySelector('svg');
|
|
const svgImage = document.querySelector('image');
|
|
const frame = new VideoFrame(svgImage, {timestamp: 0});
|
|
assert_equals(frame.codedWidth, svg.width.baseVal.value);
|
|
assert_equals(frame.codedHeight, svg.height.baseVal.value);
|
|
|
|
let outputs = 0;
|
|
const codecInit = getDefaultCodecInit(t);
|
|
codecInit.output = (chunk, metadata) => {
|
|
outputs += 1;
|
|
}
|
|
|
|
const encoder = new VideoEncoder(codecInit);
|
|
encoder.configure({
|
|
codec: 'vp8',
|
|
width: frame.codedWidth,
|
|
height: frame.codedHeight,
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
encoder.close();
|
|
|
|
assert_equals(outputs, 1, 'outputs');
|
|
}, 'Test encode SVGImageElement-constructed VideoFrame');
|
|
|
|
function drawFourColors(canvas) {
|
|
let ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#FFFF00'; // yellow
|
|
ctx.fillRect(0, 0, canvas.width / 2, canvas.height / 2);
|
|
ctx.fillStyle = '#FF0000'; // red
|
|
ctx.fillRect(canvas.width / 2, 0, canvas.width / 2, canvas.height / 2);
|
|
ctx.fillStyle = '#0000FF'; // blue
|
|
ctx.fillRect(0, canvas.height / 2, canvas.width / 2, canvas.height / 2);
|
|
ctx.fillStyle = '#00FF00'; // green
|
|
ctx.fillRect(canvas.width / 2, canvas.height / 2, canvas.width / 2,
|
|
canvas.height / 2);
|
|
}
|
|
|
|
promise_test(async t => {
|
|
const canvas = document.querySelector('canvas');
|
|
canvas.width = 320;
|
|
canvas.height = 240;
|
|
drawFourColors(canvas);
|
|
|
|
const frame = new VideoFrame(canvas, {timestamp: 0});
|
|
assert_equals(frame.codedWidth, canvas.width);
|
|
assert_equals(frame.codedHeight, canvas.height);
|
|
|
|
let outputs = 0;
|
|
const codecInit = getDefaultCodecInit(t);
|
|
codecInit.output = (chunk, metadata) => {
|
|
outputs += 1;
|
|
}
|
|
|
|
const encoder = new VideoEncoder(codecInit);
|
|
encoder.configure({
|
|
codec: 'vp8',
|
|
width: canvas.width,
|
|
height: canvas.height,
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
encoder.close();
|
|
|
|
assert_equals(outputs, 1, 'outputs');
|
|
}, 'Test encode canvas-element-constructed VideoFrame');
|
|
|
|
promise_test(async t => {
|
|
const canvas = document.querySelector('canvas');
|
|
canvas.width = 320;
|
|
canvas.height = 240;
|
|
drawFourColors(canvas);
|
|
|
|
// Crop to top-left quadrant (yellow: #FFFF00).
|
|
const visibleRect = {x: 0, y: 0, width: 160, height: 120};
|
|
const frame = new VideoFrame(canvas, {timestamp: 0, visibleRect: visibleRect});
|
|
assert_equals(frame.codedWidth, canvas.width);
|
|
assert_equals(frame.codedHeight, canvas.height);
|
|
assert_equals(frame.visibleRect.x, 0);
|
|
assert_equals(frame.visibleRect.y, 0);
|
|
assert_equals(frame.visibleRect.width, 160);
|
|
assert_equals(frame.visibleRect.height, 120);
|
|
|
|
let outputChunk = null;
|
|
let outputMetadata = null;
|
|
const codecInit = getDefaultCodecInit(t);
|
|
codecInit.output = (chunk, metadata) => {
|
|
outputChunk = chunk;
|
|
if (metadata && metadata.decoderConfig) {
|
|
outputMetadata = metadata;
|
|
}
|
|
};
|
|
|
|
const encoder = new VideoEncoder(codecInit);
|
|
encoder.configure({
|
|
codec: 'vp8',
|
|
width: visibleRect.width,
|
|
height: visibleRect.height,
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
encoder.close();
|
|
|
|
assert_not_equals(outputChunk, null, 'output chunk');
|
|
|
|
let decodedFrame = null;
|
|
const decoder = new VideoDecoder({
|
|
output(f) {
|
|
decodedFrame = f;
|
|
},
|
|
error(e) {
|
|
assert_unreached(e.message);
|
|
},
|
|
});
|
|
decoder.configure(outputMetadata.decoderConfig);
|
|
decoder.decode(outputChunk);
|
|
await decoder.flush();
|
|
decoder.close();
|
|
|
|
assert_not_equals(decodedFrame, null, 'decoded frame');
|
|
assert_equals(decodedFrame.displayWidth, 160);
|
|
assert_equals(decodedFrame.displayHeight, 120);
|
|
|
|
const testCanvas = document.createElement('canvas');
|
|
testCanvas.width = 160;
|
|
testCanvas.height = 120;
|
|
const testCtx = testCanvas.getContext('2d');
|
|
testCtx.drawImage(decodedFrame, 0, 0);
|
|
decodedFrame.close();
|
|
|
|
// All 4 quadrants in the decoded frame should be yellow (#FFFF00).
|
|
const samplePoints = [
|
|
{x: 40, y: 30},
|
|
{x: 120, y: 30},
|
|
{x: 40, y: 90},
|
|
{x: 120, y: 90},
|
|
];
|
|
for (const pt of samplePoints) {
|
|
const pixel = testCtx.getImageData(pt.x, pt.y, 1, 1).data;
|
|
assert_greater_than(pixel[0], 200, `Red channel at (${pt.x}, ${pt.y})`);
|
|
assert_greater_than(pixel[1], 200, `Green channel at (${pt.x}, ${pt.y})`);
|
|
assert_less_than(pixel[2], 50, `Blue channel at (${pt.x}, ${pt.y})`);
|
|
}
|
|
}, 'Test encode canvas-element-constructed VideoFrame with visibleRect');
|
|
|
|
promise_test(async t => {
|
|
const canvas = document.querySelector('canvas');
|
|
canvas.width = 320;
|
|
canvas.height = 240;
|
|
drawFourColors(canvas);
|
|
|
|
// Crop to bottom-right quadrant (green: #00FF00).
|
|
const visibleRect = {x: 160, y: 120, width: 160, height: 120};
|
|
const frame = new VideoFrame(canvas, {timestamp: 0, visibleRect: visibleRect});
|
|
assert_equals(frame.codedWidth, canvas.width);
|
|
assert_equals(frame.codedHeight, canvas.height);
|
|
assert_equals(frame.visibleRect.x, 160);
|
|
assert_equals(frame.visibleRect.y, 120);
|
|
assert_equals(frame.visibleRect.width, 160);
|
|
assert_equals(frame.visibleRect.height, 120);
|
|
|
|
let outputChunk = null;
|
|
let outputMetadata = null;
|
|
const codecInit = getDefaultCodecInit(t);
|
|
codecInit.output = (chunk, metadata) => {
|
|
outputChunk = chunk;
|
|
if (metadata && metadata.decoderConfig) {
|
|
outputMetadata = metadata;
|
|
}
|
|
};
|
|
|
|
const encoder = new VideoEncoder(codecInit);
|
|
encoder.configure({
|
|
codec: 'vp8',
|
|
width: visibleRect.width,
|
|
height: visibleRect.height,
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
encoder.close();
|
|
|
|
assert_not_equals(outputChunk, null, 'output chunk');
|
|
|
|
let decodedFrame = null;
|
|
const decoder = new VideoDecoder({
|
|
output(f) {
|
|
decodedFrame = f;
|
|
},
|
|
error(e) {
|
|
assert_unreached(e.message);
|
|
},
|
|
});
|
|
decoder.configure(outputMetadata.decoderConfig);
|
|
decoder.decode(outputChunk);
|
|
await decoder.flush();
|
|
decoder.close();
|
|
|
|
assert_not_equals(decodedFrame, null, 'decoded frame');
|
|
assert_equals(decodedFrame.displayWidth, 160);
|
|
assert_equals(decodedFrame.displayHeight, 120);
|
|
|
|
const testCanvas = document.createElement('canvas');
|
|
testCanvas.width = 160;
|
|
testCanvas.height = 120;
|
|
const testCtx = testCanvas.getContext('2d');
|
|
testCtx.drawImage(decodedFrame, 0, 0);
|
|
decodedFrame.close();
|
|
|
|
// All 4 quadrants in the decoded frame should be green (#00FF00).
|
|
const samplePoints = [
|
|
{x: 40, y: 30},
|
|
{x: 120, y: 30},
|
|
{x: 40, y: 90},
|
|
{x: 120, y: 90},
|
|
];
|
|
for (const pt of samplePoints) {
|
|
const pixel = testCtx.getImageData(pt.x, pt.y, 1, 1).data;
|
|
assert_less_than(pixel[0], 50, `Red channel at (${pt.x}, ${pt.y})`);
|
|
assert_greater_than(pixel[1], 200, `Green channel at (${pt.x}, ${pt.y})`);
|
|
assert_less_than(pixel[2], 50, `Blue channel at (${pt.x}, ${pt.y})`);
|
|
}
|
|
}, 'Test encode canvas-element-constructed VideoFrame with non-zero offset visibleRect');
|
|
</script>
|