Video as code, for the live web

Product videos that never go stale.

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.

Watch the films
promo.mjs — the entire build. Input: any live URL. Output: an MP4.
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 },
  ],
});

Every screen recording starts dying the day it is made

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.

Real-time capture drops frames, structurally

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.

Hand work does not survive a release cadence

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.

The video becomes a build artifact

The storyboard is a file in your repo

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.

Capture runs on a virtual clock

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.

Interactions are authored physics

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.

Post-production is in the same file

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:

Films

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.

Quickstart

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.

Status