diff --git a/video-renderer/render.js b/video-renderer/render.js index f223645e..ecdb5658 100644 --- a/video-renderer/render.js +++ b/video-renderer/render.js @@ -8,17 +8,18 @@ // render call produces its own temp file the caller cleans up once it has // streamed the response. import { createRenderJob, executeRenderJob } from "@hyperframes/producer"; -import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { cp, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { Readable } from "node:stream"; import * as tar from "tar"; const FPS = 30; -const DIMENSIONS = { - vertical: { width: 1080, height: 1920 }, - square: { width: 1080, height: 1080 }, -}; +// @hyperframes/producer reads each cut's dimensions from the composition HTML +// itself (data-width/data-height on the stage), so the sidecar no longer +// passes width/height — it only picks the quality tier. +const QUALITY = "high"; // Caps the DECOMPRESSED size (a gzip bomb inflates a tiny upload into a huge // tar stream); MAX_UPLOAD_BYTES in server.js only bounds the compressed @@ -125,7 +126,6 @@ export async function renderComposition({ ) { throw new UnknownCompositionError(compositionId, []); } - const htmlPath = path.join(compositionDir, `${orientation}.html`); // ponytail: readdir is the smallest thing that fails if the dir is // missing OR the orientation file is missing — one stat-vs-readdir // branch collapsed into a single listing used for the 400 error. @@ -150,16 +150,31 @@ export async function renderComposition({ `window.__ORIENTATION__ = ${JSON.stringify(orientation)};`; await writeFile(path.join(compositionDir, "props.js"), propsJs); - const { width, height } = DIMENSIONS[orientation]; + // @hyperframes/producer serves the compiled entry at the file-server ROOT + // (/index.html) and every other asset from projectDir at its relative path. + // So projectDir must be the composition dir — theme.css/props.js are direct + // children — and the shared motion/public tree, which theme.css references + // as ../../public/fonts and the root-served entry clamps to /public/fonts, + // must be staged into it or the fonts 404 and fall back to system faces. + const publicSrc = path.join(extractDir, "motion", "public"); + if (existsSync(publicSrc)) { + await cp(publicSrc, path.join(compositionDir, "public"), { + recursive: true, + }); + } + outDir = await mkdtemp(path.join(tmpdir(), "hyperframes-out-")); const outputLocation = path.join(outDir, "render.mp4"); + // createRenderJob carries only render params; @hyperframes/producer@0.7.36 + // takes the source dir + output path as executeRenderJob args (they moved + // out of the job config), and resolves the cut's HTML from entryFile + // relative to projectDir. const job = createRenderJob({ - inputPath: htmlPath, - outputPath: outputLocation, - width, - height, fps: FPS, + quality: QUALITY, + format: "mp4", + entryFile: `${orientation}.html`, }); let timer; try { @@ -170,10 +185,10 @@ export async function renderComposition({ ); }); await Promise.race([ - executeRenderJob(job, (progress) => { + executeRenderJob(job, compositionDir, outputLocation, (progress) => { console.log( `hyperframes-renderer: ${compositionId}/${orientation} ${Math.round( - progress.percent * 100, + (progress?.percent ?? 0) * 100, )}%`, ); }),