mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
co-authored by
Claude Opus 5.5
parent
ab8a4da53f
commit
bf4cf860fb
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user