mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(cli): accept ffmpeg-style rational fps (NTSC, PAL, slow-mo)
Replaces the rigid `--fps 24|30|60` whitelist with a numeric range and
adds support for ffmpeg-style fractional framerates so NTSC stays exact
end-to-end.
- `--fps 30` keeps working (integer fps)
- `--fps 30000/1001` now means exact NTSC 29.97 (not the lossy decimal)
- `--fps 24000/1001`, `--fps 60000/1001`, `--fps 25/50/120/240` all work
- Decimals like `--fps 29.97` are rejected with a friendly error pointing
the user at the rational form, since `29.97` and `30000/1001` round
to different framerates inside ffmpeg
Carries an `Fps = { num: number; den: number }` rational end-to-end:
RenderConfig, EncoderOptions, StreamingEncoderOptions, CaptureOptions,
DockerRenderOptions, Studio API request body, regression-harness
meta.json. The `-r` and `-framerate` ffmpeg args emit the rational form
verbatim (`30000/1001`) so no decimal round-trip happens at the encoder
boundary. Frame-interval math uses `1000 * den / num` ms (33.366… for
NTSC, 33.333… for integer 30).
Helpers live in @hyperframes/core:
- `parseFps(input: string | number): FpsParseResult` — discriminated
parser used by both the CLI and the Studio API route
- `fpsToFfmpegArg(fps: Fps): string` — emits "30" or "30000/1001"
- `fpsToNumber(fps: Fps): number` — for arithmetic (telemetry, frame
count, frame-index → time)
Studio API wire format accepts polymorphic `fps: number | string`:
- number → integer fps (`30`)
- string → rational (`"30000/1001"`)
Decimals are rejected; matches the same rule as the CLI.
Existing meta.json fixtures with integer `"fps": 30` continue to load
unchanged — the regression-harness validator now normalizes both number
and string inputs through `parseFps`.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
import { type Browser, type Page, type Viewport, type ConsoleMessage } from "puppeteer-core";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { quantizeTimeToFrame } from "@hyperframes/core";
|
||||
import { quantizeTimeToFrame, fpsToNumber } from "@hyperframes/core";
|
||||
|
||||
// ── Extracted modules ───────────────────────────────────────────────────────
|
||||
import {
|
||||
@@ -228,7 +228,10 @@ export async function createCaptureSession(
|
||||
},
|
||||
captureMode,
|
||||
beginFrameTimeTicks: 0,
|
||||
beginFrameIntervalMs: 1000 / Math.max(1, options.fps),
|
||||
// Frame interval in ms: 1000 * den / num. For 30/1 → 33.333…, for
|
||||
// 30000/1001 (NTSC) → 33.366…. JavaScript number precision is fine at
|
||||
// these scales — no rounding required.
|
||||
beginFrameIntervalMs: (1000 * options.fps.den) / Math.max(1, options.fps.num),
|
||||
beginFrameHasDamageCount: 0,
|
||||
beginFrameNoDamageCount: 0,
|
||||
config,
|
||||
@@ -591,7 +594,7 @@ async function prepareFrameForCapture(
|
||||
throw new Error("[FrameCapture] Session not initialized");
|
||||
}
|
||||
|
||||
const quantizedTime = quantizeTimeToFrame(time, options.fps);
|
||||
const quantizedTime = quantizeTimeToFrame(time, fpsToNumber(options.fps));
|
||||
|
||||
const seekStart = Date.now();
|
||||
// Seek via the __hf protocol. The page's seek() implementation handles
|
||||
|
||||
Reference in New Issue
Block a user