HyperFrames is a Node module. Drop it into your Next.js App Router, expose a /api/render endpoint, and stream MP4 back to the browser. Cache hot renders at the edge by composition hash.
The HyperFrames npm package bundles the runtime and the FFmpeg binary it needs. No global install, no system dependencies.
# Install
pnpm add hyperframes
# Or
npm i hyperframesThe route handler takes inline HTML, renders to MP4, and returns the buffer with video/mp4. Force the Node runtime — Chromium can't run on the Edge runtime.
// app/api/render/route.ts
import { NextResponse } from "next/server";
import { renderHtml } from "hyperframes";
export const runtime = "nodejs"; // Chromium needs the Node runtime
export const dynamic = "force-dynamic";
export async function POST(req: Request) {
const { html, width = 1920, height = 1080, fps = 30 } = await req.json();
const mp4 = await renderHtml({ html, width, height, fps });
return new NextResponse(mp4, {
headers: {
"content-type": "video/mp4",
"cache-control": "public, max-age=31536000, immutable",
},
});
}// app/page.tsx
"use client";
import { useState } from "react";
export default function Page() {
const [src, setSrc] = useState<string | null>(null);
async function go() {
const res = await fetch("/api/render", {
method: "POST",
body: JSON.stringify({ html: "<h1 data-start='0' data-duration='4'>Hi</h1>" }),
});
const blob = await res.blob();
setSrc(URL.createObjectURL(blob));
}
return (
<>
<button onClick={go}>Render</button>
{src && <video src={src} controls autoPlay />}
</>
);
}Renders are deterministic, which means they cache cleanly. Hash the HTML, look up Vercel Blob (or any object store), and serve cached MP4s without re-rendering.
// Optional: edge-cache the rendered MP4 by composition hash.
import { createHash } from "node:crypto";
import { put } from "@vercel/blob";
const key = createHash("sha256").update(html).digest("hex");
const url = `renders/${key}.mp4`;
const cached = await fetch(`https://blob.vercel-storage.com/${url}`);
if (cached.ok) return new Response(cached.body, { headers: { "content-type": "video/mp4" } });
const mp4 = await renderHtml({ html, width, height, fps });
await put(url, mp4, { access: "public" });
return new Response(mp4, { headers: { "content-type": "video/mp4" } });A single route handler turns your existing site into a video pipeline.