feat(render): auto-detect HDR from media probes, add --sdr flag

Replace the --hdr opt-in model with automatic detection. When no flags
are passed, the renderer probes all video/image sources and enables HDR
output if any HDR color space is detected. Existing --hdr flag becomes
a force override. New --sdr flag forces SDR output.

Behavior matrix:
  (no flags) + HDR content → HDR output
  (no flags) + SDR content → SDR output
  --hdr → force HDR (defaults to HLG if no HDR sources)
  --sdr → force SDR (skips probing)
  --hdr --sdr → error

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-04-27 22:20:53 -07:00
co-authored by Claude Opus 4.6
parent cc89d0aae2
commit 69fb52196f
5 changed files with 76 additions and 27 deletions
+17 -6
View File
@@ -87,7 +87,12 @@ export default defineCommand({
},
hdr: {
type: "boolean",
description: "Enable HDR: probe sources for PQ/HLG, output H.265 10-bit BT.2020",
description: "Force HDR output even if no HDR sources are detected",
default: false,
},
sdr: {
type: "boolean",
description: "Force SDR output even if HDR sources are detected",
default: false,
},
crf: {
@@ -301,6 +306,12 @@ export default defineCommand({
}
}
// ── Validate HDR/SDR mutual exclusion ────────────────────────────────
if (args.hdr && args.sdr) {
console.error("Error: --hdr and --sdr are mutually exclusive.");
process.exit(1);
}
// ── Render ────────────────────────────────────────────────────────────
if (useDocker) {
await renderDocker(project.dir, outputPath, {
@@ -309,7 +320,7 @@ export default defineCommand({
format,
workers: workerCount,
gpu: useGpu,
hdr: args.hdr ?? false,
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
quiet,
@@ -321,7 +332,7 @@ export default defineCommand({
format,
workers: workerCount,
gpu: useGpu,
hdr: args.hdr ?? false,
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
quiet,
@@ -337,7 +348,7 @@ interface RenderOptions {
format: "mp4" | "webm" | "mov";
workers: number;
gpu: boolean;
hdr: boolean;
hdrMode: "auto" | "force-hdr" | "force-sdr";
crf?: number;
videoBitrate?: string;
quiet: boolean;
@@ -461,7 +472,7 @@ async function renderDocker(
format: options.format,
workers: options.workers,
gpu: options.gpu,
hdr: options.hdr,
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
quiet: options.quiet,
@@ -527,7 +538,7 @@ async function renderLocal(
format: options.format,
workers: options.workers,
useGpu: options.gpu,
hdr: options.hdr,
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
});
+24 -6
View File
@@ -7,7 +7,7 @@ const BASE: DockerRenderOptions = {
format: "mp4",
workers: 4,
gpu: false,
hdr: false,
hdrMode: "auto",
crf: undefined,
videoBitrate: undefined,
quiet: false,
@@ -53,7 +53,14 @@ describe("buildDockerRunArgs", () => {
expect(
buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, gpu: true, hdr: true, crf: 18, videoBitrate: undefined, quiet: true },
options: {
...BASE,
gpu: true,
hdrMode: "force-hdr",
crf: 18,
videoBitrate: undefined,
quiet: true,
},
}),
).toMatchInlineSnapshot(`
[
@@ -92,17 +99,28 @@ describe("buildDockerRunArgs", () => {
// Regression for the original PR feedback: --hdr was silently dropped from
// the docker arg array. Keep this assertion explicit (in addition to the
// snapshot above) so the failure message points directly at the flag.
it("forwards --hdr to the container when hdr is enabled", () => {
it("forwards --hdr to the container when hdrMode is force-hdr", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, hdr: true },
options: { ...BASE, hdrMode: "force-hdr" },
});
expect(args).toContain("--hdr");
expect(args).not.toContain("--sdr");
});
it("omits --hdr when hdr is disabled", () => {
it("forwards --sdr to the container when hdrMode is force-sdr", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, hdrMode: "force-sdr" },
});
expect(args).toContain("--sdr");
expect(args).not.toContain("--hdr");
});
it("omits --hdr and --sdr when hdrMode is auto", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--hdr");
expect(args).not.toContain("--sdr");
});
it("requests host GPU passthrough only when gpu is enabled", () => {
@@ -130,7 +148,7 @@ describe("buildDockerRunArgs", () => {
format: "webm",
workers: 8,
gpu: true,
hdr: true,
hdrMode: "force-hdr",
crf: 16,
videoBitrate: undefined,
quiet: true,
+3 -2
View File
@@ -24,7 +24,7 @@ export interface DockerRenderOptions {
format: "mp4" | "webm" | "mov";
workers: number;
gpu: boolean;
hdr: boolean;
hdrMode: "auto" | "force-hdr" | "force-sdr";
crf?: number;
videoBitrate?: string;
quiet: boolean;
@@ -60,6 +60,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
...(options.quiet ? ["--quiet"] : []),
...(options.gpu ? ["--gpu"] : []),
...(options.hdr ? ["--hdr"] : []),
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),
...(options.hdrMode === "force-sdr" ? ["--sdr"] : []),
];
}
+1 -1
View File
@@ -600,7 +600,7 @@ async function runTestSuite(
workers: suite.meta.renderConfig.workers,
useGpu: false,
debug: false,
hdr: suite.meta.renderConfig.hdr ?? false,
hdrMode: suite.meta.renderConfig.hdr ? "force-hdr" : "auto",
});
await executeRenderJob(job, tempSrcDir, renderedOutputPath);
@@ -222,8 +222,8 @@ export interface RenderConfig {
crf?: number;
/** Target video bitrate (e.g. "10M"). Mutually exclusive with `crf`. */
videoBitrate?: string;
/** Enable HDR color space probing on video/image sources. */
hdr?: boolean;
/** HDR rendering mode. `auto` probes sources and enables HDR if any HDR content is found. */
hdrMode?: "auto" | "force-hdr" | "force-sdr";
}
export interface RenderPerfSummary {
@@ -1477,7 +1477,7 @@ export async function executeRenderJob(
// overhead on SDR-only compositions.
const nativeHdrVideoIds = new Set<string>();
const videoTransfers = new Map<string, HdrTransfer>();
if (job.config.hdr && composition.videos.length > 0) {
if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
await Promise.all(
composition.videos.map(async (v) => {
let videoPath = v.src;
@@ -1504,7 +1504,7 @@ export async function executeRenderJob(
const imageTransfers = new Map<string, HdrTransfer>();
const hdrImageSrcPaths = new Map<string, string>();
const imageColorSpaces: (VideoColorSpace | null)[] = [];
if (job.config.hdr && composition.images.length > 0) {
if (job.config.hdrMode !== "force-sdr" && composition.images.length > 0) {
const probed = await Promise.all(
composition.images.map(async (img) => {
let imgPath = img.src;
@@ -1573,14 +1573,24 @@ export async function executeRenderJob(
// dominant transfer (PQ if any PQ source is present, otherwise HLG).
// Image-only compositions can trigger HDR output without any video.
let effectiveHdr: { transfer: HdrTransfer } | undefined;
if (job.config.hdr) {
{
const hdrMode = job.config.hdrMode ?? "auto";
const videoColorSpaces = (extractionResult?.extracted ?? []).map(
(ext) => ext.metadata.colorSpace,
);
const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
if (allColorSpaces.length > 0) {
const info = analyzeCompositionHdr(allColorSpaces);
if (info.hasHdr && info.dominantTransfer) {
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
if (hdrMode === "force-sdr") {
effectiveHdr = undefined;
} else if (hdrMode === "force-hdr") {
if (info?.hasHdr && info.dominantTransfer) {
effectiveHdr = { transfer: info.dominantTransfer };
} else {
effectiveHdr = { transfer: "hlg" };
}
} else {
if (info?.hasHdr && info.dominantTransfer) {
effectiveHdr = { transfer: info.dominantTransfer };
}
}
@@ -1591,10 +1601,19 @@ export async function executeRenderJob(
);
effectiveHdr = undefined;
}
if (effectiveHdr) {
log.info(
`[Render] HDR source detected — output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`,
);
{
const hdrMode = job.config.hdrMode ?? "auto";
if (effectiveHdr) {
const reason =
hdrMode === "force-hdr" ? "forced by --hdr flag" : "auto-detected from source(s)";
log.info(
`[Render] HDR ${reason} — output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`,
);
} else if (hdrMode === "force-sdr") {
log.info("[Render] SDR forced by --sdr flag");
} else {
log.info("[Render] No HDR sources detected — rendering SDR");
}
}
// ── Stage 3: Audio processing ───────────────────────────────────────