fix(engine): local renders lay out width-only videos like the browser (#5771)

* fix(engine): local renders lay out width-only videos like the browser

* chore(engine): name the poster instead of commenting it
This commit is contained in:
Miguel Ángel
2026-10-17 08:45:59 +00:00
committed by GitHub
parent a498cf2032
commit fc41fa277a
2 changed files with 77 additions and 13 deletions
@@ -0,0 +1,62 @@
import { spawnSync } from "node:child_process";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import puppeteer, { type Browser } from "puppeteer-core";
import { resolveHeadlessShellPath } from "./browserManager.js";
import { applyVideoMetadataHints } from "./frameCapture.js";
const chromePath = resolveHeadlessShellPath();
const hasBrowser = chromePath !== undefined && spawnSync(chromePath, ["--version"]).status === 0;
// Boxes Chrome gives these styles once a 640x360 video's metadata has loaded.
const cases = {
noSize: { style: "", box: "640x360" },
widthOnly: { style: "width:200px", box: "200x112.5" },
heightOnly: { style: "height:90px", box: "160x90" },
bothSizes: { style: "width:200px;height:300px;object-fit:cover", box: "200x300" },
maxWidthOnly: { style: "max-width:100px", box: "100x56.25" },
// A flex item may not shrink below its natural width, so its border overflows the 400px row.
borderedFlexItem: { style: "width:100%;border:8px solid", box: "416x241" },
};
const ids = Object.keys(cases);
describe.skipIf(!hasBrowser)("applyVideoMetadataHints in Chrome", () => {
let browser: Browser;
beforeAll(async () => {
browser = await puppeteer.launch({
executablePath: chromePath,
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
});
afterAll(async () => {
await browser?.close();
});
it("lays out a video without loaded metadata as it would with it", async () => {
const page = await browser.newPage();
const videos = Object.entries(cases).map(
([id, { style }]) =>
`<div style="display:flex;align-items:center;width:400px"><video id="${id}" style="${style}"></video></div>`,
);
await page.setContent(`<body style="margin:0">${videos.join("")}</body>`);
await applyVideoMetadataHints(
page,
ids.map((id) => ({ id, width: 640, height: 360 })),
);
const boxes = await page.evaluate((videoIds) => {
return Object.fromEntries(
videoIds.map((id) => {
const rect = document.getElementById(id)!.getBoundingClientRect();
return [id, `${rect.width}x${rect.height}`];
}),
);
}, ids);
expect(boxes).toEqual(
Object.fromEntries(Object.entries(cases).map(([id, { box }]) => [id, box])),
);
await page.close();
});
});
+15 -13
View File
@@ -2217,7 +2217,7 @@ async function decodeAllImages(page: Page): Promise<void> {
});
}
async function applyVideoMetadataHints(
export async function applyVideoMetadataHints(
page: Page,
hints: readonly CaptureVideoMetadataHint[] | undefined,
): Promise<void> {
@@ -2225,7 +2225,8 @@ async function applyVideoMetadataHints(
// fallow-ignore-next-line complexity
await page.evaluate(
(metadataHints: CaptureVideoMetadataHint[]) => {
async (metadataHints: CaptureVideoMetadataHint[]) => {
const posters = new Set<string>();
for (const hint of metadataHints) {
if (
!hint.id ||
@@ -2239,19 +2240,20 @@ async function applyVideoMetadataHints(
const video = (window.__hfMediaEl?.(hint.id) ??
document.getElementById(hint.id)) as HTMLVideoElement | null;
if (!video) continue;
if (!video || video.hasAttribute("poster")) continue;
if (!video.hasAttribute("width")) video.setAttribute("width", String(hint.width));
if (!video.hasAttribute("height")) video.setAttribute("height", String(hint.height));
const computed = window.getComputedStyle(video);
if (
!video.style.aspectRatio &&
(!computed.aspectRatio || computed.aspectRatio === "auto")
) {
video.style.aspectRatio = `${hint.width} / ${hint.height}`;
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${hint.width}" height="${hint.height}"/>`;
const intrinsicSizePoster = `data:image/svg+xml,${encodeURIComponent(svg)}`;
video.poster = intrinsicSizePoster;
posters.add(intrinsicSizePoster);
}
await Promise.all(
[...posters].map((src) => {
const image = new Image();
image.src = src;
return image.decode();
}),
);
},
[...hints],
);