fix(video-renderer): align render.js with @hyperframes/producer 0.7.36 API

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.
This commit is contained in:
Renn F
2026-07-08 13:12:08 +02:00
parent 312ec990dd
commit cf96fa7f11
+28 -13
View File
@@ -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,
)}%`,
);
}),