From cf96fa7f1146a3dde05d75bd83f47839d6407d97 Mon Sep 17 00:00:00 2001 From: Renn F Date: Wed, 8 Jul 2026 13:12:08 +0200 Subject: [PATCH] fix(video-renderer): align render.js with @hyperframes/producer 0.7.36 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidecar's render.js was written against an older producer API and never rendered on this deploy: it called createRenderJob({inputPath, outputPath, width, height, fps}) and executeRenderJob(job, onProgress), but 0.7.36 moved the paths to executeRenderJob(job, projectDir, outputPath, onProgress) and takes only render params in the job config. outputPath arrived undefined and dirname(undefined) threw on every render (every composition), HTTP 500. - Pass projectDir (the composition dir) + outputPath as executeRenderJob args; select the cut via config.entryFile. - Drop width/height — 0.7.36 reads dimensions from the composition HTML (data-width/data-height); add the required quality tier. - Stage motion/public into the composition dir: the producer serves the compiled entry at the file-server root, clamping the composition's ../../public/fonts to /public/fonts, so fonts must sit under projectDir. Verified by rendering both v0.19.0 cuts against the running sidecar's 0.7.36 libraries: styled MP4s, no asset 404s. --- video-renderer/render.js | 41 +++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) 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, )}%`, ); }),