mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(core,cli): defer __renderReady until root timeline is bound
The runtime set __renderReady at the same time as __playerReady, before the root timeline was bound. Consumers waiting for __renderReady (the render-safe signal) could observe a player with no captured timeline, making renderSeek a no-op. Root cause: init.ts set both flags together, but timeline binding happens later — synchronously via bindRootTimelineIfAvailable(), via a deferred setTimeout(0) for bundled compositions, or asynchronously via loadExternalCompositions(). Fix in init.ts: - Remove __renderReady from the __playerReady assignment - Set it after bindRootTimelineIfAvailable() when timeline is found - Set it in the setTimeout(0) deferred path - Set it in the external compositions .finally() path Fix in snapshot.ts: - Wait for __renderReady (truthful signal) not __timelines - Use renderSeek() with frame quantization, not seek() - Tick the GSAP ticker after seeking - Await document.fonts.ready before capturing Closes #1047
This commit is contained in:
@@ -131,10 +131,13 @@ async function captureSnapshots(
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for runtime to initialize and sub-compositions to load
|
// Wait for the runtime to be fully render-ready: player constructed
|
||||||
|
// AND root timeline bound. __renderReady is only set after the timeline
|
||||||
|
// binding completes (synchronous or deferred), so this guarantees
|
||||||
|
// renderSeek will operate on the real timeline.
|
||||||
const timeoutMs = opts.timeout ?? 5000;
|
const timeoutMs = opts.timeout ?? 5000;
|
||||||
await page
|
await page
|
||||||
.waitForFunction(() => !!(window as any).__timelines || !!(window as any).__playerReady, {
|
.waitForFunction(() => !!(window as any).__renderReady, {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -195,7 +198,10 @@ async function captureSnapshots(
|
|||||||
)
|
)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
// Extra settle time for media, fonts, and animations to initialize
|
// Wait for fonts to finish loading before capturing
|
||||||
|
await page.evaluate(() => document.fonts.ready).catch(() => {});
|
||||||
|
|
||||||
|
// Extra settle time for media and animations to initialize
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
|
||||||
// Font verification — report which fonts loaded vs fell back
|
// Font verification — report which fonts loaded vs fell back
|
||||||
@@ -312,38 +318,30 @@ async function captureSnapshots(
|
|||||||
|
|
||||||
await page.evaluate((t: number) => {
|
await page.evaluate((t: number) => {
|
||||||
const win = window as any;
|
const win = window as any;
|
||||||
if (win.__player?.seek) {
|
const player = win.__player;
|
||||||
win.__player.seek(t);
|
if (player) {
|
||||||
} else {
|
const fps = 30;
|
||||||
const tls = win.__timelines;
|
const safe = Math.max(0, Number(t) || 0);
|
||||||
if (tls) {
|
const frame = Math.floor(safe * fps + 1e-9);
|
||||||
for (const key in tls) {
|
const quantized = frame / fps;
|
||||||
if (tls[key]?.seek) {
|
if (typeof player.renderSeek === "function") {
|
||||||
// Sub-composition timelines run in local time relative to
|
player.renderSeek(quantized);
|
||||||
// their data-start. Seeking them to global time causes beats
|
} else if (typeof player.seek === "function") {
|
||||||
// with exit animations to appear black (global t clamps past
|
player.seek(quantized);
|
||||||
// the exit). Compute local time: global_t - data_start.
|
|
||||||
const host = document.querySelector<HTMLElement>(
|
|
||||||
`[data-composition-id="${key}"]`,
|
|
||||||
);
|
|
||||||
const dataStart = host
|
|
||||||
? parseFloat(host.getAttribute("data-start") ?? "0") || 0
|
|
||||||
: 0;
|
|
||||||
const localTime = Math.max(0, t - dataStart);
|
|
||||||
tls[key].pause();
|
|
||||||
tls[key].seek(localTime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (win.gsap?.ticker?.tick) {
|
||||||
|
win.gsap.ticker.tick();
|
||||||
}
|
}
|
||||||
}, time);
|
}, time);
|
||||||
|
|
||||||
// Wait for rendering to settle after seek
|
// Wait for rendering to settle — match the parity harness pattern
|
||||||
await page.evaluate(
|
await page.evaluate(`new Promise(function(r) {
|
||||||
() =>
|
var settled = false;
|
||||||
new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))),
|
function finish() { if (settled) return; settled = true; r(); }
|
||||||
);
|
window.setTimeout(finish, 100);
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
requestAnimationFrame(function() { requestAnimationFrame(finish); });
|
||||||
|
})`);
|
||||||
|
|
||||||
// ─── Inject real video frames over any active <video data-start> ───
|
// ─── Inject real video frames over any active <video data-start> ───
|
||||||
// Without this, Chrome-headless renders them blank/first-frame because
|
// Without this, Chrome-headless renders them blank/first-frame because
|
||||||
|
|||||||
@@ -1463,6 +1463,7 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
externalCompositionsReady = true;
|
externalCompositionsReady = true;
|
||||||
bindRootTimelineIfAvailable();
|
bindRootTimelineIfAvailable();
|
||||||
|
(window as Window & { __renderReady?: boolean }).__renderReady = true;
|
||||||
runAdapters("discover", state.currentTime);
|
runAdapters("discover", state.currentTime);
|
||||||
bindMediaMetadataListeners();
|
bindMediaMetadataListeners();
|
||||||
installAssetFailureDiagnostics();
|
installAssetFailureDiagnostics();
|
||||||
@@ -1544,7 +1545,6 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
|
|
||||||
window.__player = createPlayerApiCompat(player);
|
window.__player = createPlayerApiCompat(player);
|
||||||
(window as Window & { __playerReady?: boolean }).__playerReady = true;
|
(window as Window & { __playerReady?: boolean }).__playerReady = true;
|
||||||
(window as Window & { __renderReady?: boolean }).__renderReady = true;
|
|
||||||
|
|
||||||
// Wire analytics event emission through the bridge
|
// Wire analytics event emission through the bridge
|
||||||
initRuntimeAnalytics(postRuntimeMessage as (payload: unknown) => void);
|
initRuntimeAnalytics(postRuntimeMessage as (payload: unknown) => void);
|
||||||
@@ -1634,6 +1634,10 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
player._timeline = state.capturedTimeline;
|
player._timeline = state.capturedTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.capturedTimeline) {
|
||||||
|
(window as Window & { __renderReady?: boolean }).__renderReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
// When the bundler inlines compositions, data-composition-src is removed so
|
// When the bundler inlines compositions, data-composition-src is removed so
|
||||||
// loadExternalCompositions() is skipped. But inline scripts registering child
|
// loadExternalCompositions() is skipped. But inline scripts registering child
|
||||||
// timelines in __timelines haven't executed yet (they run in the browser's next
|
// timelines in __timelines haven't executed yet (they run in the browser's next
|
||||||
@@ -1646,6 +1650,7 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
}
|
}
|
||||||
// Re-run adapters to discover new elements
|
// Re-run adapters to discover new elements
|
||||||
runAdapters("discover", state.currentTime);
|
runAdapters("discover", state.currentTime);
|
||||||
|
(window as Window & { __renderReady?: boolean }).__renderReady = true;
|
||||||
postTimeline();
|
postTimeline();
|
||||||
postState(true);
|
postState(true);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user