Skip to content

Commit e4e97ed

Browse files
miguel-heygenclaude
authored andcommitted
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>
1 parent a562946 commit e4e97ed

1 file changed

Lines changed: 45 additions & 12 deletions

File tree

packages/engine/src/services/frameCapture.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3347,6 +3347,30 @@ function isNoCachedPaintRecordError(err: unknown): boolean {
33473347
return msg.includes("No cached paint record");
33483348
}
33493349

3350+
/**
3351+
* True for the drawElement `canvas not initialized` error (thrown by
3352+
* drawElementService when the injected capture canvas isn't set up yet —
3353+
* observed at frame 0 on some macOS/Chrome combinations, see #3423). Like the
3354+
* no-cached-paint-record case, this is recoverable per-frame: callers fall
3355+
* back to screenshot capture for the affected frame instead of hard-failing
3356+
* the whole render.
3357+
*/
3358+
function isCanvasNotInitializedError(err: unknown): boolean {
3359+
const msg = err instanceof Error ? err.message : String(err);
3360+
return msg.includes("canvas not initialized");
3361+
}
3362+
3363+
/**
3364+
* Single gate for drawElement failures the fast-capture pipeline knows how to
3365+
* recover from by falling back to screenshot capture instead of aborting the
3366+
* render. Both {@link captureFrameCore} and {@link captureFrameToBufferPipelined}
3367+
* consult this so a newly-recognized recoverable error only needs to be taught
3368+
* here once.
3369+
*/
3370+
function isRecoverableDrawElementError(err: unknown): boolean {
3371+
return isNoCachedPaintRecordError(err) || isCanvasNotInitializedError(err);
3372+
}
3373+
33503374
async function captureFrameCore(
33513375
session: CaptureSession,
33523376
frameIndex: number,
@@ -3418,7 +3442,7 @@ async function captureFrameCore(
34183442
// stale), so the "fallback" REPLACES good frames with damaged ones (validated:
34193443
// 35e8fa9f 462→0 damaged frames, 4001da8e 11→0, when this is off). The two real
34203444
// boundary failure modes are now caught reactively below — the throw case by
3421-
// isNoCachedPaintRecordError, the silent-solid-black case by the small-frame
3445+
// isRecoverableDrawElementError, the silent-solid-black case by the small-frame
34223446
// blank-guard (a solid frame is a tiny JPEG) — without touching frames drawElement
34233447
// handles. Force the old behavior with HF_FAST_CAPTURE_BOUNDARY_SS=true. The worker
34243448
// path keeps proactive boundary-SS (it has no blank-guard); see
@@ -3476,13 +3500,18 @@ async function captureFrameCore(
34763500
} catch (err) {
34773501
// drawElementImage throws `InvalidStateError: No cached paint record for
34783502
// element` when an element in the subtree has no paint record this frame
3479-
// (display toggled / detached / freshly-shown at a clip-cut boundary). This
3480-
// is a per-frame condition, not a whole-comp one — fall back to screenshot
3481-
// for THIS frame instead of aborting the render. See fast-capture-limitations.md.
3482-
if (isNoCachedPaintRecordError(err)) {
3503+
// (display toggled / detached / freshly-shown at a clip-cut boundary), and
3504+
// `canvas not initialized` when the injected capture canvas isn't set up yet
3505+
// (observed at frame 0 on some macOS/Chrome combinations, see #3423). Both
3506+
// are per-frame conditions, not whole-comp ones — fall back to screenshot for
3507+
// THIS frame instead of aborting the render. See fast-capture-limitations.md.
3508+
if (isRecoverableDrawElementError(err)) {
34833509
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
3510+
const reason = isCanvasNotInitializedError(err)
3511+
? "drawElement canvas not initialized"
3512+
: "No cached paint record";
34843513
console.log(
3485-
`[engine] fast capture: frame ${frameIndex}No cached paint record; ` +
3514+
`[engine] fast capture: frame ${frameIndex}${reason}; ` +
34863515
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
34873516
);
34883517
screenshotBuffer = await pageScreenshotCapture(page, options);
@@ -3691,14 +3720,18 @@ export async function captureFrameToBufferPipelined(
36913720

36923721
return { encodeResult, captureTimeMs };
36933722
} catch (captureError) {
3694-
// Per-frame `No cached paint record`: fall back to screenshot for THIS frame
3695-
// instead of aborting the render (clip-cut boundary / freshly-shown element).
3696-
// The worker isn't involved for this frame; return a resolved encodeResult so
3697-
// the pipeline loop writes it like any other. See fast-capture-limitations.md.
3698-
if (isNoCachedPaintRecordError(captureError)) {
3723+
// Per-frame `No cached paint record` or `canvas not initialized` (#3423): fall
3724+
// back to screenshot for THIS frame instead of aborting the render (clip-cut
3725+
// boundary / freshly-shown element / capture canvas not yet set up). The worker
3726+
// isn't involved for this frame; return a resolved encodeResult so the pipeline
3727+
// loop writes it like any other. See fast-capture-limitations.md.
3728+
if (isRecoverableDrawElementError(captureError)) {
36993729
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
3730+
const reason = isCanvasNotInitializedError(captureError)
3731+
? "drawElement canvas not initialized"
3732+
: "No cached paint record";
37003733
console.log(
3701-
`[engine] fast capture: frame ${frameIndex}No cached paint record; ` +
3734+
`[engine] fast capture: frame ${frameIndex}${reason}; ` +
37023735
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
37033736
);
37043737
const buffer = await pageScreenshotCapture(page, options);

0 commit comments

Comments
 (0)