mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 13:07:11 +00:00
fix(engine): a background decode timeout is reported at every caller, url redacted
This commit is contained in:
@@ -1002,7 +1002,13 @@ export async function produceDrawElementFrameBatch(
|
||||
width: number,
|
||||
height: number,
|
||||
quality = 80,
|
||||
): Promise<{ encodeResults: Array<Promise<Buffer>>; failedAt: number | null; error?: string }> {
|
||||
): Promise<{
|
||||
encodeResults: Array<Promise<Buffer>>;
|
||||
failedAt: number | null;
|
||||
error?: string;
|
||||
/** Background urls whose decode timed out during this batch. */
|
||||
lateBackgrounds: string[];
|
||||
}> {
|
||||
const state = workerEncodeStates.get(page);
|
||||
if (!state) {
|
||||
throw new Error(
|
||||
@@ -1070,7 +1076,8 @@ export async function produceDrawElementFrameBatch(
|
||||
__hf3d?: { update: () => void };
|
||||
__hf?: { seek?: (t: number) => void };
|
||||
__hfWaitForSeekCompletion?: () => Promise<void>;
|
||||
__hfDecodeDynamicCssBackgroundImages?: () => Promise<void>;
|
||||
__hfDecodeDynamicCssBackgroundImages?: () => Promise<string[] | void>;
|
||||
__hf_css_background_late?: string[];
|
||||
__hfDeInvalidate?: () => boolean;
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
@@ -1106,7 +1113,8 @@ export async function produceDrawElementFrameBatch(
|
||||
try {
|
||||
if (aw.__hf && typeof aw.__hf.seek === "function") aw.__hf.seek(t);
|
||||
await aw.__hfWaitForSeekCompletion?.();
|
||||
await aw.__hfDecodeDynamicCssBackgroundImages?.();
|
||||
const late = await aw.__hfDecodeDynamicCssBackgroundImages?.();
|
||||
if (late?.length) (aw.__hf_css_background_late ??= []).push(...late);
|
||||
aw.__hf3d?.update();
|
||||
const accel = (aw.__hf_accel_canvases ?? []).filter((c) => root.contains(c));
|
||||
for (const c of accel) {
|
||||
@@ -1233,5 +1241,9 @@ export async function produceDrawElementFrameBatch(
|
||||
}
|
||||
}
|
||||
|
||||
return { encodeResults, failedAt: outcome.failedAt, error: outcome.error };
|
||||
const lateBackgrounds = await page.evaluate(() => {
|
||||
const w = window as Window & { __hf_css_background_late?: string[] };
|
||||
return w.__hf_css_background_late?.splice(0) ?? [];
|
||||
});
|
||||
return { encodeResults, failedAt: outcome.failedAt, error: outcome.error, lateBackgrounds };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Page } from "puppeteer-core";
|
||||
import { BACKGROUND_DECODE_TIMEOUT_MS, decodeDynamicCssBackgroundImages } from "./frameCapture.js";
|
||||
import {
|
||||
BACKGROUND_DECODE_TIMEOUT_MS,
|
||||
decodeDynamicCssBackgroundImages,
|
||||
recordBackgroundDecodeTimeouts,
|
||||
type CaptureSession,
|
||||
} from "./frameCapture.js";
|
||||
|
||||
function makeMockPage(
|
||||
getBackgroundImage: () => string,
|
||||
@@ -139,6 +144,30 @@ describe("decodeDynamicCssBackgroundImages", () => {
|
||||
expect(decoded).toEqual(["/assets/stuck.jpg"]);
|
||||
});
|
||||
|
||||
it("reports a timed-out url without its credentials or query, in the text and the warning", () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const session = { warnings: [], browserConsoleBuffer: [] } as unknown as CaptureSession;
|
||||
const secret = "https://user:pw@cdn.test/bg.jpg?token=SECRET#frag";
|
||||
|
||||
recordBackgroundDecodeTimeouts(session, [secret]);
|
||||
recordBackgroundDecodeTimeouts(session, [secret]);
|
||||
|
||||
expect(session.browserConsoleBuffer.join("\n")).toContain("https://cdn.test/bg.jpg");
|
||||
expect(JSON.stringify([session.browserConsoleBuffer, session.warnings])).not.toMatch(
|
||||
/SECRET|user|pw@|frag/,
|
||||
);
|
||||
expect(session.warnings).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "media_readiness_timeout",
|
||||
details: {
|
||||
mediaType: "image",
|
||||
sources: ["https://cdn.test/bg.jpg"],
|
||||
timeoutMs: BACKGROUND_DECODE_TIMEOUT_MS,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
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[] = [];
|
||||
@@ -168,7 +197,7 @@ describe("decodeDynamicCssBackgroundImages", () => {
|
||||
);
|
||||
|
||||
expect(batchSource).toMatch(
|
||||
/aw\.__hf\.seek\(t\);\s*await aw\.__hfWaitForSeekCompletion\?\.\(\);\s*await aw\.__hfDecodeDynamicCssBackgroundImages\?\.\(\);/,
|
||||
/aw\.__hf\.seek\(t\);\s*await aw\.__hfWaitForSeekCompletion\?\.\(\);\s*const late = await aw\.__hfDecodeDynamicCssBackgroundImages\?\.\(\);\s*if \(late\?\.length\) \(aw\.__hf_css_background_late \?\?= \[\]\)\.push\(\.\.\.late\);/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,6 +125,7 @@ describe("fresh screenshot fallback", () => {
|
||||
vi.mocked(produceDrawElementFrameBatch).mockImplementationOnce(async () => ({
|
||||
encodeResults: [Promise.resolve(Buffer.from("ok")), Promise.reject(timedOut(8))],
|
||||
failedAt: null,
|
||||
lateBackgrounds: [],
|
||||
}));
|
||||
const results = await captureFramesBatchPipelined(await session(), [7, 8], [7 / 30, 8 / 30]);
|
||||
expect((await results[0]!.encodeResult).toString()).toBe("ok");
|
||||
@@ -255,6 +256,7 @@ describe("fresh screenshot fallback", () => {
|
||||
vi.mocked(produceDrawElementFrameBatch).mockResolvedValueOnce({
|
||||
encodeResults: [prefix],
|
||||
failedAt: 1,
|
||||
lateBackgrounds: [],
|
||||
error,
|
||||
});
|
||||
await expect(
|
||||
|
||||
@@ -1385,7 +1385,7 @@ async function finalizeDrawElementInit(
|
||||
const { transparent, forceDE } = opts;
|
||||
// Install the page-local decoder before batch drawElement capture begins;
|
||||
// the batch loop re-runs it after every in-page seek.
|
||||
await decodeDynamicCssBackgroundImages(page);
|
||||
recordBackgroundDecodeTimeouts(session, await decodeDynamicCssBackgroundImages(page));
|
||||
// Self-verification ground truth: must run pre-injection — after the canvas
|
||||
// wraps the root, a page screenshot shows the canvas's last-drawn bitmap,
|
||||
// not the live DOM (see the Lim 6 boundary-screenshot note).
|
||||
@@ -2495,6 +2495,29 @@ function recordCaptureWarnings(session: CaptureSession, warnings: readonly Captu
|
||||
}
|
||||
}
|
||||
|
||||
/** Report background urls that timed out decoding; the shown url drops credentials and query. */
|
||||
export function recordBackgroundDecodeTimeouts(
|
||||
session: CaptureSession,
|
||||
urls: readonly string[] | undefined,
|
||||
): void {
|
||||
// Mocked pages answer evaluate() with undefined, so tolerate a missing list.
|
||||
if (!urls?.length) return;
|
||||
const sources = [...new Set(urls.map(sanitizeDiagnosticUrl))].sort();
|
||||
for (const source of sources) {
|
||||
appendBrowserDiagnostic(
|
||||
session,
|
||||
`[FrameCapture] background image still decoding after ${BACKGROUND_DECODE_TIMEOUT_MS}ms, captured without it: ${source}`,
|
||||
);
|
||||
}
|
||||
recordCaptureWarnings(session, [
|
||||
{
|
||||
code: "media_readiness_timeout",
|
||||
message: `image media did not finish decoding within ${BACKGROUND_DECODE_TIMEOUT_MS}ms`,
|
||||
details: { mediaType: "image", sources, timeoutMs: BACKGROUND_DECODE_TIMEOUT_MS },
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function recordSubTimelineWarning(session: CaptureSession, timeoutMs: number): void {
|
||||
const outcome = session.subTimelineWaitOutcome;
|
||||
if (outcome === "ready" || !outcome) return;
|
||||
@@ -3382,12 +3405,7 @@ async function prepareFrameForCapture(
|
||||
|
||||
session.captureStep = "decoding CSS background images";
|
||||
// Mocked pages answer evaluate() with undefined, so tolerate a missing list.
|
||||
for (const url of (await decodeDynamicCssBackgroundImages(page)) ?? []) {
|
||||
appendBrowserDiagnostic(
|
||||
session,
|
||||
`[FrameCapture] background image still decoding after ${BACKGROUND_DECODE_TIMEOUT_MS}ms, captured without it: ${url}`,
|
||||
);
|
||||
}
|
||||
recordBackgroundDecodeTimeouts(session, await decodeDynamicCssBackgroundImages(page));
|
||||
|
||||
const seekMs = Date.now() - seekStart;
|
||||
|
||||
@@ -5243,13 +5261,14 @@ export async function captureFramesBatchPipelined(
|
||||
const quantized = times.map((t) => quantizeTimeToFrame(t, fps));
|
||||
|
||||
session.captureStep = "capturing the frame";
|
||||
const { encodeResults, failedAt, error } = await produceDrawElementFrameBatch(
|
||||
const { encodeResults, failedAt, error, lateBackgrounds } = await produceDrawElementFrameBatch(
|
||||
page,
|
||||
quantized,
|
||||
options.width,
|
||||
options.height,
|
||||
options.quality ?? 80,
|
||||
);
|
||||
recordBackgroundDecodeTimeouts(session, lateBackgrounds);
|
||||
|
||||
// A later batch failure abandons this prefix. Its asynchronous encodes can
|
||||
// still reject while session cleanup shuts the worker down.
|
||||
|
||||
Reference in New Issue
Block a user