fix(producer): keep the requested CRF in the distributed cfr re-encode (#5557)

The opt-in `cfr` step in assemble() re-encoded every mp4 at libx264
medium / CRF 18 regardless of how the chunks were encoded, so a render
asked for at CRF 12 (or quality "high", or a target bitrate) came out at
CRF 18 whenever cfr was on. It now reads quality / crf / bitrate from
meta/encoder.json and applies the same rate control the chunks used:
crf, else bitrate, else the quality preset's CRF. Default (standard, no
override) is unchanged: medium / 18.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Russo
2026-10-13 20:30:28 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ab8a4da53f
commit bf4cf860fb
2 changed files with 62 additions and 10 deletions
@@ -35,7 +35,7 @@ import {
renderProvenanceArgs,
} from "@hyperframes/engine";
import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
import { assemble } from "./assemble.js";
import { assemble, cfrRateControlArgs } from "./assemble.js";
import type { DistributedFormat } from "./shared.js";
let runRoot: string;
@@ -550,6 +550,50 @@ describe("assemble()", () => {
TIMEOUT_MS,
);
it("cfr re-encode keeps the chunks' rate control", () => {
expect(cfrRateControlArgs({})).toEqual(["-preset", "medium", "-crf", "18"]);
expect(cfrRateControlArgs({ quality: "high" })).toEqual(["-preset", "slow", "-crf", "15"]);
expect(cfrRateControlArgs({ quality: "standard", crf: 12 })).toEqual([
"-preset",
"medium",
"-crf",
"12",
]);
expect(cfrRateControlArgs({ bitrate: "10M" })).toEqual(["-preset", "medium", "-b:v", "10M"]);
expect(cfrRateControlArgs({ crf: 12, bitrate: "10M" })).toEqual([
"-preset",
"medium",
"-crf",
"12",
]);
});
it(
"cfr:true output size follows the locked crf",
async () => {
if (!hasFfmpeg) {
console.warn("[assemble.test] skipping cfr crf test — ffmpeg not available on this host");
return;
}
const sizeAt = async (crf: number) => {
const chunks: ChunkSliceJson[] = [{ index: 0, startFrame: 0, endFrame: 10 }];
const planDir = buildPlanDir("mp4", chunks, 10, false);
writeFileSync(
join(planDir, "meta", "encoder.json"),
JSON.stringify({ encoder: "libx264-software", quality: "standard", crf }),
"utf-8",
);
const chunkPath = join(planDir, "chunk-0.mp4");
makeMp4Chunk(chunkPath, 10);
const outputPath = join(planDir, `output-crf${crf}.mp4`);
await assemble(planDir, [chunkPath], null, outputPath, { cfr: true });
return statSync(outputPath).size;
};
expect(await sizeAt(10)).toBeGreaterThan(await sizeAt(40));
},
TIMEOUT_MS,
);
it(
"cfr:true rejects non-mp4 formats with a clear error",
async () => {
@@ -39,6 +39,7 @@ import {
appendRenderProvenanceArgs,
applyFaststart,
describeFfmpegFailure,
getEncoderPreset,
MIXED_AUDIO_FILENAME,
muxVideoWithAudio,
resolveConfig,
@@ -49,7 +50,7 @@ import { defaultLogger, type ProducerLogger } from "../../logger.js";
import { formatExportFrameName } from "../../utils/paths.js";
import { padOrTrimAudioToVideoFrameCount } from "../render/audioPadTrim.js";
import { encoderFailureError } from "../render/encoderInterruption.js";
import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
import type { ChunkSliceJson, LockedRenderConfig } from "../render/stages/freezePlan.js";
import { DISTRIBUTED_RENDER_CAPABILITIES, readPlanProtocolV1 } from "./planProtocol.js";
import { validatePlanV2MaterializedTarget } from "./planV2.js";
import type { DistributedFormat } from "./shared.js";
@@ -81,6 +82,16 @@ interface PlanJsonForAssemble {
};
}
type CfrRateControlSource = Partial<Pick<LockedRenderConfig, "quality" | "crf" | "bitrate">>;
export function cfrRateControlArgs(locked: CfrRateControlSource): string[] {
const preset = getEncoderPreset(locked.quality ?? "standard", "mp4");
if (locked.crf == null && locked.bitrate) {
return ["-preset", preset.preset, "-b:v", locked.bitrate];
}
return ["-preset", preset.preset, "-crf", String(locked.crf ?? preset.quality)];
}
/**
* Assemble the chunk outputs into a single deliverable.
*
@@ -112,7 +123,7 @@ export async function assemble(
* duration lands on the requested `frameCount / fps` to ms
* precision, with no PTS-derived drift). Trade-off: ~2-5x the
* stitch time for a 60s 1080p clip plus second-generation H.264
* quality loss (negligible at `-crf 18` but non-zero). Default
* quality loss (at the chunks' own CRF or bitrate, non-zero). Default
* `false` preserves the existing `-c copy` behavior. mp4 only
* (libx264); webm / mov pass through unchanged because their
* stream-copy paths don't exhibit the same avg-frame-rate drift.
@@ -266,9 +277,9 @@ export async function assemble(
if (!existsSync(encoderJsonPath)) {
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
}
const encoderJson = JSON.parse(readFileSync(encoderJsonPath, "utf-8")) as {
encoder?: string;
};
const encoderJson = JSON.parse(
readFileSync(encoderJsonPath, "utf-8"),
) as CfrRateControlSource & { encoder?: string };
if (encoderJson.encoder === "libx265-software") {
throw new Error(
`[assemble] cfr=true is not yet supported with codec: "h265". The ` +
@@ -282,10 +293,7 @@ export async function assemble(
concatOutputPath,
"-c:v",
"libx264",
"-preset",
"medium",
"-crf",
"18",
...cfrRateControlArgs(encoderJson),
"-pix_fmt",
"yuv420p",
"-fps_mode",