Keenan Pereira My CV

Product films recorded from the app, with no ffmpeg

Demo videos were wanted in lengths that kept changing, from 20 seconds to 90. The app now records its own films in headless Chrome, exact to the frame on a virtual clock, and encodes them with Chrome’s own H.264 encoder.

27 Oct 2026 3 min readFrom the project Showroom 3D tour

The problem

Demo videos were wanted to send, in lengths that kept changing from 20 seconds to 90.

The fix

Record the app’s own films in headless Chrome on a virtual clock, and encode them in Chrome.

87 s
lock film, no feature left out
89 → 65 s
switch film, after “a bit faster”
< 25 MB
every 720p version, small enough to email

The showroom app already plays a film for each product it demos, like the smart lock turning to face you. In October the ask was to send those demos to clients as videos. Then the length kept moving: “no longer than 20 seconds”, then 50, then 90 with no feature left out.

So the videos come from a script that records the app itself, and a new length is a new list of cuts.

Taking the page’s clock away

A screenshot takes as long as it takes, and a live page keeps animating meanwhile, so a plain recording comes out uneven. The recorder fixes that before any of the page’s own scripts run. Timers, animation frames, Date.now and performance.now all run on a virtual clock. Every CSS transition and animation is paused and set to that clock’s time, and any video playing in the page is moved on by seeking.

// once per frame of the video
clock += 1000 / 30;            // exactly one frame, however slow the screenshot
runTimersAndFramesDue(clock);  // the page's own code, on the recorder's time
seekAnimationsAndVideos(clock);
await picturesLoaded();        // nothing pops in late
screenshot();

Each frame is the film exactly 1/30 s after the one before, at 1920 × 1080, on a fast laptop or a slow one. The page is drawn at twice the pixels and scaled down, which keeps the turning lock’s edges smooth. Recording the lock’s film takes about 25 minutes, and that’s fine: every frame is right.

Chrome as the encoder

The tool needs Google Chrome and nothing else: no ffmpeg, no npm install. A second headless Chrome hands the frames to Chrome’s own H.264 encoder through WebCodecs, and a small MP4 writer in the tool wraps what comes out. Writing the MP4 by hand meant meeting H.264’s details directly:

  • Extra units before every frame. Chrome’s encoder puts an SVC prefix unit before each slice. Plain players are meant to skip them; a strict one may not. The writer drops them.
  • The wrong level. The encoder can declare level 5.0, which some TVs refuse to play. The writer declares what the size needs: 4.0 for 1080p, 3.1 for 720p.
  • The index up front, so a video starts playing before it has all downloaded.
  • No silent damage. If frames come out of order or any are missing, it stops rather than write a broken file.

The cuts

Each cut is a list of moments by the film’s own clock. A --timeline run prints when every shot, title and subtitle comes and goes, and a cut longer than its limit stops the script.

  • Smart lock: 87 seconds, every feature and every subtitle line, with only the waiting taken out. 15.2 MB at 720p for email, 38.9 MB at 1080p.
  • The 20-second cut: 19 seconds, seven moments.
  • The switch: 89 to 65 seconds, after a note to “make the video a bit faster”.
  • All the products: 178 seconds, with the 720p bitrate worked out from the length so it stays under email’s 25 MB.

Bitrate was settled by eye: on the lock’s dark, mostly slow film, 6 Mbps at 1080p looked the same as 10, and 4 nearly so. The same encoder route also shrinks the app’s upright 4K phone clips from 23 to 35 MB each to 1.5 to 2.8 MB.

What I took from it

  1. Own the clock and every frame is exact. Recording speed stops mattering.
  2. The browser already ships an encoder. WebCodecs and a small MP4 writer do the job of a toolchain.
  3. Write for the strictest player. Declare the level the picture needs, and drop what a strict player may choke on.
  4. Make length a setting. When 20 seconds becomes 90, a script reruns and nobody re-edits.

I directed the work and Claude Code wrote much of the code. The films are the app’s own, so when the app changes, the videos are one command behind it.

Keep reading