import { defineCommand } from "citty"; import type { Example } from "./_examples.js"; import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync } from "node:fs"; export const examples: Example[] = [ ["Render to MP4", "hyperframes render --output output.mp4"], ["Render a specific composition", "hyperframes render -c compositions/intro.html -o intro.mp4"], [ "Upsample any composition to 4K (supersamples via Chrome DPR)", "hyperframes render --resolution 4k --output 4k.mp4", ], ["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"], ["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"], [ "Render PNG sequence (RGBA frames for AE/Nuke/Fusion)", "hyperframes render --format png-sequence --output frames/", ], ["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"], ["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"], ["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"], ["Opt out of browser GPU render", "hyperframes render --no-browser-gpu --output cpu.mp4"], ["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"], [ "Override composition variables (parametrized render)", 'hyperframes render --variables \'{"title":"Q4 Report","theme":"dark"}\' --output q4.mp4', ], [ "Variables from a JSON file", "hyperframes render --variables-file ./vars.json --output out.mp4", ], ]; import { cpus, freemem, tmpdir } from "node:os"; import { resolve, dirname, join, basename } from "node:path"; import { execFileSync, spawn } from "node:child_process"; import { resolveProject } from "../utils/project.js"; import { lintProject, shouldBlockRender } from "../utils/lintProject.js"; import { formatLintFindings } from "../utils/lintFormat.js"; import { loadProducer } from "../utils/producer.js"; import { c } from "../ui/colors.js"; import { formatBytes, formatDuration, errorBox } from "../ui/format.js"; import { renderProgress } from "../ui/progress.js"; import { trackRenderComplete, trackRenderError } from "../telemetry/events.js"; import { bytesToMb } from "../telemetry/system.js"; import { VERSION } from "../version.js"; import { isDevMode } from "../utils/env.js"; import { buildDockerRunArgs } from "../utils/dockerRunArgs.js"; import { ensureDOMParser } from "../utils/dom.js"; import type { RenderJob } from "@hyperframes/producer"; import { extractCompositionMetadata, validateVariables, formatVariableValidationIssue, type VariableValidationIssue, type CanvasResolution, } from "@hyperframes/core"; const VALID_RENDER_RESOLUTIONS: readonly CanvasResolution[] = [ "landscape", "portrait", "landscape-4k", "portrait-4k", ] as const; const RENDER_RESOLUTION_ALIASES: Record = { "1080p": "landscape", hd: "landscape", "1080p-portrait": "portrait", "portrait-1080p": "portrait", "4k": "landscape-4k", uhd: "landscape-4k", "4k-portrait": "portrait-4k", }; function normalizeRenderResolutionFlag(input: string | undefined): CanvasResolution | undefined { if (!input) return undefined; const lowered = input.toLowerCase(); if ((VALID_RENDER_RESOLUTIONS as readonly string[]).includes(lowered)) { return lowered as CanvasResolution; } return RENDER_RESOLUTION_ALIASES[lowered]; } const VALID_FPS = new Set([24, 30, 60]); const VALID_QUALITY = new Set(["draft", "standard", "high"]); const VALID_FORMAT = new Set(["mp4", "webm", "mov", "png-sequence"]); // `png-sequence` writes a directory of frames rather than a single muxed file, // so its "extension" is empty — the auto-output path becomes a directory name. const FORMAT_EXT: Record = { mp4: ".mp4", webm: ".webm", mov: ".mov", "png-sequence": "", }; const CPU_CORE_COUNT = cpus().length; export default defineCommand({ meta: { name: "render", description: "Render a composition to MP4, WebM, MOV, or a PNG sequence", }, args: { dir: { type: "positional", description: "Project directory", required: false, }, composition: { type: "string", alias: "c", description: "Render a specific composition file instead of index.html (e.g. compositions/intro.html). " + "Sub-compositions using