Frame 250 of a 25-frame-per-second video belongs at ten seconds. It still belongs there if the machine takes half a second to render it.

I can wait. A player must keep up with a clock. A renderer can give each frame the time it needs and assemble the results afterwards.

My video editor already displays its elements in a browser. To produce a video file I can drive that page, set its time and take screenshots, using the layout and drawing already working there for the finished video too.

(YouTube does not accept “open this tab and press play” as a file format.)

For a constant frame rate, the frame index determines the timestamp:

const fps = 25;
for (let frame = 0; frame < 5; frame++) {
  console.log(frame, (frame / fps).toFixed(2));
}
// 0 0.00
// 1 0.04
// 2 0.08
// 3 0.12
// 4 0.16

Frame time comes from division. A slow capture changes how long the job takes, but it shouldn't change the animation time represented by a frame.

A Go renderer can control a headless Chromium page through go-rod, call the page's setTime function, poll a readiness flag and capture a PNG. Numbered frame files can then be assembled into video. Only the page's own clock can move the scene to that time. That clock is what makes this useful as an offline renderer. The screenshot is the generic part. Chrome's captureScreenshot protocol shows the general browser facility.

Here's how the renderer and page coordinate:

renderer                     page
setTime(10.0) ──────────────→ update scene and seek media
                              wait for required media
poll readiness ────────────→ report ready
capture frame 250 ─────────→ paint at the requested time

Ready for what?

Waiting for the right picture

Setting a media element's playback time asks the browser to seek. If it still needs decoding or fetching, it isn't ready yet. Taking a screenshot immediately can capture the previous image, a blank region, or a mixture of elements that reached the requested time at different moments.

Suppose the renderer polls a readiness flag up to a hundred times, sleeping ten milliseconds between attempts. If it continues towards the screenshot after that loop, it can capture a frame even when the flag never became true. A flag named ready sounds like the answer I need. Then I read who sets it. The player's seek-timeout handler can set it to true even when the image hasn't arrived. I've taught both sides to agree that waiting ended. That's useful for ending a wait, but it hasn't given me frame 250.

I'd rather have a failed frame to investigate than a complete video with the wrong picture tucked into it, because giving that image a filename and encoding it makes the mistake harder to find. The renderer should capture after successful readiness, or report that this frame never became ready.

Warm-up waits before capturing a segment can help with loading. Browsers are good at delaying work they think a viewer won't need yet. A renderer that jumps directly to a later timestamp can ask for assets that normal playback would have encouraged the browser to load along the way.

A longer warm-up is tempting because I can change one number and try again, but I'd still be guessing which part needed the extra time. Name the outstanding asset in the readiness report, and a timeout gives me something to work on. The wrong-looking screenshot can then have an explanation.

The scene clock controls animations that use it. Other sources of change still need attention. An animation may use its own clock, a network response or font may arrive late, or random state may be consumed in a different order.

I can make the requirement concrete. Given assets and configuration, requesting a timestamp should produce the intended scene state before capture. Each participating element has to honour that request.

Audio is another timeline to arrange. I can assemble it separately, trimming and delaying media before mixing. The image sequence and the sound then need to agree about frame rate, duration and start times when encoded together.

I've got a time for each frame even when this machine takes a while to render it. That part is settled. Now I need the images, animations and audio to agree about that time, and a readiness report that tells me which one is still holding things up.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.