Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/guides/rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, WebM
| `--quality` | draft, standard, high | standard | Encoding quality preset |
| `--crf` | 0–51 | — | Override CRF (lower = higher quality). Cannot combine with `--video-bitrate` |
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate encoding. Cannot combine with `--crf` |
| `--video-frame-format` | auto, jpg, png | auto | Source video frame extraction format. Use `png` for UI recordings, screen captures, and color-sensitive source videos |
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
| `--max-concurrent-renders` | 1-10 | 2 | Max simultaneous renders via the producer server (see [Concurrent Renders](#concurrent-renders) below) |
| `--batch` | path | — | JSON array of variable rows (or `{ "rows": [...] }`), rendering one output per row |
Expand Down Expand Up @@ -168,6 +169,8 @@ GIF output uses a two-pass FFmpeg palette encode (`palettegen` with diff statist

GIF does not carry audio and only has 1-bit transparency. For transparent overlays, use `--format webm`, `--format mov`, or `--format png-sequence` instead.

For UI recordings, screen captures, or other source videos where saturated interface colors matter, pass `--video-frame-format png` to extract source video layers as PNG before browser capture. The default `auto` mode preserves the historical behavior: alpha-capable sources use PNG, opaque sources use JPG.

## GPU Acceleration

Hyperframes has two separate GPU acceleration surfaces:
Expand Down
3 changes: 2 additions & 1 deletion docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--quality` | draft, standard, high | standard | Encoding quality preset (drives CRF/bitrate) |
| `--crf` | 0-51 | — | Override encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate` |
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target video bitrate. Mutually exclusive with `--crf` |
| `--video-frame-format` | auto, jpg, png | auto | Source video frame extraction format. Use `png` for UI recordings, screen captures, and color-sensitive source videos |
| `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
Expand All @@ -678,7 +679,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--strict-variables` | — | off | Fail render if any `--variables` key is undeclared or has a wrong type vs the composition's `data-composition-variables`. Without this flag, mismatches print as warnings and the render continues. |
| `--browser-timeout` | seconds (0.001–86400) | 60 | Puppeteer page-navigation timeout for the entry HTML. Increase when heavy compositions (many videos, fonts, or asset requests) cannot reach `domcontentloaded` within the default 60 s. The flag takes **seconds**; the env fallback `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS` takes **milliseconds**. This controls `page.goto` only — very heavy compositions may also need `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS` and/or `PRODUCER_PLAYER_READY_TIMEOUT_MS` bumped (post-navigation `window.__hf` readiness has its own 45 s budget). |

CRF and target bitrate default to the `--quality` preset. Use `--crf` or `--video-bitrate` for fine-grained overrides; `RenderConfig.crf` and `RenderConfig.videoBitrate` accept the same overrides programmatically.
CRF and target bitrate default to the `--quality` preset. Use `--crf` or `--video-bitrate` for fine-grained overrides; `RenderConfig.crf` and `RenderConfig.videoBitrate` accept the same overrides programmatically. Use `--video-frame-format png` when source videos are UI recordings, screen captures, or other color-sensitive clips that should avoid JPEG frame extraction.

#### Parametrized renders

Expand Down
9 changes: 9 additions & 0 deletions packages/aws-lambda/src/sdk/validateConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("validateDistributedRenderConfig", () => {
maxParallelChunks: 16,
runtimeCap: "lambda",
hdrMode: "force-sdr",
videoFrameFormat: "png",
};
expect(validateDistributedRenderConfig(cfg)).toBe(cfg);
});
Expand Down Expand Up @@ -92,6 +93,14 @@ describe("validateDistributedRenderConfig", () => {
{ ...VALID, bitrate: "fast" } satisfies SerializableDistributedRenderConfig,
"config.bitrate",
],
[
"unsupported videoFrameFormat",
{
...VALID,
videoFrameFormat: "webp",
} as unknown as SerializableDistributedRenderConfig,
"config.videoFrameFormat",
],
[
"non-positive chunkSize",
{ ...VALID, chunkSize: 0 } satisfies SerializableDistributedRenderConfig,
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,21 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.gifLoop).toBe(3);
});

it("forwards videoFrameFormat to createRenderJob", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
videoFrameFormat: "png",
});

expect(producerState.createdJobs[0]?.videoFrameFormat).toBe("png");
});

