diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index e65317aa..07575b3e 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -496,13 +496,15 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | `--format` | mp4, webm, mov | mp4 | Output format (WebM/MOV render with transparency) | | `--fps` | 24, 30, 60 | 30 | Frames per second | | `--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` | | `--hdr` | — | off | Detect HDR sources and output HDR10 (H.265 10-bit, BT.2020 PQ/HLG). MP4 only. SDR-only compositions are unaffected. See [HDR Rendering](/guides/hdr) | | `--workers` | 1-8 | 4 | Parallel render workers | | `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI) | | `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) | | `--quiet` | — | off | Suppress verbose output | - CRF and target bitrate are now driven by `--quality`. For programmatic renders, `RenderConfig.crf` and `RenderConfig.videoBitrate` still accept overrides. + 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. #### WebM with Transparency diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 79bed8a2..c372539a 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1186,6 +1186,26 @@ export async function executeRenderJob( const encoderHdr = hasHdrContent ? effectiveHdr : undefined; const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr); + // CLI overrides (--crf, --video-bitrate) flow through job.config and must + // win over the preset-derived defaults. The CLI enforces mutual exclusivity + // upstream, but we still resolve them defensively. Without this, the flags + // are silently ignored at the encoder spawn sites below — see PR #268 which + // dropped the prior baseEncoderOpts wiring. + // + // Programmatic callers can construct RenderConfig directly and bypass the + // CLI's mutual-exclusivity guard. If both are set we honor crf (matches the + // CLI semantics where --crf is the explicit override) and warn loudly so + // the caller doesn't get a quietly-different bitrate than they passed in. + if (job.config.crf != null && job.config.videoBitrate) { + log.warn( + `[Render] Both crf=${job.config.crf} and videoBitrate=${job.config.videoBitrate} were set. ` + + `These are mutually exclusive; honoring crf and ignoring videoBitrate. ` + + `Set only one to silence this warning.`, + ); + } + const effectiveQuality = job.config.crf ?? preset.quality; + const effectiveBitrate = job.config.crf != null ? undefined : job.config.videoBitrate; + job.framesRendered = 0; // ── HDR z-ordered multi-layer compositing ────────────────────────────── @@ -1316,7 +1336,8 @@ export async function executeRenderJob( height, codec: preset.codec, preset: preset.preset, - quality: preset.quality, + quality: effectiveQuality, + bitrate: effectiveBitrate, pixelFormat: preset.pixelFormat, hdr: preset.hdr, rawInputFormat: "rgb48le", @@ -2033,7 +2054,8 @@ export async function executeRenderJob( height, codec: preset.codec, preset: preset.preset, - quality: preset.quality, + quality: effectiveQuality, + bitrate: effectiveBitrate, pixelFormat: preset.pixelFormat, useGpu: job.config.useGpu, imageFormat: captureOptions.format || "jpeg", @@ -2259,7 +2281,8 @@ export async function executeRenderJob( height, codec: preset.codec, preset: preset.preset, - quality: preset.quality, + quality: effectiveQuality, + bitrate: effectiveBitrate, pixelFormat: preset.pixelFormat, useGpu: job.config.useGpu, hdr: preset.hdr,