fix(runtime): honor render fps when seeking (#1739)

This commit is contained in:
Miguel Ángel
2026-06-26 12:28:41 -04:00
committed by GitHub
parent 88fffb04d1
commit c9e8dd3862
12 changed files with 326 additions and 26 deletions
+94
View File
@@ -109,6 +109,7 @@ describe("initSandboxRuntimeModular", () => {
delete window.__player;
delete window.__playerReady;
delete window.__renderReady;
delete (window as { __HF_EXPORT_RENDER_SEEK_CONFIG?: unknown }).__HF_EXPORT_RENDER_SEEK_CONFIG;
delete window.__hfTimelinesBuilding;
delete (window as { THREE?: unknown }).THREE;
vi.restoreAllMocks();
@@ -146,6 +147,99 @@ describe("initSandboxRuntimeModular", () => {
expect(child.style.visibility).toBe("visible");
});
it("uses export render fps when quantizing renderSeek", () => {
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "1");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const timeline = createMockTimeline(1);
window.__timelines = { main: timeline };
(
window as {
__HF_EXPORT_RENDER_SEEK_CONFIG?: { fps: number; fpsSource: "render-options" };
}
).__HF_EXPORT_RENDER_SEEK_CONFIG = {
fps: 60,
fpsSource: "render-options",
};
initSandboxRuntimeModular();
window.__player?.renderSeek(1 / 60);
expect(timeline.time()).toBeCloseTo(1 / 60, 6);
expect(infoSpy).toHaveBeenCalledWith(
"[hyperframes] render runtime fps",
expect.objectContaining({
canonicalFps: 60,
source: "render-options",
rawFpsSource: "render-options",
rawFps: 60,
}),
);
});
it("surfaces unknown export render fps sources without collapsing them to render-options", () => {
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "1");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
window.__timelines = { main: createMockTimeline(1) };
(
window as {
__HF_EXPORT_RENDER_SEEK_CONFIG?: { fps: number; fpsSource: string };
}
).__HF_EXPORT_RENDER_SEEK_CONFIG = {
fps: 60,
fpsSource: "future-source",
};
initSandboxRuntimeModular();
expect(infoSpy).toHaveBeenCalledWith(
"[hyperframes] render runtime fps",
expect.objectContaining({
canonicalFps: 60,
source: "unknown",
rawFpsSource: "future-source",
}),
);
});
it("keeps the default 30fps renderSeek grid when export render fps is absent", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "1");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const timeline = createMockTimeline(1);
window.__timelines = { main: timeline };
initSandboxRuntimeModular();
// This is the originally broken 60fps render sample under the historical
// 30fps runtime default: floor((1 / 60) * 30) / 30 = 0.
window.__player?.renderSeek(1 / 60);
expect(timeline.time()).toBe(0);
});
it("uses live child timeline duration when a composition host has no authored duration", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
+41
View File
@@ -40,8 +40,49 @@ import { swallow } from "./diagnostics";
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
const AUTHORED_END_ATTR = "data-hf-authored-end";
type ExportRenderFpsResolution = {
fps: number | null;
source: "render-options" | "default" | "unknown";
rawFpsSource: unknown;
rawFps: unknown;
fallbackReason?: "missing" | "invalid";
};
function resolveExportRenderFps(): ExportRenderFpsResolution {
const config = window.__HF_EXPORT_RENDER_SEEK_CONFIG;
const rawFps = config?.fps;
const rawFpsSource = config?.fpsSource;
const fps = Number(rawFps);
if (!config || rawFps == null) {
return { fps: null, source: "default", rawFpsSource, rawFps, fallbackReason: "missing" };
}
if (!Number.isFinite(fps) || fps <= 0) {
return { fps: null, source: "default", rawFpsSource, rawFps, fallbackReason: "invalid" };
}
const source =
rawFpsSource === "render-options" || rawFpsSource === "default" ? rawFpsSource : "unknown";
return {
fps,
source,
rawFpsSource,
rawFps,
fallbackReason: config.fpsFallbackReason,
};
}
export function initSandboxRuntimeModular(): void {
const state = createRuntimeState();
const exportRenderFps = resolveExportRenderFps();
state.canonicalFps = exportRenderFps.fps ?? state.canonicalFps;
if (window.__HF_EXPORT_RENDER_SEEK_CONFIG) {
console.info("[hyperframes] render runtime fps", {
canonicalFps: state.canonicalFps,
source: exportRenderFps.source,
rawFpsSource: exportRenderFps.rawFpsSource,
rawFps: exportRenderFps.rawFps,
fallbackReason: exportRenderFps.fallbackReason,
});
}
let colorGradingRuntime: RuntimeColorGradingApi | null = null;
let runtimeErrorListener: ((event: ErrorEvent) => void) | null = null;
let runtimeUnhandledRejectionListener: ((event: PromiseRejectionEvent) => void) | null = null;
+11
View File
@@ -39,7 +39,18 @@ declare global {
__playerReady?: boolean;
__renderReady?: boolean;
__hfRuntimeTeardown?: (() => void) | null;
__HF_EXPORT_RENDER_SEEK_CONFIG?: {
mode?: string;
diagnostics?: boolean;
step?: number;
offsetFraction?: number;
fps?: number;
fpsSource?: "render-options" | "default";
fpsFallbackReason?: "missing" | "invalid";
owner?: string;
};
__HF_PARITY_MODE?: boolean;
/** Legacy debug-only fps hint. Render-mode runtime fps uses __HF_EXPORT_RENDER_SEEK_CONFIG.fps. */
__HF_FPS?: number;
__HF_MAX_DURATION_SEC?: number;
__hfThreeTime?: number;