Live demo: hyperframes-on-cloudflare.jdrusso1020.workers.dev
A HyperFrames template that previews HTML video compositions in the browser and renders MP4s server-side using a Cloudflare Container (Chromium + FFmpeg) and stores them in R2.
Demonstrates Worker-to-Container fetching via Durable Object bindings, streaming response bodies through the Worker into R2, and bundling sub-compositions into a single self-contained preview HTML at build time.
Deploying provisions a Worker, the RenderContainer Durable Object, and an R2 bucket (hyperframes-renders). Cloudflare Containers requires a Workers Paid plan.
- Preview a bundled composition (
cloudflare-intro) in the browser using<hyperframes-player>, the zero-dependency web component from@hyperframes/player. - Render the composition to an MP4 by POSTing to
/api/render. The Worker streams the composition to a Cloudflare Container running a pre-built image with Chromium + FFmpeg + HyperFrames, streams the rendered MP4 directly into R2, and returns a URL. - Generate from a prompt (BYOK) — paste an OpenRouter API key and a text prompt; the Worker calls OpenRouter (Gemini 3 Flash by default) to synthesize a HyperFrames composition, lints it with
@hyperframes/core/lint, self-heals up to 2× if needed, and previews the result in the player. Click "Render MP4" to capture it. Off by default — see AI generation.
Authoring happens locally. This template ships with one pre-authored composition. To build your own, use the HyperFrames CLI on your machine:
npx hyperframes init my-video
cd my-video
npx hyperframes preview # live-reload editor in your browserThen swap it into this template (see Swapping the composition below).
Browser Worker Container DO (instance_type: standard-4)
┌──────────────────┐ ┌────────────────────────┐ ┌──────────────────────────────────┐
│ <hyperframes- │ ─────▶ │ /api/render │ ────▶ │ Node HTTP server (port 8080) │
│ player> │ │ - load files from │ │ - writes files to /tmp/ │
│ preview iframe │ │ ASSETS │ │ - hyperframes render │
│ │ │ - POST → container │ │ (Chromium + ffmpeg) │
│ │ ◀──── │ - stream → R2 bucket │ ◀──── │ - streams mp4 in response │
│ │ url │ - return /r/<key> │ mp4 │ │
└──────────────────┘ └────────────────────────┘ └──────────────────────────────────┘
│
├─▶ R2 (hyperframes-renders)
│
└─▶ ASSETS (preview HTML, composition files)
Cold-start of a render container is faster than installing dependencies on every request because the renderer is baked into the image at build time, not installed at runtime:
node:22-bookworm-slimbaseapt-get installChromium system libs (libnss3,libxcomposite1,pango, …)npm install hyperframes ffmpeg-static- Symlink
ffmpeg-static/ffmpegto/usr/local/bin/ffmpeg npx hyperframes browser ensureto downloadchrome-headless-shell- Copy
container/server.mjs(a small Node HTTP server) andCMD ["node", "server.mjs"]
At render time, the Worker sends composition files in the request body, the container writes them to a tmp dir, runs hyperframes render, and streams the MP4 back. Container instances sleep after 10 minutes of inactivity (sleepAfter on the Container class).
Cloudflare's Browser Rendering is a hosted Chromium API — great for screenshots and PDFs, but you can't install FFmpeg into it. HyperFrames needs full control of the Chromium process plus an FFmpeg binary on the same filesystem, which is exactly what Cloudflare Containers gives you: an OCI container in a Worker-bound Durable Object, with up to 4 vCPUs and 12 GiB of RAM on standard-4.
With 4 vCPUs, hyperframes render --workers auto launches 3 parallel Chrome workers, cutting the render time roughly 2× vs. the single-worker default.
npm install
npm run devwrangler dev runs the Worker locally and builds + runs the container against your local Docker daemon (Docker is required for local container dev). The browser preview works without Docker; only /api/render needs the container.
If you want to iterate on the Dockerfile or container/server.mjs without booting Wrangler, you can hit the container directly:
docker build -t hf-render .
docker run -d --rm --name hf-test -p 18080:8080 hf-render
node scripts/test-render.mjs 18080 /tmp/out.mp4
docker stop hf-testThe script reads src/composition-manifest.json, base64-encodes the composition files, POSTs them to the container, and writes the MP4 it returns. The bundled 9s composition renders in ~17s on a 6-vCPU host.
src/
index.ts # Worker entry — preview + /api/render + /r/<key>
container.ts # RenderContainer Durable Object
composition-manifest.json # Generated by scripts/build.mjs
container/
server.mjs # Node HTTP server inside the container
package.json # Container deps (hyperframes + ffmpeg-static)
public/
index.html # Preview UI + Render button
compositions/
cloudflare-intro/ # The bundled example composition
index.html
assets/ # Cloud SVG + HF icon used by the intro
scripts/
build.mjs # Run via wrangler.jsonc → build.command
bundle-preview.ts # Bundles composition into single HTML via @hyperframes/core
test-render.mjs # Local container E2E
Dockerfile # Render container image
wrangler.jsonc # Worker + Container + R2 bindings
- Drop your composition bundle into
public/compositions/<your-name>/. - Set
PREVIEW_COMPOSITION_DIRenv var when running build/deploy:Or edit the default inPREVIEW_COMPOSITION_DIR=compositions/<your-name> npm run deploy
scripts/build.mjs(line 16). - Optionally update the player dimensions in
public/index.htmlif your composition isn't 1920×1080. - Re-run
npm run devornpm run deploy—scripts/build.mjsregenerates the manifest and bundle.
The "Generate from a prompt" panel lets a viewer paste their own OpenRouter API key, type a description, and synthesize a HyperFrames composition end-to-end. The composition previews in the player; the Render button then captures it to MP4 just like the bundled one.
It's already on for self-deployers — wrangler.jsonc sets ENABLE_AI_GEN: "true" in vars. Set it to "false" if you're hosting a public demo and don't want visitors triggering renders on your account.
- The user pastes their key into the panel; it's sent in the body of
POST /api/generate. - The Worker forwards the key once to
https://openrouter.ai/api/v1/chat/completionsasAuthorization: Bearer <key>. - The Worker does not log, cache, or persist the key. It exists only for the duration of one request.
- Client-side, the key is mirrored to the tab's
sessionStorageso generate→edit→regenerate doesn't require pasting it every time. Closing the tab clears it.
prompt + key
│
▼
POST /api/generate (Worker)
│
├─▶ build skill prompt (src/lib/hyperframes-skill.ts)
├─▶ fetch openrouter.ai (Gemini 3 Flash by default)
├─▶ lintHyperframeHtml(html) (@hyperframes/core/lint)
├─▶ if lint fails, retry up to 2× with feedback
└─▶ return { html, model, attempts, lintOk, lintErrors }
frontend
│
└─▶ player.setAttribute("srcdoc", html) (no Blob URL needed)
POST /api/render { html } (existing endpoint, now accepts inline HTML)
│
└─▶ container → MP4 → R2 → /r/<key>
The default model is google/gemini-3-flash-preview — cheapest and fastest direct generation per ~80 eval runs in llm-stories-hyperframes, which the prompt is adapted from. You can pass a different model field in the request body to swap in any OpenRouter model.
Cloudflare Containers pricing — pay-per-10ms for memory, CPU, and disk. A 70-second render on standard-4 (4 vCPU, 12 GiB) costs ~$0.008. R2 storage is $0.015/GB-month with no egress fees within Cloudflare's network.
Apache-2.0 — same license as HyperFrames itself.
