mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(runtime): honor render fps when seeking (#1739)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user