mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* fix(cli): default render fps to the composition's data-fps
hyperframes render hard-coded fps to 30 when --fps was omitted, ignoring a
data-fps declared on the composition root — so a composition authored at
data-fps="24" silently rendered at 30fps unless the user knew to pass --fps 24.
The runtime already honors data-fps; the CLI now matches it.
Precedence: explicit --fps > composition root data-fps > 30. New pure
readCompositionFps() extracts the root data-fps via linkedom (mirrors the
runtime's root resolution: [data-composition-id][data-root=true], else the
outermost [data-composition-id]); render validates it through parseFps and
falls back to 30 on an absent/invalid value. Unit-tested.
* fix(cli): honor composition data-fps on cloud renders and --composition targets
The local render command read data-fps from project.dir/index.html even when
--composition rendered a different file, and the lambda/cloudrun render paths
ignored data-fps entirely (hardcoded ?? 30). Both are the same silently-wrong-
fps bug on other render entry points:
- render.ts resolves the entry file first, then reads data-fps from the file
actually being rendered (falling back to index.html).
- lambda render/render-batch and cloudrun render/render-batch default fps from
the composition's data-fps, accepted only when it is one of the cloud-allowed
values {24,30,60}, else the existing 30 default. Explicit --fps still wins.
* fix(cli): drop citty fps default so data-fps resolution actually runs
The fps arg had default: "30", so citty set args.fps="30" on omission and
resolveDefaultFpsArg short-circuited (explicitFps never null) — reverting the
command to always-30 and making the whole data-fps feature a no-op (caught in
review). Remove the arg default; the "30" fallback already lives at
parseFps(fpsArg ?? "30"). Adds a regression guard asserting the arg has no
default.
* test(cli): read citty args through a plain record in the fps-default guard
The regression guard accessed cmd.args.fps directly, but citty types args as
Resolvable<ArgsDef> so .fps failed typecheck in CI. Read it through a plain
record cast.
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { parseHTML } from "linkedom";
|
|
|
|
/**
|
|
* Read a composition's declared frame rate from its root element's `data-fps`
|
|
* attribute — the same attribute the runtime honors (core/runtime/init.ts) — so
|
|
* `hyperframes render` can default to it instead of a hard-coded 30 when `--fps`
|
|
* is not passed. Returns the raw attribute string (for the caller to validate
|
|
* via `parseFps`, which supports fractional rates like `30000/1001`), or `null`
|
|
* when no root `data-fps` is present.
|
|
*
|
|
* Root resolution mirrors the runtime: prefer an explicit
|
|
* `[data-composition-id][data-root="true"]`, else the outermost
|
|
* `[data-composition-id]` (one with no `[data-composition-id]` ancestor).
|
|
*/
|
|
export function readCompositionFps(html: string): string | null {
|
|
let doc: Document;
|
|
try {
|
|
doc = parseHTML(html).document as unknown as Document;
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const explicitRoot = doc.querySelector('[data-composition-id][data-root="true"]');
|
|
const root =
|
|
explicitRoot ??
|
|
Array.from(doc.querySelectorAll("[data-composition-id]")).find(
|
|
(el) => !el.parentElement?.closest("[data-composition-id]"),
|
|
) ??
|
|
null;
|
|
|
|
const raw = root?.getAttribute("data-fps")?.trim();
|
|
return raw ? raw : null;
|
|
}
|
|
|
|
/**
|
|
* Cloud render backends (Lambda, Cloud Run) accept only an integer fps
|
|
* from a small fixed allowed set (currently {24, 30, 60}) — unlike local
|
|
* `render`, they can't take an arbitrary/fractional data-fps. Reads
|
|
* `<projectDir>/index.html` and returns its declared data-fps as a number
|
|
* ONLY when it parses to an integer AND is a member of `allowed`;
|
|
* otherwise `null` so the caller keeps its own existing default (30).
|
|
*/
|
|
export function readAllowedCompositionFpsFromDir(
|
|
projectDir: string,
|
|
allowed: readonly number[],
|
|
): number | null {
|
|
let html: string;
|
|
try {
|
|
html = readFileSync(join(projectDir, "index.html"), "utf8");
|
|
} catch {
|
|
return null;
|
|
}
|
|
const raw = readCompositionFps(html);
|
|
if (raw == null) return null;
|
|
const n = Number(raw);
|
|
return Number.isInteger(n) && allowed.includes(n) ? n : null;
|
|
}
|