mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(render): add CRF/bitrate controls and improve default quality (#292)
Raise default encoding quality to visually lossless at 1080p (CRF 18) and expose fine-grained encoding controls for power users. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2718de8776
commit
ebc12f7dc9
@@ -7,6 +7,8 @@ export const examples: Example[] = [
|
||||
["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"],
|
||||
["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"],
|
||||
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
|
||||
["Custom CRF for maximum quality", "hyperframes render --crf 15 --output pristine.mp4"],
|
||||
["Target bitrate encoding", "hyperframes render --video-bitrate 10M --output hq.mp4"],
|
||||
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
|
||||
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
|
||||
];
|
||||
@@ -79,6 +81,20 @@ export default defineCommand({
|
||||
description: "Use Docker for deterministic render",
|
||||
default: false,
|
||||
},
|
||||
crf: {
|
||||
type: "string",
|
||||
description:
|
||||
"CRF (Constant Rate Factor) for the video encoder. " +
|
||||
"Lower = higher quality / larger file. Range: 0–51 for H.264. " +
|
||||
"Overrides the quality preset CRF. Cannot be used with --video-bitrate.",
|
||||
},
|
||||
"video-bitrate": {
|
||||
type: "string",
|
||||
description:
|
||||
"Target video bitrate (e.g. '10M', '5000k'). " +
|
||||
"Uses bitrate-based encoding instead of CRF. " +
|
||||
"Cannot be used with --crf.",
|
||||
},
|
||||
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
|
||||
quiet: {
|
||||
type: "boolean",
|
||||
@@ -128,6 +144,36 @@ export default defineCommand({
|
||||
}
|
||||
const format = formatRaw as "mp4" | "webm" | "mov";
|
||||
|
||||
// ── Validate CRF / video-bitrate ────────────────────────────────────
|
||||
let crf: number | undefined;
|
||||
let videoBitrate: string | undefined;
|
||||
if (args.crf != null && args["video-bitrate"] != null) {
|
||||
errorBox(
|
||||
"Conflicting options",
|
||||
"--crf and --video-bitrate cannot be used together. Choose one.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (args.crf != null) {
|
||||
const parsed = parseInt(args.crf, 10);
|
||||
if (isNaN(parsed) || parsed < 0 || parsed > 51) {
|
||||
errorBox("Invalid CRF", `Got "${args.crf}". Must be a number between 0 and 51.`);
|
||||
process.exit(1);
|
||||
}
|
||||
crf = parsed;
|
||||
}
|
||||
if (args["video-bitrate"] != null) {
|
||||
const raw = args["video-bitrate"];
|
||||
if (!/^\d+(\.\d+)?[kKM]$/.test(raw)) {
|
||||
errorBox(
|
||||
"Invalid video bitrate",
|
||||
`Got "${raw}". Must be a number followed by k, K, or M (e.g. "10M", "5000k", "1.5M").`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
videoBitrate = raw;
|
||||
}
|
||||
|
||||
// ── Validate workers ──────────────────────────────────────────────────
|
||||
let workers: number | undefined;
|
||||
if (args.workers != null && args.workers !== "auto") {
|
||||
@@ -185,7 +231,12 @@ export default defineCommand({
|
||||
c.accent(project.name) +
|
||||
c.dim(" \u2192 " + outputPath),
|
||||
);
|
||||
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
|
||||
const encodeLabel = videoBitrate
|
||||
? `bitrate ${videoBitrate}`
|
||||
: crf != null
|
||||
? `crf ${crf}`
|
||||
: quality;
|
||||
console.log(c.dim(" " + fps + "fps \u00B7 " + encodeLabel + " \u00B7 " + workerLabel));
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -266,6 +317,8 @@ export default defineCommand({
|
||||
workers: workerCount,
|
||||
gpu: useGpu,
|
||||
quiet,
|
||||
crf,
|
||||
videoBitrate,
|
||||
});
|
||||
} else {
|
||||
await renderLocal(project.dir, outputPath, {
|
||||
@@ -276,6 +329,8 @@ export default defineCommand({
|
||||
gpu: useGpu,
|
||||
quiet,
|
||||
browserPath,
|
||||
crf,
|
||||
videoBitrate,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -289,6 +344,8 @@ interface RenderOptions {
|
||||
gpu: boolean;
|
||||
quiet: boolean;
|
||||
browserPath?: string;
|
||||
crf?: number;
|
||||
videoBitrate?: string;
|
||||
}
|
||||
|
||||
const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";
|
||||
@@ -423,6 +480,8 @@ async function renderDocker(
|
||||
String(options.workers),
|
||||
...(options.quiet ? ["--quiet"] : []),
|
||||
...(options.gpu ? ["--gpu"] : []),
|
||||
...(options.crf != null ? ["--crf", String(options.crf)] : []),
|
||||
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
|
||||
];
|
||||
|
||||
if (!options.quiet) {
|
||||
@@ -484,6 +543,8 @@ async function renderLocal(
|
||||
format: options.format,
|
||||
workers: options.workers,
|
||||
useGpu: options.gpu,
|
||||
crf: options.crf,
|
||||
videoBitrate: options.videoBitrate,
|
||||
});
|
||||
|
||||
const onProgress = options.quiet
|
||||
|
||||
Reference in New Issue
Block a user