fix(render): reject unsupported ProRes rate controls (#3702)

This commit is contained in:
Miguel Ángel
2026-09-08 18:09:19 +00:00
committed by GitHub
parent e1ff7e6f53
commit 2f7b7a9529
5 changed files with 49 additions and 6 deletions
+3
View File
@@ -95,6 +95,9 @@ browser already supports the source or you are diagnosing the proxy itself.
## Choose quality and frame rate
The default `standard` quality is the right choice for most finished work.
MOV is an alpha-preserving editing intermediate and always uses the fixed
ProRes 4444 profile. `--crf` and `--video-bitrate` do not map to that profile
and are rejected for MOV; choose MP4 or WebM when you need those controls.
```bash
# Faster review version
+3 -3
View File
@@ -857,7 +857,7 @@ The flags that come up most:
| `--composition, -c` | `index.html` | Render a different composition file. Sub-compositions using `<template>` wrappers must be referenced from `index.html` via `data-composition-src`. |
| `--format` | `mp4` | `mp4`, `webm`, `mov`, `gif`, or `png-sequence`. WebM and MOV carry transparency; `png-sequence` writes RGBA frames to a directory. |
| `--fps, -f` | root `data-fps`, otherwise 30 | 1–240, or an ffmpeg rational like `30000/1001` for 29.97 |
| `--quality, -q` | `standard` | `draft`, `standard`, or `high`. Drives CRF and bitrate. |
| `--quality, -q` | `standard` | `draft`, `standard`, or `high`. Drives MP4/WebM encoder settings; MOV always uses the fixed alpha-preserving ProRes 4444 profile. |
| `--resolution` | the composition's size | Supersample to a preset via Chrome's `deviceScaleFactor`. Aspect ratio must match and the scale must be a whole multiple. Not with `--hdr`. See [4K rendering](/guides/4k-rendering). |
| `--docker` | off | Render inside Docker for [deterministic output](/concepts/determinism) |
| `--variables` | — | JSON object merged over the composition's `data-composition-variables` defaults |
@@ -869,8 +869,8 @@ Quality and file size:
| Flag | Default | Description |
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
| `--crf` | from `--quality`| Override encoder CRF, 0–51. Lower is better quality. Not with `--video-bitrate`. |
| `--video-bitrate` | from `--quality`| Target bitrate, e.g. `10M` or `5000k`. Not with `--crf`. |
| `--crf` | from `--quality`| Override MP4/WebM encoder CRF, 0–51. Lower is better quality. Not with `--video-bitrate`; rejected for MOV. |
| `--video-bitrate` | from `--quality`| Target MP4/WebM bitrate, e.g. `10M` or `5000k`. Not with `--crf`; rejected for MOV. |
| `--vp9-cpu-used` | encoder default | libvpx-vp9 speed/quality trade-off for WebM, -8 to 8. Env: `PRODUCER_VP9_CPU_USED`. |
| `--gif-loop` | `0` | GIF loop count, `0` for forever. Range 0–65535, `--format gif` only. |
| `--video-frame-format` | `auto` | How source video frames are extracted: `auto`, `jpg`, `png`. Use `png` for UI recordings, screen captures, and other colour-sensitive footage. |
+6 -3
View File
@@ -142,7 +142,8 @@ export default defineCommand({
quality: {
type: "string",
alias: "q",
description: "Quality: draft, standard, high",
description:
"Quality: draft, standard, high. MOV always uses the fixed alpha-preserving ProRes 4444 profile.",
default: "standard",
},
skill: {
@@ -195,11 +196,13 @@ export default defineCommand({
},
crf: {
type: "string",
description: "Override encoder CRF. Mutually exclusive with --video-bitrate.",
description:
"Override MP4/WebM encoder CRF. Mutually exclusive with --video-bitrate; unsupported for MOV.",
},
"video-bitrate": {
type: "string",
description: "Target video bitrate such as 10M. Mutually exclusive with --crf.",
description:
"Target MP4/WebM video bitrate such as 10M. Mutually exclusive with --crf; unsupported for MOV.",
},
"vp9-cpu-used": {
type: "string",
@@ -89,6 +89,34 @@ describe("createRenderPlan", () => {
expect(() => createRenderPlan({ dir: projectDir, quality: "maximum" })).toThrow(CliUsageError);
});
it.each([
["--crf", { crf: "18" }],
["--video-bitrate", { "video-bitrate": "78M" }],
])("rejects unsupported %s rate control for ProRes MOV", (flag, encoderArgs) => {
expect(() => createRenderPlan({ dir: projectDir, format: "mov", ...encoderArgs })).toThrow(
CliUsageError,
);
expect(vi.mocked(console.error).mock.calls.flat().join("\n")).toContain(flag);
expect(vi.mocked(console.error).mock.calls.flat().join("\n")).toContain(
"fixed alpha-preserving ProRes 4444",
);
});
it("keeps MOV quality tiers on the fixed alpha-preserving profile", () => {
const plan = createRenderPlan({ dir: projectDir, format: "mov", quality: "high" });
expect(plan).toMatchObject({ format: "mov", quality: "high" });
expect(plan.crf).toBeUndefined();
expect(plan.videoBitrate).toBeUndefined();
});
it("keeps MP4 and WebM rate controls available", () => {
expect(createRenderPlan({ dir: projectDir, format: "mp4", crf: "18" }).crf).toBe(18);
expect(
createRenderPlan({ dir: projectDir, format: "webm", "video-bitrate": "10M" }).videoBitrate,
).toBe("10M");
});
it("rejects batch and single-render variables before execution", () => {
expect(() =>
createRenderPlan({ dir: projectDir, batch: "rows.json", variables: '{"name":"Ada"}' }),
+9
View File
@@ -408,6 +408,15 @@ export function createRenderPlan(args: RenderCommandArgs, now = new Date()): Ren
);
failUsage();
}
if (format === "mov" && (crf !== undefined || videoBitrate !== undefined)) {
const flag = crf !== undefined ? "--crf" : "--video-bitrate";
errorBox(
"Unsupported ProRes rate control",
`${flag} does not apply to MOV. MOV uses a fixed alpha-preserving ProRes 4444 profile.`,
`Remove ${flag}, or choose MP4/WebM when you need CRF or target-bitrate control.`,
);
failUsage();
}
const quiet = args.quiet ?? false;
const batchJson = args.json ?? false;