mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat: add MOV (ProRes 4444) as transparent video output format (#224)
## Summary - Adds `--format mov` to the render CLI for ProRes 4444 transparent video output - ProRes 4444 with alpha is the industry standard for transparent video overlays, supported by CapCut, Final Cut, Premiere, DaVinci, and After Effects - WebM VP9 alpha technically works but is ignored by all major video editors — only browsers decode it - Adds MOV to the studio export dropdown alongside MP4 and WebM ## Transparency format comparison | Format | Codec | Alpha | Video editors | Browsers | File size | | --- | --- | --- | --- | --- | --- | | **MOV** | ProRes 4444 | Yes | CapCut, Final Cut, Premiere, DaVinci, After Effects | No (won't play in browser) | Large (~5-40 MB) | | **WebM** | VP9 | Yes | None (shows black) | Chrome, Firefox | Small (~200 KB) | | **MP4** | H.264 | No | All | All | Small | > **Note:** ProRes MOV files do not play in Chromium browsers — they are an intermediate/editing format, not a delivery format. Use [rotato.app/tools/transparent-video](https://rotato.app/tools/transparent-video) to verify transparency works correctly. ## Changes - **CLI**: Add `mov` to `--format` validation, examples, and output path logic - **Engine**: `getEncoderPreset()` returns ProRes 4444 (`yuva444p10le`) for `mov` format; handle `.mov` in `applyFaststart` and `muxVideoWithAudio`; add `pix_fmt` to streaming encoder ProRes path - **Producer**: Treat `mov` like `webm` for alpha capture (PNG frames, screenshot mode, `forceScreenshot`) - **Studio**: Add MOV option to export format dropdown and render queue hook - **Core**: Add `mov` to studio API types, render route, and mime helpers - **Tests**: Add encoder preset tests for mov format (42 total, all passing) ## Usage ```bash hyperframes render --format mov --output overlay.mov ``` ## Test plan - [x] `pnpm build` passes - [x] `pnpm --filter @hyperframes/engine test` — 42 tests pass (2 new for MOV) - [x] `oxlint` and `oxfmt` clean on all 12 changed files - [x] End-to-end local render produces ProRes 4444 (`yuva444p12le`) with working alpha - [x] Docker render with `--format mov` — ProRes 4444 confirmed via ffprobe - [x] Studio dropdown shows MOV option in built JS - [x] Transparency verified with [rotato.app/tools/transparent-video](https://rotato.app/tools/transparent-video)
This commit is contained in:
@@ -4,6 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync, rmSync } from "node:f
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Render to MP4", "hyperframes render --output output.mp4"],
|
||||
["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"],
|
||||
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
|
||||
@@ -27,7 +28,8 @@ import type { RenderJob } from "@hyperframes/producer";
|
||||
|
||||
const VALID_FPS = new Set([24, 30, 60]);
|
||||
const VALID_QUALITY = new Set(["draft", "standard", "high"]);
|
||||
const VALID_FORMAT = new Set(["mp4", "webm"]);
|
||||
const VALID_FORMAT = new Set(["mp4", "webm", "mov"]);
|
||||
const FORMAT_EXT: Record<string, string> = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
|
||||
|
||||
const CPU_CORE_COUNT = cpus().length;
|
||||
|
||||
@@ -39,7 +41,7 @@ function defaultWorkerCount(): number {
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: "render",
|
||||
description: "Render a composition to MP4 or WebM",
|
||||
description: "Render a composition to MP4, WebM, or MOV",
|
||||
},
|
||||
args: {
|
||||
dir: {
|
||||
@@ -63,7 +65,7 @@ export default defineCommand({
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
description: "Output format: mp4, webm (WebM renders with transparency)",
|
||||
description: "Output format: mp4, webm, mov (MOV/WebM render with transparency)",
|
||||
default: "mp4",
|
||||
},
|
||||
workers: {
|
||||
@@ -117,10 +119,10 @@ export default defineCommand({
|
||||
// ── Validate format ─────────────────────────────────────────────────
|
||||
const formatRaw = args.format ?? "mp4";
|
||||
if (!VALID_FORMAT.has(formatRaw)) {
|
||||
errorBox("Invalid format", `Got "${formatRaw}". Must be mp4 or webm.`);
|
||||
errorBox("Invalid format", `Got "${formatRaw}". Must be mp4, webm, or mov.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const format = formatRaw as "mp4" | "webm";
|
||||
const format = formatRaw as "mp4" | "webm" | "mov";
|
||||
|
||||
// ── Validate workers ──────────────────────────────────────────────────
|
||||
let workers: number | undefined;
|
||||
@@ -135,7 +137,7 @@ export default defineCommand({
|
||||
|
||||
// ── Resolve output path ───────────────────────────────────────────────
|
||||
const rendersDir = resolve("renders");
|
||||
const ext = format === "webm" ? ".webm" : ".mp4";
|
||||
const ext = FORMAT_EXT[format] ?? ".mp4";
|
||||
const now = new Date();
|
||||
const datePart = now.toISOString().slice(0, 10);
|
||||
const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
|
||||
@@ -265,7 +267,7 @@ export default defineCommand({
|
||||
interface RenderOptions {
|
||||
fps: 24 | 30 | 60;
|
||||
quality: "draft" | "standard" | "high";
|
||||
format: "mp4" | "webm";
|
||||
format: "mp4" | "webm" | "mov";
|
||||
workers: number;
|
||||
gpu: boolean;
|
||||
quiet: boolean;
|
||||
|
||||
Reference in New Issue
Block a user