mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(engine): publish video frames only after decoding (#6092)
This commit is contained in:
@@ -662,6 +662,7 @@ async function injectResolvedVideoFramesBatch(
|
||||
) => {
|
||||
const injectedIds: string[] = [];
|
||||
const pendingDecodes: Array<Promise<void>> = [];
|
||||
const imagesToPublish: HTMLImageElement[] = [];
|
||||
const replacementLayoutProperties = new Set([
|
||||
"width",
|
||||
"height",
|
||||
@@ -803,6 +804,8 @@ async function injectResolvedVideoFramesBatch(
|
||||
img.style.objectPosition = computedStyle.objectPosition;
|
||||
img.style.zIndex = computedStyle.zIndex;
|
||||
|
||||
// Publish after full decode so raster cannot race it with a reduced JPEG decode.
|
||||
img.style.setProperty("visibility", "hidden", "important");
|
||||
img.decoding = "sync";
|
||||
if (img.getAttribute("src") !== item.dataUri) {
|
||||
if (item.crossOrigin) img.crossOrigin = item.crossOrigin;
|
||||
@@ -821,7 +824,7 @@ async function injectResolvedVideoFramesBatch(
|
||||
);
|
||||
}
|
||||
img.style.opacity = String(computedOpacity);
|
||||
img.style.visibility = "visible";
|
||||
imagesToPublish.push(img);
|
||||
// Hide the native <video> with visibility only — never clobber inline
|
||||
// opacity, so subsequent reads (and queryElementStacking) see the real
|
||||
// GSAP-controlled value.
|
||||
@@ -832,6 +835,7 @@ async function injectResolvedVideoFramesBatch(
|
||||
if (pendingDecodes.length > 0) {
|
||||
await Promise.all(pendingDecodes);
|
||||
}
|
||||
for (const img of imagesToPublish) img.style.visibility = "visible";
|
||||
if (injectedIds.length > 0) {
|
||||
const redraw = (window as Window & { __hf?: { colorGrading?: { redraw?: () => void } } })
|
||||
.__hf?.colorGrading?.redraw;
|
||||
|
||||
@@ -105,6 +105,42 @@ describe.skipIf(!hasBrowser)("video frame transport in Chrome", () => {
|
||||
expect(unexpectedFrameRequests).toBe(0);
|
||||
});
|
||||
|
||||
it("publishes a replacement only after its full-resolution decode completes", async () => {
|
||||
await injectVideoFramesBatch(page, [{ videoId: "a", dataUri: blueJpeg }]);
|
||||
await page.addStyleTag({ content: ".__render_frame__ { visibility: visible !important }" });
|
||||
await page.evaluate(() => {
|
||||
const decode = HTMLImageElement.prototype.decode;
|
||||
HTMLImageElement.prototype.decode = async function () {
|
||||
document.documentElement.setAttribute("data-decode-pending", "1");
|
||||
await new Promise<void>((resolve) =>
|
||||
window.addEventListener("release-frame-decode", () => resolve(), { once: true }),
|
||||
);
|
||||
await decode.call(this);
|
||||
};
|
||||
});
|
||||
const injection = injectVideoFramesBatch(page, [{ videoId: "a", dataUri: redPng }]);
|
||||
try {
|
||||
await page.waitForSelector('[data-decode-pending="1"]');
|
||||
const visibility = await page.evaluate(() => {
|
||||
const img = document.querySelector<HTMLImageElement>("img.__render_frame__");
|
||||
if (!img) throw new Error("No replacement frame");
|
||||
return getComputedStyle(img).visibility;
|
||||
});
|
||||
expect(visibility).toBe("hidden");
|
||||
} finally {
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("release-frame-decode")));
|
||||
await injection;
|
||||
}
|
||||
expect((await pixels())[0]?.rgba).toEqual([255, 0, 0, 255]);
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
const img = document.querySelector("img");
|
||||
if (!img) throw new Error("No replacement frame");
|
||||
return getComputedStyle(img).visibility;
|
||||
}),
|
||||
).toBe("visible");
|
||||
});
|
||||
|
||||
it("loads frames even while all same-origin download connections are busy", async () => {
|
||||
await page.evaluate(() => {
|
||||
for (let i = 0; i < 6; i++) void fetch(`/slow${i}.mp4`);
|
||||
|
||||
Reference in New Issue
Block a user