mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
* fix(engine): tune VP9 cpu-used across render paths * fix: address VP9 review feedback
146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
/**
|
|
* Build the argument array for `docker run` that invokes the Hyperframes
|
|
* renderer inside a container.
|
|
*
|
|
* Pure function with no I/O so it can be snapshot-tested. Any new render
|
|
* flag added to the CLI must also be threaded through here AND covered by
|
|
* a test in `dockerRunArgs.test.ts` — that combination is what catches
|
|
* silent-drop regressions like the one that lost `--hdr` historically.
|
|
*/
|
|
import { fpsToFfmpegArg, type Fps } from "@hyperframes/core";
|
|
|
|
export interface DockerRunArgsInput {
|
|
imageTag: string;
|
|
/** Absolute host path to the project directory (mounted read-only at /project). */
|
|
projectDir: string;
|
|
/** Absolute host path to the output directory (mounted read-write at /output). */
|
|
outputDir: string;
|
|
/** Filename within `outputDir` (joined to /output inside the container). */
|
|
outputFilename: string;
|
|
/**
|
|
* Docker `--platform` value (`linux/amd64` or `linux/arm64`). When omitted,
|
|
* resolves to the host architecture via `resolveDockerPlatform()`. Pinning
|
|
* to `linux/amd64` on an arm64 host (the legacy default) forces qemu
|
|
* emulation of chrome-headless-shell, which segfaults or stalls on Apple
|
|
* Silicon — see issue #1193. Native `linux/arm64` falls back to the
|
|
* system chromium baked into the image at the cost of byte-for-byte
|
|
* parity with amd64 renders.
|
|
*/
|
|
platform?: string;
|
|
options: DockerRenderOptions;
|
|
}
|
|
|
|
export interface DockerRenderOptions {
|
|
/**
|
|
* Frame rate as an exact rational; see `Fps` in @hyperframes/core. The
|
|
* docker-run arg builder serializes this back to a `--fps` string
|
|
* (`"30"` or `"30000/1001"`) which the in-container CLI re-parses with
|
|
* `parseFps`, so the rational survives the host → container hop.
|
|
*/
|
|
fps: Fps;
|
|
quality: "draft" | "standard" | "high";
|
|
format: "mp4" | "webm" | "mov" | "png-sequence" | "gif";
|
|
gifLoop?: number;
|
|
workers?: number;
|
|
gpu: boolean;
|
|
browserGpu: boolean;
|
|
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
|
crf?: number;
|
|
vp9CpuUsed?: number;
|
|
videoBitrate?: string;
|
|
videoFrameFormat?: "auto" | "jpg" | "png";
|
|
quiet: boolean;
|
|
variables?: Record<string, unknown>;
|
|
entryFile?: string;
|
|
/** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */
|
|
outputResolution?: string;
|
|
pageSideCompositing?: boolean;
|
|
/**
|
|
* Puppeteer page-navigation timeout, in milliseconds. Forwarded to the
|
|
* in-container CLI as `--browser-timeout <seconds>` (the CLI takes
|
|
* seconds; the engine takes ms — kept consistent with the host-side
|
|
* `--browser-timeout` flag).
|
|
*/
|
|
pageNavigationTimeoutMs?: number;
|
|
}
|
|
|
|
/**
|
|
* Maps Node's `process.arch` to a Docker `--platform` string. We only emit
|
|
* the two architectures the renderer actively supports — arm64 hosts (Apple
|
|
* Silicon, Graviton, Ampere) and everything else (treated as amd64).
|
|
*
|
|
* Honors `HYPERFRAMES_DOCKER_PLATFORM` as an escape hatch (typed loosely so
|
|
* the override can target future platforms without a CLI release):
|
|
*
|
|
* - Apple Silicon users running an x64 Node binary under Rosetta (where
|
|
* `process.arch === "x64"` despite the host being arm64) can set it to
|
|
* `linux/arm64` to avoid re-triggering issue #1193.
|
|
* - Maintainers regenerating amd64 golden baselines on an arm64 host can set
|
|
* it to `linux/amd64` to keep the byte-for-byte guarantee.
|
|
* - Users on remote daemons (`DOCKER_HOST=ssh://amd64-server`) can force the
|
|
* actual daemon arch instead of relying on local `process.arch`.
|
|
*/
|
|
export function resolveDockerPlatform(
|
|
arch: string = process.arch,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): string {
|
|
const override = env.HYPERFRAMES_DOCKER_PLATFORM;
|
|
if (override && override.trim() !== "") return override.trim();
|
|
return arch === "arm64" ? "linux/arm64" : "linux/amd64";
|
|
}
|
|
|
|
// Pure argv builder — the cognitive count tracks the number of optional CLI
|
|
// flags it forwards, not branching depth. Each conditional spread is one
|
|
// option = O(1) to read. Inherited from main (#1196 added platform handling);
|
|
// this PR added one more conditional for --browser-timeout.
|
|
// fallow-ignore-next-line complexity
|
|
export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
|
const { imageTag, projectDir, outputDir, outputFilename, options } = input;
|
|
const platform = input.platform ?? resolveDockerPlatform();
|
|
return [
|
|
"run",
|
|
"--rm",
|
|
"--platform",
|
|
platform,
|
|
"--shm-size=2g",
|
|
// GPU encoding requires host GPU passthrough.
|
|
...(options.gpu ? ["--gpus", "all"] : []),
|
|
"-v",
|
|
`${projectDir}:/project:ro`,
|
|
"-v",
|
|
`${outputDir}:/output`,
|
|
imageTag,
|
|
"/project",
|
|
"--output",
|
|
`/output/${outputFilename}`,
|
|
"--fps",
|
|
fpsToFfmpegArg(options.fps),
|
|
"--quality",
|
|
options.quality,
|
|
"--format",
|
|
options.format,
|
|
...(options.gifLoop != null ? ["--gif-loop", String(options.gifLoop)] : []),
|
|
...(options.workers != null ? ["--workers", String(options.workers)] : []),
|
|
...(options.crf != null ? ["--crf", String(options.crf)] : []),
|
|
...(options.vp9CpuUsed != null ? ["--vp9-cpu-used", String(options.vp9CpuUsed)] : []),
|
|
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
|
|
...(options.videoFrameFormat && options.videoFrameFormat !== "auto"
|
|
? ["--video-frame-format", options.videoFrameFormat]
|
|
: []),
|
|
...(options.quiet ? ["--quiet"] : []),
|
|
...(options.gpu ? ["--gpu"] : []),
|
|
...(options.browserGpu ? [] : ["--no-browser-gpu"]),
|
|
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),
|
|
...(options.hdrMode === "force-sdr" ? ["--sdr"] : []),
|
|
...(options.variables && Object.keys(options.variables).length > 0
|
|
? ["--variables", JSON.stringify(options.variables)]
|
|
: []),
|
|
...(options.entryFile ? ["--composition", options.entryFile] : []),
|
|
...(options.outputResolution ? ["--resolution", options.outputResolution] : []),
|
|
...(options.pageSideCompositing === false ? ["--no-page-side-compositing"] : []),
|
|
...(options.pageNavigationTimeoutMs != null
|
|
? ["--browser-timeout", String(options.pageNavigationTimeoutMs / 1000)]
|
|
: []),
|
|
];
|
|
}
|