Building a Browser-Based Offline Video Recorder

Elijah Koulaxis

August 19, 2025

building-a-browser-based-offline-recorder

Alright, buckle up, because this is going to be a long ride. I’m about to take you through the nitty-gritty details of how I built a browser-based offline video recorder, the challenges I faced, and the solutions I came up with. This isn’t just a “here’s what I did” post.. it’s a deep dive into the why and how behind every decision, and every workaround.

First things first, when I started this project, I thought, “Why not just livestream the browser’s gameplay and record it in real time?” It seemed like the simplest solution. Just run the browser, capture the screen, and pipe the frames into ffmpeg. Easy, right? Wrong.

Setting Up the Infrastructure: Docker, Virtual Display, and Audio

Challenges with Virtual Display

Browsers need a display to render graphics, even in headless mode. Without a display, you’ll run into nasty errors..

I used Xvfb (X Virtual Framebuffer) to create a virtual display inside the Docker container and fluxbox, a lightweight window manager, to manage the virtual display.

Challenges with Capturing Audio!

Capturing audio from the browser was another challenge. Browsers don’t output audio directly in headless mode, so I had to use PulseAudio to create a virtual audio sink and Configure the browser to output audio to this virtual sink to finally use ffmpeg to record audio from the virtual sink.

We're Just Getting Started

  1. Laggy Video: The browser couldn’t keep up with rendering complex graphics in real time. Frames were dropped, animations stuttered, and the final video looked like it was running on a potato.

  2. Desynchronized Audio: Audio and video were out of sync because the browser’s rendering speed fluctuated. Sometimes the browser would lag behind, and the audio would keep playing, resulting in a mess.

  3. Unpredictable FPS: The FPS of the captured video was all over the place. One second it was 30 FPS, the next it was 15 FPS, and then it jumped to 60 FPS, making the video unwatchable.

I realized that livestreaming wasn’t going to cut it. Instead, I needed to capture the browser’s output in offline mode. This meant:

  1. Instead of letting the browser run at its own pace, I would manually advance its time in fixed increments
  2. By controlling virtual time, I could ensure that every frame was captured at exactly the right moment
  3. Once I had all the frames, I could use ffmpeg to create a smooth, high-quality video

Why Virtual Time Was Needed

Browsers are designed to run in real time. They rely on the system clock to schedule animations, timers, and rendering. But when the browser is under heavy load (e.g., rendering a graphics-heavy game), it can’t keep up.

soo... I faced the following issues

  1. The browser skips frames to catch up with the system clock
  2. Animations and timers don’t run smoothly because the browser is struggling to keep up

Virtual time is like a cheat code for the browser. Instead of relying on the system clock, I injected a javascript patch that let me control the browser’s time manually.

What did I do?

  1. I disabled the browser’s reliance on the system clock
  2. I manually advanced the browser’s time in fixed increments (e.g., 33.33 ms for 30 FPS) (will explain later the maths behind it)
  3. I patched the browser’s animation and timer APIs to use virtual time instead of real time

This gave me complete control over the browser’s rendering. No more dropped frames, no more inconsistent timing.. just smooth, predictable output.

The FPS Math Behind It

Let’s say I wanted to create a 10-second video at 30 FPS. That means:

The Calculations

Time Step per Frame

stepMs = 1000 / targetFps = 33.33ms

Total Frames

totalFrames = 10000 / 33.33 = 300 frames

Advancing Virtual Time

In each loop, I advanced the browser's virtual time by stepMs, using the following javascript patch:

window.__advanceVirtualTime(stepMs);

The advanceVirtualTime is a method that does a lot of stuff behind the scenes, basically manipulating builtin functions such as setInterval etc.

Capturing Frames

After advancing virtual time, I captured a screenshot of the browser’s output using Puppeteer’s page.CaptureScreenshot()

And I Still Had Issues...

Offline Capturing Isn’t Real-Time!

When you capture frames offline, the actual time spent capturing them doesn’t match the intended duration of the video. For example:

If you capture 300 frames in 2 seconds, ffmpeg will assume the video duration is 2 seconds. But the actual gameplay duration is 10 seconds, this results in a video that plays in fast forward.

To fix this, I used ffmpeg’s setpts filter to adjust the playback speed:

-filter:v "setpts=slowdownFactor*PTS"

where:

slowdownFactor = actualDuration / intendedDuration

in this case: 10 / 2 = 5

so now the video plays at the correct speed!

How It All Comes Together

I used PulseAudio to create a virtual audio sink (virtual_sink.monitor) and ffmpeg to record audio from it

I injected a JavaScript patch into the browser to control virtual time

I advanced virtual time in fixed increments and captured frames using Puppeteer

I piped the captured frames into ffmpeg to create the video

I adjusted playback speed

So by going with offline capturing, I had consistent fps by controlling the virtual time, I basically ensured that every frame was capture at exactly the right time. No dropped frames, since the browser wasn't running in real time, it didn't drop frames to catch up with the system clock. Every single frame was rendered and captured perfectly. All in all, the final video was smooth, high-resolution and with no lag

Conclusion

It wasn’t easy, but damn I liked it. If you’re building something similar, I hope this post saves you some headaches haha And if you’re just here for the nerdy details, I hope you enjoyed the deep dive :D

Tags:
Back to Home