Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ const AUTHORED_END_ATTR = "data-hf-authored-end";

export function initSandboxRuntimeModular(): void {
const state = createRuntimeState();
// Export harness injects the render fps (window.__hfCanonicalFps) so the
// deterministic seek quantizes on the export grid, not the preview default.
{
const injectedFps = (window as unknown as { __hfCanonicalFps?: number }).__hfCanonicalFps;
if (typeof injectedFps === "number" && Number.isFinite(injectedFps) && injectedFps > 0) {
state.canonicalFps = injectedFps;
}
}
let colorGradingRuntime: RuntimeColorGradingApi | null = null;
let runtimeErrorListener: ((event: ErrorEvent) => void) | null = null;
let runtimeUnhandledRejectionListener: ((event: PromiseRejectionEvent) => void) | null = null;
Expand Down Expand Up @@ -1035,6 +1043,19 @@ export function initSandboxRuntimeModular(): void {
if (typeof state.capturedTimeline.timeScale === "function") {
state.capturedTimeline.timeScale(state.playbackRate);
}
// `render -c <scene>` mounts the composition under a synthetic wrapper root
// whose host has no own timeline; only the inner composition registers in
// window.__timelines. The export poll then waits forever for the root's own
// timeline and child seeks never engage. Publish the resolved (composite)
// timeline under the root id so the poll resolves. No-op for full reels,
// whose root already registers its own timeline.
const resolvedRootId = resolveRootCompositionElement()?.getAttribute("data-composition-id");
const timelineRegistry = window.__timelines as
| Record<string, RuntimeTimelineLike | undefined>
| undefined;
if (resolvedRootId && timelineRegistry && !timelineRegistry[resolvedRootId]) {
timelineRegistry[resolvedRootId] = state.capturedTimeline;
}
const boundDuration = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
if (boundDuration <= 0) {
// No resolvable duration (e.g. a set()-only timeline, or one whose
Expand Down
9 changes: 9 additions & 0 deletions packages/engine/src/services/frameCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,15 @@ export async function createCaptureSession(
}
}, variablesJson);
}
// Tell the runtime the export fps so its deterministic seek quantizes on the
// export grid, not its preview default (otherwise single-composition renders,
// which have no authored fps, snap to the 30fps grid). Set before page scripts.
const exportFps = fpsToNumber(options.fps);
if (Number.isFinite(exportFps) && exportFps > 0) {
await page.evaluateOnNewDocument((fps: number) => {
(window as unknown as { __hfCanonicalFps?: number }).__hfCanonicalFps = fps;
}, exportFps);
}
const browserVersion = await browser.version();
const sessionOptions = resolveCaptureSessionOptions(options, browserVersion);
const expectedMajor = config?.expectedChromiumMajor;
Expand Down
Loading