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
@@ -500,6 +500,9 @@ export async function renderChunk(
compiledDir,
port: 0,
preHeadScripts: [buildVirtualTimeShim({ seedRandomFromFrame: true })],
// These dimensions are frozen by the controller from the render job, so
// chunk runtime seek quantization stays on the same fps grid as capture.
fps: { num: plan.dimensions.fpsNum, den: plan.dimensions.fpsDen },
});
const captureOptions: CaptureOptions = {
@@ -315,6 +315,77 @@ describe("parseRangeHeader", () => {
});
describe("createFileServer", () => {
async function expectInjectedRenderFps(
fps: Parameters<typeof createFileServer>[0]["fps"],
expected: {
value: string;
source: "render-options" | "default";
fallbackReason?: "missing" | "invalid";
},
): Promise<void> {
const projectDir = mkdtempSync(join(tmpdir(), "hf-file-server-render-fps-"));
try {
writeEmptyIndex(projectDir);
const server = await createFileServer({
projectDir,
preHeadScripts: [],
headScripts: [],
...(fps ? { fps } : {}),
});
try {
const response = await fetch(`${server.url}/index.html`);
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain("window.__HF_EXPORT_RENDER_SEEK_CONFIG");
expect(html).toContain(`var __renderFps = ${expected.value}`);
expect(html).toContain(`var __renderFpsSource = "${expected.source}"`);
if (expected.fallbackReason) {
expect(html).toContain(`var __renderFpsFallbackReason = "${expected.fallbackReason}"`);
} else {
expect(html).toContain("var __renderFpsFallbackReason = null");
}
expect(html).toContain("fps: __renderFps");
expect(html).toContain("fpsSource: __renderFpsSource");
expect(html).not.toContain("[hyperframes] render fps defaulted");
} finally {
server.close();
}
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
}
it("injects the requested render fps into the page render config", async () => {
await expectInjectedRenderFps({ num: 60, den: 1 }, { value: "60", source: "render-options" });
});
it("injects fractional render fps without rounding", async () => {
await expectInjectedRenderFps(
{ num: 24000, den: 1001 },
{ value: "23.976023976023978", source: "render-options" },
);
});
it("marks missing render fps as an explicit 30fps default", async () => {
await expectInjectedRenderFps(undefined, {
value: "30",
source: "default",
fallbackReason: "missing",
});
});
it("marks invalid render fps as an explicit 30fps default", async () => {
await expectInjectedRenderFps(
{ num: 60, den: 0 },
{
value: "30",
source: "default",
fallbackReason: "invalid",
},
);
});
it("serves asset files through project-root symlinked directories", async () => {
const workspaceDir = mkdtempSync(join(tmpdir(), "hf-file-server-symlink-assets-"));
const adsDir = join(workspaceDir, "Ads");
+27 -2
View File
@@ -16,6 +16,7 @@ import { readFile } from "node:fs/promises";
import { Readable } from "node:stream";
import { join, extname, resolve, sep } from "node:path";
import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
import { fpsToNumber, type Fps } from "@hyperframes/core";
import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js";
import { getHfEarlyStub } from "../generated/hf-early-stub-inline.js";
import { defaultLogger, type ProducerLogger } from "../logger.js";
@@ -390,7 +391,22 @@ const RENDER_SEEK_OFFSET_FRACTION = Math.max(
Math.min(0.95, Number(process.env.PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION || 0.5)),
);
const RENDER_MODE_SCRIPT = `(function() {
function resolveRenderFpsConfig(fps: Fps | undefined): {
value: number;
source: "render-options" | "default";
fallbackReason?: "missing" | "invalid";
} {
if (!fps) return { value: 30, source: "default", fallbackReason: "missing" };
const value = fpsToNumber(fps);
if (!Number.isFinite(value) || value <= 0) {
return { value: 30, source: "default", fallbackReason: "invalid" };
}
return { value, source: "render-options" };
}
function buildRenderModeScript(fps: Fps | undefined): string {
const renderFps = resolveRenderFpsConfig(fps);
return `(function() {
var __realSetTimeout =
window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetTimeout === "function"
? window.__HF_VIRTUAL_TIME__.originalSetTimeout
@@ -399,11 +415,17 @@ const RENDER_MODE_SCRIPT = `(function() {
var __seekDiagnostics = ${RENDER_SEEK_DIAGNOSTICS ? "true" : "false"};
var __seekStep = ${RENDER_SEEK_STEP};
var __seekOffsetFraction = ${RENDER_SEEK_OFFSET_FRACTION};
var __renderFps = ${renderFps.value};
var __renderFpsSource = ${JSON.stringify(renderFps.source)};
var __renderFpsFallbackReason = ${JSON.stringify(renderFps.fallbackReason ?? null)};
window.__HF_EXPORT_RENDER_SEEK_CONFIG = {
mode: __seekMode,
diagnostics: __seekDiagnostics,
step: __seekStep,
offsetFraction: __seekOffsetFraction,
fps: __renderFps,
fpsSource: __renderFpsSource,
fpsFallbackReason: __renderFpsFallbackReason || undefined,
owner: "runtime",
};
function installMediaFallbackPlayer() {
@@ -499,6 +521,7 @@ const RENDER_MODE_SCRIPT = `(function() {
}
waitForPlayer();
})();`;
}
/**
* Early stub: ensures `window.__hf` exists *before* any user `<script>` in
@@ -640,6 +663,8 @@ export interface FileServerOptions {
headScripts?: string[];
/** Scripts injected before </body> of index.html. Default: render mode extension. */
bodyScripts?: string[];
/** Actual render fps so page-side runtime quantization matches the output container. */
fps?: Fps;
/** Strip embedded runtime scripts from HTML before injection. Default: true. */
stripEmbeddedRuntime?: boolean;
}
@@ -687,7 +712,7 @@ export function createFileServer(options: FileServerOptions): Promise<FileServer
const preHeadScripts = [HF_EARLY_STUB, ...(options.preHeadScripts ?? [])];
// Default scripts: Hyperframe runtime in <head>, render mode in </body>
const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
const bodyScripts = options.bodyScripts ?? [buildRenderModeScript(options.fps), HF_BRIDGE_SCRIPT];
const app = new Hono();
@@ -166,6 +166,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
compiledDir: join(workDir, "compiled"),
port: 0,
preHeadScripts: [VIRTUAL_TIME_SHIM],
fps: job.config.fps,
});
assertNotAborted();
@@ -1227,6 +1227,7 @@ export async function executeRenderJob(
compiledDir: join(workDir, "compiled"),
port: 0,
preHeadScripts: [VIRTUAL_TIME_SHIM],
fps: job.config.fps,
});
assertNotAborted();
observability.stageEnd("file_server", fileServerStart);