Skip to content

Commit 3482d16

Browse files
committed
fix(engine): poll for sub-composition timeline readiness before capture
The renderer now waits for all sub-composition timelines to be registered in window.__timelines before starting frame capture. Previously only window.__hf root readiness was checked, causing blank frames when sub-compositions use async data loading (fetch) or when the headless renderer starts capturing before scripts complete. Adds pollSubCompositionTimelines() to both screenshot and beginFrame render paths, with a diagnostic warning listing which composition IDs are missing if the timeout expires.
1 parent ae1716d commit 3482d16

1 file changed

Lines changed: 39 additions & 0 deletions

File tree

packages/engine/src/services/frameCapture.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,41 @@ async function pollPageExpression(
349349
return Boolean(await page.evaluate(expression));
350350
}
351351

352+
async function pollSubCompositionTimelines(
353+
page: Page,
354+
timeoutMs: number,
355+
intervalMs: number = 150,
356+
): Promise<void> {
357+
const expression = `(function() {
358+
var hosts = document.querySelectorAll("[data-composition-id]");
359+
if (hosts.length <= 1) return true;
360+
var timelines = window.__timelines || {};
361+
for (var i = 0; i < hosts.length; i++) {
362+
var id = hosts[i].getAttribute("data-composition-id");
363+
if (!id) continue;
364+
if (!timelines[id]) return false;
365+
}
366+
return true;
367+
})()`;
368+
const ready = await pollPageExpression(page, expression, timeoutMs, intervalMs);
369+
if (!ready) {
370+
const missing = await page.evaluate(`(function() {
371+
var hosts = document.querySelectorAll("[data-composition-id]");
372+
var timelines = window.__timelines || {};
373+
var m = [];
374+
for (var i = 0; i < hosts.length; i++) {
375+
var id = hosts[i].getAttribute("data-composition-id");
376+
if (id && !timelines[id]) m.push(id);
377+
}
378+
return m.join(", ");
379+
})()`);
380+
console.warn(
381+
`[FrameCapture] Sub-composition timelines not registered after ${timeoutMs}ms: ${missing}. ` +
382+
`Compositions that load data asynchronously (e.g. fetch) must register window.__timelines[id] after setup completes.`,
383+
);
384+
}
385+
}
386+
352387
async function pollVideosReady(
353388
page: Page,
354389
skipIds: readonly string[],
@@ -499,6 +534,8 @@ export async function initializeSession(session: CaptureSession): Promise<void>
499534
);
500535
}
501536

537+
await pollSubCompositionTimelines(page, pageReadyTimeout);
538+
502539
await applyVideoMetadataHints(page, session.options.videoMetadataHints);
503540

504541
// Wait for all video elements to have decoded their CURRENT frame, not
@@ -615,6 +652,8 @@ export async function initializeSession(session: CaptureSession): Promise<void>
615652
);
616653
}
617654

655+
await pollSubCompositionTimelines(page, pageReadyTimeout);
656+
618657
await applyVideoMetadataHints(page, session.options.videoMetadataHints);
619658

620659
// Same readyState contract as the screenshot path above (>= 2 / HAVE_CURRENT_DATA).

0 commit comments

Comments
 (0)