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