mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(producer): auto-fallback screenshot capture for raf and iframes (#331)
* fix(core): drive adapter seeks when composition has no GSAP timeline renderSeek returned early when deps.getTimeline() was null, skipping the onDeterministicSeek call that drives all frame adapters (CSS, WAAPI, Lottie, Three.js). That meant compositions using any non-GSAP animation primitive froze on their initial frame during capture. Now we still quantize the seek time and fire onDeterministicSeek even without a timeline, so each adapter gets a chance to advance. GSAP compositions are unaffected — timeline-driven seek still takes the same path it did before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(producer): auto-fallback screenshot capture for raf and iframes Co-Authored-By: Codex <codex@openai.com> * test(producer): add render compatibility regression fixtures Co-Authored-By: Codex <codex@openai.com> * fix(core): scrub CSS animations via WAAPI currentTime Co-Authored-By: Codex <codex@openai.com> * test(producer): cover css keyframe renders Co-Authored-By: Codex <codex@openai.com> * fix(producer): propagate virtual time into iframe documents Co-Authored-By: Codex <codex@openai.com> * test(producer): refresh iframe docker golden Co-Authored-By: Codex <codex@openai.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
Codex
parent
59aa2c9ec9
commit
ad11de698c
@@ -163,6 +163,21 @@ export function isFontResourceError(type: string, text: string, locationUrl: str
|
||||
);
|
||||
}
|
||||
|
||||
async function pollPageExpression(
|
||||
page: Page,
|
||||
expression: string,
|
||||
timeoutMs: number,
|
||||
intervalMs: number = 100,
|
||||
): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const ready = Boolean(await page.evaluate(expression));
|
||||
if (ready) return true;
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
return Boolean(await page.evaluate(expression));
|
||||
}
|
||||
|
||||
export async function initializeSession(session: CaptureSession): Promise<void> {
|
||||
const { page, serverUrl } = session;
|
||||
|
||||
@@ -213,17 +228,29 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
|
||||
const pageReadyTimeout =
|
||||
session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
|
||||
await page.waitForFunction(
|
||||
const pageReady = await pollPageExpression(
|
||||
page,
|
||||
`!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
|
||||
{ timeout: pageReadyTimeout },
|
||||
pageReadyTimeout,
|
||||
);
|
||||
if (!pageReady) {
|
||||
throw new Error(
|
||||
`[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for all video elements to have loaded metadata (dimensions + duration)
|
||||
// Without this, frame 0 captures videos at their 300x150 default size
|
||||
await page.waitForFunction(
|
||||
const videosReady = await pollPageExpression(
|
||||
page,
|
||||
`document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
|
||||
{ timeout: pageReadyTimeout },
|
||||
pageReadyTimeout,
|
||||
);
|
||||
if (!videosReady) {
|
||||
throw new Error(
|
||||
`[FrameCapture] video metadata not ready after ${pageReadyTimeout}ms. Video elements must load metadata before capture starts.`,
|
||||
);
|
||||
}
|
||||
|
||||
await page.evaluate(`document.fonts?.ready`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user