Stale the week after recording
The product ships a redesign, and someone re-records by hand — or the video quietly rots. Teams that ship weekly effectively cannot keep marketing video current.
Video as code, for the live web
Give your LLM the vocabulary of human website gestures — scroll, swipe, hover, type, zoom. Brainstorm with your agent; render deterministic 60 fps film of real pages; surgically edit the storyboard for easy updates.
A storyboard file in your repo, rendered against your actual product — cinematic post-production, every aspect ratio, re-rendered from CI on every release. If a browser renders it, playreel can film it.
import { record, assemble, decayScroll } from 'playreel';
const scenes = {
hero: async (page, t) => {
await page.goto('https://your-site.example');
await t.start(); // freeze the clock
t.mark('hero'); // timing marker
await t.cap(t.F(3.0)); // 3 s = 180 frames
},
features: async (page, t) => {
await page.goto('https://your-site.example');
await t.start();
const ys = decayScroll(1200, { flicks: 1 });
await t.cap(ys.length, {
act: (i) => ({ scrollY: ys[i] }),
});
},
};
await record({ scenes, format: 'desktop' });
await assemble({
name: 'promo', format: 'desktop', dir: 'out',
timeline: [
{ scene: 'hero',
filter: 'fade=t=in:st=0:d=0.5:color=white' },
{ scene: 'features', fade: 0.4 },
],
});
The product ships a redesign, and someone re-records by hand — or the video quietly rots. Teams that ship weekly effectively cannot keep marketing video current.
Screen capture, Playwright and Cypress video, CDP screencast: every real-time approach competes with rendering for the same real-time budget. Variable frame rates are not a bug in the tool, they are the method.
Re-recording is the good case. The usual case is a launch video that shows a product you no longer ship, in a UI your users cannot find.
A plain JS file that imports playreel. Versions are commits, variants are parameters, different videos are different files. No schema language, no GUI, no cloud.
The page's Date, performance.now, rAF and timers are frozen and advanced exactly 1/60 s per screenshot; CSS animations are seeked per frame through the Web Animations API. Every frame is perfect regardless of machine speed.
Momentum swipes that feed the page's own inertia, iOS-decay-curve scrolls, hover tours, real :hover states, human-cadence typing, a visible cursor that pulses on every press.
One ffmpeg graph: crossfade chains, haloed titles keyed to timing markers, Ken Burns photo bursts, and 16:9 / 9:16 / 4:5 presets from one storyboard.
The vocabulary your agent writes against:
Every film below was rendered from a storyboard, against a live site. The first decision is always the archetype — what role the site plays in the video — because it changes what you capture, what you compose, and what the film must never claim.
Requires Node ≥ 20. Two dependencies — playwright and ffmpeg-static — and no system installs.
# once
npm install playreel && npx playwright install chromium
# your first render is your actual product, not a demo page
node promo.mjs → promo-desktop.mp4, 60 fps
Copy the storyboard at the top of this page into promo.mjs, point it at your own URL, and run it.
Different videos are different storyboard files; format, language and campaign variants are parameters.
Outputs are gitignored build artifacts — the recipe is what belongs in git.
npm install playreel, Apache-2.0, engine only.npx playreel CLI, a built-in validate() pass, the optional AI-music plugin, GPU rendering for WebGL-heavy pages, burned-in captions.