feat(producer): add codec knob to DistributedRenderConfig

This commit is contained in:
James
2026-05-15 01:57:08 +00:00
committed by James Russo
parent d1e5ac2939
commit eccea88daf
3 changed files with 111 additions and 13 deletions
@@ -209,3 +209,62 @@ describe("plan() — golden planDir + planHash determinism", () => {
TIMEOUT_MS,
);
});
describe("plan() — codec knob", () => {
const TIMEOUT_MS = 30_000;
it(
"defaults `codec` to h264 (libx264-software) for mp4",
async () => {
const planDir = join(runRoot, "plan-codec-default");
mkdirSync(planDir, { recursive: true });
await plan(projectDir, { fps: 30, width: 320, height: 240, format: "mp4" }, planDir);
const encoder = JSON.parse(
readFileSync(join(planDir, "meta", "encoder.json"), "utf-8"),
) as Record<string, unknown>;
expect(encoder.encoder).toBe("libx264-software");
expect(encoder.pixelFormat).toBe("yuv420p");
},
TIMEOUT_MS,
);
it(
'maps `codec: "h265"` to libx265-software for mp4',
async () => {
const planDir = join(runRoot, "plan-codec-h265");
mkdirSync(planDir, { recursive: true });
await plan(
projectDir,
{ fps: 30, width: 320, height: 240, format: "mp4", codec: "h265" },
planDir,
);
const encoder = JSON.parse(
readFileSync(join(planDir, "meta", "encoder.json"), "utf-8"),
) as Record<string, unknown>;
expect(encoder.encoder).toBe("libx265-software");
// SDR 8-bit yuv420p, same as h264. Distributed mode is SDR-only —
// anyone reading this and tempted to bump to 10-bit, that's HDR
// territory and lives in v1.5.
expect(encoder.pixelFormat).toBe("yuv420p");
},
TIMEOUT_MS,
);
it("rejects `codec` with format other than mp4", async () => {
const planDir = join(runRoot, "plan-codec-bad-format");
mkdirSync(planDir, { recursive: true });
let caught: unknown;
try {
await plan(
projectDir,
// @ts-expect-error — runtime check is the test's purpose.
{ fps: 30, width: 320, height: 240, format: "mov", codec: "h265" },
planDir,
);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(Error);
expect((caught as Error).message).toMatch(/codec.*only valid for format="mp4"/);
});
});
@@ -80,6 +80,17 @@ export interface DistributedRenderConfig {
* supports both.
*/
format: "mp4" | "mov" | "png-sequence";
/**
* Codec selection for `format: "mp4"`. `"h264"` (the default) → libx264 +
* yuv420p; `"h265"` → libx265 + yuv420p with closed-GOP keyint params
* (`min-keyint=N:scenecut=0:open-gop=0:repeat-headers=1`) so chunked
* concat-copy round-trips losslessly the same way h264 does. Ignored for
* `format: "mov"` (always ProRes 4444) and `format: "png-sequence"`
* (no encoder). Passing `codec` with a non-mp4 format throws at plan
* time so caller errors surface immediately rather than producing a
* silently-wrong planDir.
*/
codec?: "h264" | "h265";
quality?: "draft" | "standard" | "high";
/** Constant-rate-factor override; mutually exclusive with `bitrate`. */
crf?: number;
@@ -425,7 +436,7 @@ function buildLockedRenderConfig(input: {
runtimeEnv: Record<string, string>;
}): LockedRenderConfig {
const { config, forceScreenshot, deviceScaleFactor, ffmpegVersion } = input;
const { encoder, pixelFormat, preset } = FORMAT_ENCODER_TABLE[config.format];
const { encoder, pixelFormat, preset } = resolveEncoderTriple(config);
return {
captureMode: forceScreenshot ? "screenshot" : "beginframe",
forceScreenshot,
@@ -455,18 +466,39 @@ function buildLockedRenderConfig(input: {
}
/**
* Per-format encoder + pixel-format + preset triple. Distributed mode is
* SDR-only: H.264 8-bit for mp4, ProRes 4444 for mov, raw RGBA for
* png-sequence.
* Resolve the encoder + pixel-format + preset triple for a distributed
* render. Distributed mode is SDR-only: H.264 or H.265 8-bit for mp4,
* ProRes 4444 for mov, raw RGBA for png-sequence.
*
* `config.codec` is consulted only when `config.format === "mp4"`. Passing
* `codec` with a non-mp4 format throws at plan time — surfaces the
* caller error immediately rather than producing a silently-wrong planDir
* whose chunk worker would override the codec choice.
*/
const FORMAT_ENCODER_TABLE: Record<
DistributedRenderConfig["format"],
{ encoder: LockedRenderConfig["encoder"]; pixelFormat: string; preset: string }
> = {
mp4: { encoder: "libx264-software", pixelFormat: "yuv420p", preset: "medium" },
mov: { encoder: "prores-software", pixelFormat: "yuva444p10le", preset: "4444" },
"png-sequence": { encoder: "png-sequence", pixelFormat: "rgba", preset: "lossless" },
};
function resolveEncoderTriple(config: DistributedRenderConfig): {
encoder: LockedRenderConfig["encoder"];
pixelFormat: string;
preset: string;
} {
if (config.format === "mp4") {
const codec = config.codec ?? "h264";
if (codec === "h265") {
return { encoder: "libx265-software", pixelFormat: "yuv420p", preset: "medium" };
}
return { encoder: "libx264-software", pixelFormat: "yuv420p", preset: "medium" };
}
if (config.codec !== undefined) {
throw new Error(
`[plan] DistributedRenderConfig.codec is only valid for format="mp4"; received ` +
`codec=${JSON.stringify(config.codec)} with format=${JSON.stringify(config.format)}. ` +
`Omit codec for non-mp4 formats — mov is always ProRes 4444 and png-sequence has no encoder.`,
);
}
if (config.format === "mov") {
return { encoder: "prores-software", pixelFormat: "yuva444p10le", preset: "4444" };
}
return { encoder: "png-sequence", pixelFormat: "rgba", preset: "lossless" };
}
/**
* Activity A of the distributed render pipeline. Produces a self-contained
@@ -552,7 +552,14 @@ export async function renderChunk(
const presetFormat: "mp4" | "mov" | "webm" = isPngSequence
? "mp4"
: (plan.dimensions.format as "mp4" | "mov");
const preset = getEncoderPreset(job.config.quality, presetFormat, undefined);
const basePreset = getEncoderPreset(job.config.quality, presetFormat, undefined);
// Override the preset's codec from the planDir's locked encoder so
// h265 mp4 chunks call libx265 instead of getEncoderPreset's default
// h264. `getEncoderPreset` only returns h265 in HDR paths today;
// distributed mode is SDR-only, so the override here is the
// canonical way for chunks to honor `DistributedRenderConfig.codec`.
const preset: typeof basePreset =
encoder.encoder === "libx265-software" ? { ...basePreset, codec: "h265" } : basePreset;
const effectiveQuality = encoder.crf ?? preset.quality;
const effectiveBitrate = encoder.crf != null ? undefined : encoder.bitrate;
// For non-pngseq, encodeStage writes to `outputPath` when `isPngSequence`