From 7871ab178c8c6e9a2979b5026dbb7987c9ba8dae Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Tue, 21 Apr 2026 05:56:25 -0700 Subject: [PATCH 1/2] fix(producer): wire --crf and --video-bitrate CLI overrides into encoders The CLI flags `--crf` and `--video-bitrate` were defined and parsed in `packages/cli/src/commands/render.ts`, validated for mutual exclusivity, and threaded into `RenderConfig.crf`/`RenderConfig.videoBitrate`, but the values were silently dropped at the encoder spawn sites in `renderOrchestrator.ts`. PR #292 originally wired these through with a `baseEncoderOpts` object using `effectiveQuality`/`effectiveBitrate`; PR #268 rewrote the encode paths and reverted to `preset.quality` only. This change re-introduces the override at the three encoder spawn sites: 1. HDR streaming encoder (rgb48le path) 2. SDR streaming encoder (jpeg/png path) 3. Disk-based encode (encodeFramesFromDir / encodeFramesChunkedConcat) At each site, `quality` defaults to `preset.quality` but is overridden by `job.config.crf` when set, and `bitrate` is set from `job.config.videoBitrate`. Mutual exclusivity is enforced upstream in the CLI, so we do not need to re-check it here. Also fixes the contradictory note in `docs/packages/cli.mdx` that claimed CRF/bitrate were now driven only by `--quality`. The flags table now lists `--crf` and `--video-bitrate` consistent with `docs/guides/rendering.mdx`. --- docs/packages/cli.mdx | 4 +++- .../producer/src/services/renderOrchestrator.ts | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) 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..4c405053 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1186,6 +1186,14 @@ 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. + const effectiveQuality = job.config.crf ?? preset.quality; + const effectiveBitrate = job.config.videoBitrate; + job.framesRendered = 0; // ── HDR z-ordered multi-layer compositing ────────────────────────────── @@ -1316,7 +1324,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 +2042,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 +2269,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, From e7bae09cfdbd893e80fe3e9bc6633e8bfb883733 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Tue, 21 Apr 2026 21:13:59 -0700 Subject: [PATCH 2/2] fix(producer): warn and prefer crf over videoBitrate when both are set Programmatic callers can construct RenderConfig directly and bypass the CLI's mutual-exclusivity guard for --crf vs --video-bitrate. Add an orchestrator-level check that logs a warning and explicitly nulls out videoBitrate when crf is also set, so the encoder gets unambiguous inputs and downstream users aren't confused by a quietly-different bitrate than they passed in. Addresses non-blocking review feedback on PR #372. --- .../producer/src/services/renderOrchestrator.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 4c405053..c372539a 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1191,8 +1191,20 @@ export async function executeRenderJob( // 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.videoBitrate; + const effectiveBitrate = job.config.crf != null ? undefined : job.config.videoBitrate; job.framesRendered = 0;