Files
sousa-gecko/testing/web-platform/tests/webcodecs/video-encoder-canvasImageSource.https.html
Dale Curtis bb9178acd8 Bug 2062476 [wpt PR 61845] - Ensure VideoEncoder readback preserves visible_rect and natural_size, a=testonly
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
2026-08-13 09:18:27 +00:00

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>