mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +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:
@@ -7,7 +7,7 @@ interface RenderQueueProps {
|
||||
projectId: string;
|
||||
onDelete: (jobId: string) => void;
|
||||
onClearCompleted: () => void;
|
||||
onStartRender: (format: "mp4" | "webm") => void;
|
||||
onStartRender: (format: "mp4" | "webm" | "mov") => void;
|
||||
isRendering: boolean;
|
||||
}
|
||||
|
||||
@@ -15,20 +15,21 @@ function FormatExportButton({
|
||||
onStartRender,
|
||||
isRendering,
|
||||
}: {
|
||||
onStartRender: (format: "mp4" | "webm") => void;
|
||||
onStartRender: (format: "mp4" | "webm" | "mov") => void;
|
||||
isRendering: boolean;
|
||||
}) {
|
||||
const [format, setFormat] = useState<"mp4" | "webm">("mp4");
|
||||
const [format, setFormat] = useState<"mp4" | "webm" | "mov">("mp4");
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<select
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value as "mp4" | "webm")}
|
||||
onChange={(e) => setFormat(e.target.value as "mp4" | "webm" | "mov")}
|
||||
disabled={isRendering}
|
||||
className="h-5 px-1 text-[10px] rounded-l bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
||||
>
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="mov">MOV</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
<button
|
||||
|
||||
@@ -59,7 +59,7 @@ export function useRenderQueue(projectId: string | null) {
|
||||
|
||||
// Start a render and track progress via SSE
|
||||
const startRender = useCallback(
|
||||
async (fps = 30, quality = "standard", format: "mp4" | "webm" = "mp4") => {
|
||||
async (fps = 30, quality = "standard", format: "mp4" | "webm" | "mov" = "mp4") => {
|
||||
if (!projectId) return;
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -96,7 +96,8 @@ export function useRenderQueue(projectId: string | null) {
|
||||
}
|
||||
const { jobId } = await res.json();
|
||||
|
||||
const ext = format === "webm" ? ".webm" : ".mp4";
|
||||
const FORMAT_EXT: Record<string, string> = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
|
||||
const ext = FORMAT_EXT[format] ?? ".mp4";
|
||||
const job: RenderJob = {
|
||||
id: jobId,
|
||||
status: "rendering",
|
||||
|
||||
Reference in New Issue
Block a user