mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-03 04:07:03 +00:00
fix(engine): a background image that never finishes decoding no longer stalls the frame
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Page } from "puppeteer-core";
|
||||
import { decodeDynamicCssBackgroundImages } from "./frameCapture.js";
|
||||
import { BACKGROUND_DECODE_TIMEOUT_MS, decodeDynamicCssBackgroundImages } from "./frameCapture.js";
|
||||
|
||||
function makeMockPage(
|
||||
getBackgroundImage: () => string,
|
||||
@@ -10,7 +10,7 @@ function makeMockPage(
|
||||
decodeImage?: (src: string) => Promise<void>,
|
||||
): Page {
|
||||
return {
|
||||
evaluate: async (fn: () => unknown) => {
|
||||
evaluate: async (fn: (arg?: unknown) => unknown, arg?: unknown) => {
|
||||
const previousDocument = globalThis.document;
|
||||
const previousImage = globalThis.Image;
|
||||
const previousWindow = globalThis.window;
|
||||
@@ -41,7 +41,7 @@ function makeMockPage(
|
||||
});
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
return await fn(arg);
|
||||
} finally {
|
||||
Object.assign(globalThis, {
|
||||
document: previousDocument,
|
||||
@@ -56,9 +56,12 @@ function makeMockPage(
|
||||
afterEach(() => {
|
||||
const root = globalThis as {
|
||||
__hf_css_background_decoded?: Set<string>;
|
||||
__hf_css_background_timed_out?: Set<string>;
|
||||
__hfDecodeDynamicCssBackgroundImages?: () => Promise<void>;
|
||||
};
|
||||
delete root.__hf_css_background_decoded;
|
||||
delete root.__hf_css_background_timed_out;
|
||||
vi.useRealTimers();
|
||||
delete root.__hfDecodeDynamicCssBackgroundImages;
|
||||
});
|
||||
|
||||
@@ -119,6 +122,23 @@ describe("decodeDynamicCssBackgroundImages", () => {
|
||||
expect(decoded).toEqual(["/assets/late.jpg", "/assets/late.jpg"]);
|
||||
});
|
||||
|
||||
it("stops waiting for a url whose decode never settles and names it", async () => {
|
||||
vi.useFakeTimers();
|
||||
const decoded: string[] = [];
|
||||
const page = makeMockPage(
|
||||
() => 'url("/assets/stuck.jpg")',
|
||||
decoded,
|
||||
() => new Promise<void>(() => {}),
|
||||
);
|
||||
|
||||
const first = decodeDynamicCssBackgroundImages(page);
|
||||
await vi.advanceTimersByTimeAsync(BACKGROUND_DECODE_TIMEOUT_MS);
|
||||
expect(await first).toEqual(["/assets/stuck.jpg"]);
|
||||
|
||||
expect(await decodeDynamicCssBackgroundImages(page)).toEqual([]);
|
||||
expect(decoded).toEqual(["/assets/stuck.jpg"]);
|
||||
});
|
||||
|
||||
it("installs a page-local decoder that can run after an in-page seek", async () => {
|
||||
let backgroundImage = 'url("/assets/row-0.jpg")';
|
||||
const decoded: string[] = [];
|
||||
|
||||
@@ -452,15 +452,25 @@ export function getDrawElementVerificationDetails(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Wait for inline CSS background images introduced by the latest seek. */
|
||||
export async function decodeDynamicCssBackgroundImages(page: Page): Promise<void> {
|
||||
await page.evaluate(async () => {
|
||||
/** Longest the capture waits for one inline background image to decode; it is then left out. */
|
||||
export const BACKGROUND_DECODE_TIMEOUT_MS = 15_000;
|
||||
|
||||
/**
|
||||
* Wait for inline CSS background images introduced by the latest seek.
|
||||
* Resolves with the urls that did not settle in time: they are captured as they stand and not waited on again.
|
||||
*/
|
||||
export async function decodeDynamicCssBackgroundImages(page: Page): Promise<string[]> {
|
||||
return page.evaluate(async (timeoutMs: number) => {
|
||||
const root = globalThis as typeof globalThis & {
|
||||
__hf_css_background_decoded?: Set<string>;
|
||||
__hfDecodeDynamicCssBackgroundImages?: () => Promise<void>;
|
||||
__hf_css_background_timed_out?: Set<string>;
|
||||
__hf_css_background_timeout_ms?: number;
|
||||
__hfDecodeDynamicCssBackgroundImages?: () => Promise<string[]>;
|
||||
};
|
||||
const decode = (root.__hfDecodeDynamicCssBackgroundImages ??= async () => {
|
||||
const limitMs = root.__hf_css_background_timeout_ms ?? 0;
|
||||
const decoded = (root.__hf_css_background_decoded ??= new Set<string>());
|
||||
const timedOut = (root.__hf_css_background_timed_out ??= new Set<string>());
|
||||
const urls: string[] = [];
|
||||
const parseBackgroundUrls = (value: string): string[] => {
|
||||
const found: string[] = [];
|
||||
@@ -514,26 +524,42 @@ export async function decodeDynamicCssBackgroundImages(page: Page): Promise<void
|
||||
if (!backgroundImage || backgroundImage === "none") continue;
|
||||
|
||||
for (const url of parseBackgroundUrls(backgroundImage)) {
|
||||
if (!decoded.has(url)) urls.push(url);
|
||||
if (!decoded.has(url) && !timedOut.has(url)) urls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
const late: string[] = [];
|
||||
await Promise.all(
|
||||
[...new Set(urls)].map(async (url) => {
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const expired = new Promise<"late">((resolve) => {
|
||||
timer = setTimeout(() => resolve("late"), limitMs);
|
||||
});
|
||||
try {
|
||||
await image.decode();
|
||||
decoded.add(url);
|
||||
const outcome = await Promise.race([
|
||||
image.decode().then(() => "ready" as const),
|
||||
expired,
|
||||
]);
|
||||
if (outcome === "ready") decoded.add(url);
|
||||
else {
|
||||
timedOut.add(url);
|
||||
late.push(url);
|
||||
}
|
||||
} catch {
|
||||
// Keep existing capture behavior for missing assets; request diagnostics report them.
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return late;
|
||||
});
|
||||
|
||||
await decode();
|
||||
});
|
||||
root.__hf_css_background_timeout_ms = timeoutMs;
|
||||
return decode();
|
||||
}, BACKGROUND_DECODE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
// Circular buffer for browser console messages dumped on render failure diagnostics.
|
||||
@@ -3291,7 +3317,12 @@ async function prepareFrameForCapture(
|
||||
const hasPendingComposite = await seekPageTimeline(page, quantizedTime, seekOptions);
|
||||
|
||||
session.captureStep = "decoding CSS background images";
|
||||
await decodeDynamicCssBackgroundImages(page);
|
||||
for (const url of await decodeDynamicCssBackgroundImages(page)) {
|
||||
appendBrowserDiagnostic(
|
||||
session,
|
||||
`[FrameCapture] background image still decoding after ${BACKGROUND_DECODE_TIMEOUT_MS}ms, captured without it: ${url}`,
|
||||
);
|
||||
}
|
||||
|
||||
const seekMs = Date.now() - seekStart;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user