Skip to content
Open
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 @@ -121,6 +121,7 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
| `--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) |
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV) |
Expand Down Expand Up @@ -152,6 +153,8 @@ npx hyperframes render --video-bitrate 10M --output controlled.mp4

**Tip**: The default `standard` preset (CRF 18) is visually lossless at 1080p — most people cannot distinguish it from the source. Use `--quality draft` for faster iteration, or `--quality high` / `--crf 10` when file size is no concern.

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 @@ -612,6 +612,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 @@ -624,7 +625,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--variables-file` | path | — | Path to a JSON file with variable overrides (alternative to `--variables`) |
| `--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. |

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
11 changes: 11 additions & 0 deletions packages/aws-lambda/src/sdk/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const ALLOWED_FORMATS = [
] as const satisfies readonly DistributedFormat[];
const ALLOWED_CODECS = ["h264", "h265"] as const;
const ALLOWED_QUALITIES = ["draft", "standard", "high"] as const;
const ALLOWED_VIDEO_FRAME_FORMATS = ["auto", "jpg", "png"] as const;
const ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "none"] as const;
const ALLOWED_HDR_MODES = ["auto", "force-sdr"] as const;

Expand Down Expand Up @@ -120,6 +121,16 @@ export function validateDistributedRenderConfig(
);
}

if (
config.videoFrameFormat !== undefined &&
!ALLOWED_VIDEO_FRAME_FORMATS.includes(config.videoFrameFormat)
) {
throw new InvalidConfigError(
"config.videoFrameFormat",
`must be one of ${ALLOWED_VIDEO_FRAME_FORMATS.join(", ")}; got ${String(config.videoFrameFormat)}`,
);
}

if (config.chunkSize !== undefined) {
if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
throw new InvalidConfigError(
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 @@ -164,6 +164,21 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.format).toBe("png-sequence");
});

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
30 changes: 30 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ import {
} from "@hyperframes/core";

const VALID_QUALITY = new Set(["draft", "standard", "high"]);
const VIDEO_FRAME_FORMATS = ["auto", "jpg", "png"] as const;
type VideoFrameFormat = (typeof VIDEO_FRAME_FORMATS)[number];
const VALID_VIDEO_FRAME_FORMAT = new Set<string>(VIDEO_FRAME_FORMATS);

function isVideoFrameFormat(value: string): value is VideoFrameFormat {
return VALID_VIDEO_FRAME_FORMAT.has(value);
}

/**
* Map a {@link FpsParseResult} failure reason to a human-friendly
Expand Down Expand Up @@ -148,6 +155,14 @@ export default defineCommand({
"to a directory for AE/Nuke/Fusion ingest)",
default: "mp4",
},
"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 @@ -267,6 +282,16 @@ export default defineCommand({
}
const format = formatRaw as "mp4" | "webm" | "mov" | "png-sequence";

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 @@ -530,6 +555,7 @@ export default defineCommand({
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
videoFrameFormat,
quiet,
variables,
entryFile,
Expand All @@ -548,6 +574,7 @@ export default defineCommand({
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
videoFrameFormat,
quiet,
browserPath,
variables,
Expand All @@ -574,6 +601,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 @@ -734,6 +762,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 @@ -808,6 +837,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 @@ -160,6 +160,7 @@ describe("buildDockerRunArgs", () => {
hdrMode: "force-hdr",
crf: 16,
videoBitrate: undefined,
videoFrameFormat: "png",
quiet: true,
entryFile: "compositions/intro.html",
},
Expand All @@ -173,6 +174,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 @@ -202,6 +205,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 @@ -36,6 +36,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 @@ -71,6 +72,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
1 change: 1 addition & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export {
type ExtractionOptions,
type ExtractionResult,
type ExtractionPhaseBreakdown,
type VideoFrameFormat,
} from "./services/videoFrameExtractor.js";

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