it("omits variables from createRenderJob when not provided", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArg
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { runEnvironmentChecks } from "../browser/preflight.js";
import type { ProducerLogger, RenderJob } from "@hyperframes/producer";
import { isVideoFrameFormat, type VideoFrameFormat } from "@hyperframes/engine";
import {
normalizeResolutionFlag,
parseFps,
Expand Down Expand Up @@ -181,6 +182,14 @@ export default defineCommand({
type: "string",
description: "GIF loop count, 0 = infinite. Range: 0-65535. Only used with --format gif.",
},
"video-frame-format": {
type: "string",
description:
"Source video frame extraction format: auto, jpg, png (default: auto). " +
"Use png for UI recordings, screen captures, and color-sensitive source videos; " +
"alpha-capable sources always extract as PNG.",
default: "auto",
},
workers: {
type: "string",
alias: "w",
Expand Down Expand Up @@ -375,6 +384,16 @@ export default defineCommand({
}
const gifLoop = gifLoopParse.value ?? (format === "gif" ? 0 : undefined);

const videoFrameFormatRaw = args["video-frame-format"] ?? "auto";
if (!isVideoFrameFormat(videoFrameFormatRaw)) {
errorBox(
"Invalid video-frame-format",
`Got "${videoFrameFormatRaw}". Must be auto, jpg, or png.`,
);
process.exit(1);
}
const videoFrameFormat = videoFrameFormatRaw;

// ── Validate resolution ────────────────────────────────────────────────
let outputResolution: CanvasResolution | undefined;
if (args.resolution !== undefined) {
Expand Down Expand Up @@ -768,6 +787,7 @@ export default defineCommand({
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
videoFrameFormat,
quiet,
variables,
entryFile,
Expand All @@ -790,6 +810,7 @@ export default defineCommand({
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
videoFrameFormat,
quiet,
browserPath,
variables,
Expand Down Expand Up @@ -825,6 +846,7 @@ interface RenderOptions {
hdrMode: "auto" | "force-hdr" | "force-sdr";
crf?: number;
videoBitrate?: string;
videoFrameFormat?: VideoFrameFormat;
quiet: boolean;
browserPath?: string;
variables?: Record<string, unknown>;
Expand Down Expand Up @@ -1067,6 +1089,7 @@ async function renderDocker(
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
quiet: options.quiet,
variables: options.variables,
entryFile: options.entryFile,
Expand Down Expand Up @@ -1176,6 +1199,7 @@ export async function renderLocal(
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
variables: options.variables,
entryFile: options.entryFile,
outputResolution: options.outputResolution,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Requires: Docker installed and running.
- `-w, --workers` — Parallel workers 1-8 (default: auto)
- `--crf` — Override encoder CRF (mutually exclusive with `--video-bitrate`)
- `--video-bitrate` — Target video bitrate such as `10M` (mutually exclusive with `--crf`)
- `--video-frame-format` — Source video frame extraction format: `auto`, `jpg`, or `png` (default: `auto`). Use `png` for UI recordings, screen captures, and color-sensitive source videos.
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV)
- `--browser-gpu` / `--no-browser-gpu` — Force host GPU or software (SwiftShader) for Chrome/WebGL capture. Default for local renders is `auto` — probe WebGL availability on first launch and fall back to software if no GPU is reachable. Docker mode always uses software.
- `-o, --output` — Custom output path
Expand All @@ -28,5 +29,6 @@ Requires: Docker installed and running.
- Use `draft` quality for fast previews during development
- Local renders auto-detect GPU on first launch; use `--browser-gpu` to force hardware (errors if no GPU) or `--no-browser-gpu` to force SwiftShader
- Use `--gpu` when a local render also benefits from hardware FFmpeg encoding
- Use `--video-frame-format png` when source videos contain saturated UI colors that should avoid JPEG extraction
- Use `npx hyperframes benchmark` to find optimal settings
- 4 workers is usually the sweet spot for most compositions
24 changes: 24 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ describe("buildDockerRunArgs", () => {
hdrMode: "force-hdr",
crf: 16,
videoBitrate: undefined,
videoFrameFormat: "png",
quiet: true,
entryFile: "compositions/intro.html",
},
Expand All @@ -181,6 +182,8 @@ describe("buildDockerRunArgs", () => {
expect(args).toContain("8");
expect(args).toContain("--crf");
expect(args).toContain("16");
expect(args).toContain("--video-frame-format");
expect(args).toContain("png");
expect(args).toContain("--quiet");
expect(args).toContain("--gpu");
expect(args).toContain("--no-browser-gpu");
Expand Down Expand Up @@ -224,6 +227,27 @@ describe("buildDockerRunArgs", () => {
expect(args).not.toContain("--crf");
});

it("forwards --video-frame-format to the container when set to png", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, videoFrameFormat: "png" },
});
expect(args).toContain("--video-frame-format");
expect(args).toContain("png");
});

it("omits --video-frame-format when it is auto or unset", () => {
expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).not.toContain(
"--video-frame-format",
);
expect(
buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, videoFrameFormat: "auto" },
}),
).not.toContain("--video-frame-format");
});

it("forwards --variables JSON to the container when set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface DockerRenderOptions {
hdrMode: "auto" | "force-hdr" | "force-sdr";
crf?: number;
videoBitrate?: string;
videoFrameFormat?: "auto" | "jpg" | "png";
quiet: boolean;
variables?: Record<string, unknown>;
entryFile?: string;
Expand Down Expand Up @@ -121,6 +122,9 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.workers != null ? ["--workers", String(options.workers)] : []),
...(options.crf != null ? ["--crf", String(options.crf)] : []),
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
...(options.videoFrameFormat && options.videoFrameFormat !== "auto"
? ["--video-frame-format", options.videoFrameFormat]
: []),
...(options.quiet ? ["--quiet"] : []),
...(options.gpu ? ["--gpu"] : []),
...(options.browserGpu ? [] : ["--no-browser-gpu"]),
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export {
type ExtractionOptions,
type ExtractionResult,
type ExtractionPhaseBreakdown,
type VideoFrameFormat,
VIDEO_FRAME_FORMATS,
isVideoFrameFormat,
} from "./services/videoFrameExtractor.js";

export { createVideoFrameInjector } from "./services/videoFrameInjector.js";
Expand Down
Loading
Loading