Skip to content

Commit a7e8674

Browse files
miga-heygenmiguel-heygenclaude
authored
fix(producer): fall back to screenshot capture on drawElement canvas-not-initialized (#3480)
* fix(producer): fall back to screenshot capture on drawElement canvas-not-initialized The fast-capture drawElement path only special-cased the "No cached paint record" error to trigger a per-frame screenshot fallback; every other error (including "drawElement canvas not initialized", seen at frame 0 on some macOS/Chrome combinations) was rethrown, hard-failing the whole render even though the docs promise automatic fallback on incompatible compositions. Extend the existing fallback branch (in both captureFrameCore and captureFrameToBufferPipelined) to also catch canvas-not-initialized errors via a shared isRecoverableDrawElementError predicate, with a diagnostic message identifying which case triggered the fallback. Closes #3423 Co-Authored-By: Miga <noreply@anthropic.com> * fix(producer): address review — tighten error matching, audit batch path, add fallback-ratio guard * fix(engine): add prepareFrameForCapture to batch screenshot fallback loop * fix(engine): split canvas-not-initialized from composition-root-missing errors drawElementService threw the same HF_DE_CANVAS_NOT_INITIALIZED error for both !canvas and !root. Missing composition root (navigated/broken page) was classified recoverable and fell back to pageScreenshotCapture, which captured blank or wrong content silently. Now: - !root → HF_DE_COMPOSITION_ROOT_MISSING (not recoverable, hard fail) - !canvas → HF_DE_CANVAS_NOT_INITIALIZED (recoverable, screenshot fallback) Split applied at all 3 emit sites (serial, pipelined, batch). Co-Authored-By: miga-heygen <miguel.sierra_miga@heygen.com> --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> Co-authored-by: Miga <noreply@anthropic.com>
1 parent cd6a25d commit a7e8674

2 files changed

Lines changed: 226 additions & 34 deletions

File tree

packages/engine/src/services/drawElementService.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@
1616

1717
import type { Page } from "puppeteer-core";
1818

19+
/**
20+
* Discriminant prefix embedded in every "capture canvas isn't set up yet"
21+
* error THIS module throws/returns (as opposed to the native
22+
* `InvalidStateError: No cached paint record for element` DOMException that
23+
* `drawElementImage` itself throws, which we don't control the text of).
24+
*
25+
* These errors cross a `page.evaluate` boundary: Puppeteer reconstructs them
26+
* as a plain `Error` on the Node side (see puppeteer-core's
27+
* `createEvaluationError`), so custom properties/subclasses don't survive —
28+
* only `message` (and `name`, which for a plain `Error` thrown in-page is
29+
* just `"Error"`) make the round trip. A stable, low-cardinality code baked
30+
* into the message is therefore the closest available substitute for a real
31+
* error-code discriminant. frameCapture.ts matches on this exact code
32+
* (substring) rather than on the free-text tail, so classification survives
33+
* the message being re-wrapped (e.g. `produceDrawElementFrameBatch`'s
34+
* "batch produce failed at frame N: <code>: ..." wrapping) and won't
35+
* false-positive on unrelated prose that happens to contain the words
36+
* "canvas" and "initialized".
37+
*
38+
* IMPORTANT: because `page.evaluate` serializes closures via `Function#toString`,
39+
* the three throw/return sites below CANNOT reference this constant directly
40+
* (it wouldn't be in scope inside the browser) — they inline the same literal
41+
* string. Keep all three in sync with this constant if it ever changes.
42+
*/
43+
export const DE_CANVAS_NOT_INITIALIZED_CODE = "HF_DE_CANVAS_NOT_INITIALIZED";
44+
1945
/**
2046
* Resolve which capture mode to use when `useDrawElement` is true.
2147
*
@@ -331,7 +357,12 @@ export async function captureDrawElementFrame(
331357
}) => {
332358
const canvas = document.getElementById("__hf_de_canvas") as HTMLCanvasElement | null;
333359
const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
334-
if (!canvas || !root) throw new Error("drawElement canvas not initialized");
360+
if (!root) {
361+
throw new Error("HF_DE_COMPOSITION_ROOT_MISSING: drawElement composition root not found");
362+
}
363+
if (!canvas) {
364+
throw new Error("HF_DE_CANVAS_NOT_INITIALIZED: drawElement canvas not initialized");
365+
}
335366
const ctx = canvas.getContext("2d");
336367
if (!ctx) throw new Error("drawElement: 2d context unavailable");
337368
// Accelerated canvases (webgl/webgl2/webgpu) never repaint — their paint
@@ -772,7 +803,12 @@ export async function produceDrawElementFrame(
772803
({ w, h, q, sync, fid }: { w: number; h: number; q: number; sync: boolean; fid: number }) => {
773804
const canvas = document.getElementById("__hf_de_canvas") as HTMLCanvasElement | null;
774805
const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
775-
if (!canvas || !root) throw new Error("drawElement canvas not initialized");
806+
if (!root) {
807+
throw new Error("HF_DE_COMPOSITION_ROOT_MISSING: drawElement composition root not found");
808+
}
809+
if (!canvas) {
810+
throw new Error("HF_DE_CANVAS_NOT_INITIALIZED: drawElement canvas not initialized");
811+
}
776812
const ctx = canvas.getContext("2d");
777813
if (!ctx) throw new Error("drawElement: 2d context unavailable");
778814

@@ -999,7 +1035,18 @@ export async function produceDrawElementFrameBatch(
9991035
}): Promise<{ failedAt: number | null; error?: string }> => {
10001036
const canvas = document.getElementById("__hf_de_canvas") as HTMLCanvasElement | null;
10011037
const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
1002-
if (!canvas || !root) return { failedAt: 0, error: "drawElement canvas not initialized" };
1038+
if (!root) {
1039+
return {
1040+
failedAt: 0,
1041+
error: "HF_DE_COMPOSITION_ROOT_MISSING: drawElement composition root not found",
1042+
};
1043+
}
1044+
if (!canvas) {
1045+
return {
1046+
failedAt: 0,
1047+
error: "HF_DE_CANVAS_NOT_INITIALIZED: drawElement canvas not initialized",
1048+
};
1049+
}
10031050
const ctx = canvas.getContext("2d");
10041051
if (!ctx) return { failedAt: 0, error: "drawElement: 2d context unavailable" };
10051052

0 commit comments

Comments
 (0)