DEV Community

siwet zhou
siwet zhou

Posted on • Originally published at s.sum.pub

How I Recorded 10 Hours of My Screen in 3 MB Using WebCodecs

Last week I needed to debug an AI agent that runs for ~8 hours overnight. I wanted to see what it did — but not as 30+ GB of continuous video. So I built a browser-based timelapse screen recorder. 10 hours of screen activity now fits in a few MBs.

The project is live at s.sum.pub (cloud + sharing) and re.sum.pub (recorder). This post is about how the recorder part works.

The problem with normal screen recording

Continuous 1080p @ 30fps is ~5-10 MB per minute with a good codec. An 8-hour session is 2-5 GB. For workflows where you review rather than watch — training runs, agent runs, quant backtests, overnight debugging — that's a huge amount of data for footage you'll scrub through at 10x.

But scrubbing is most of what you do. What you actually need is one frame every few seconds.

The core idea: frame-interval capture via WebCodecs

Instead of recording 30fps continuously, capture one frame every N seconds and encode just those frames into an MP4. 10 hours at 1 frame / 5s = 7,200 frames. At ~0.5 KB per encoded frame (H.264, heavily delta-compressed), that's ~3.5 MB.

The whole pipeline is browser-native:

  1. getDisplayMedia() — prompt the user to share a screen/window/tab
  2. Read a frame from that stream every N seconds
  3. Feed frames to VideoEncoder (WebCodecs)
  4. Mux encoded frames into MP4 with mp4-muxer
  5. Save as a blob / upload to cloud

No FFmpeg. No WASM builds. No native app. Everything is 2024+ browser API.

Sketch of the capture loop

const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const track = stream.getVideoTracks()[0];
const processor = new MediaStreamTrackProcessor({ track });
const reader = processor.readable.getReader();

const encoder = new VideoEncoder({
  output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
  error: (e) => console.error(e),
});

encoder.configure({
  codec: 'avc1.42E01F',       // H.264 baseline
  width: 1920, height: 1080,
  bitrate: 500_000,           // low — we're only encoding a few frames/sec
  framerate: 20,              // playback fps, not capture fps
});

let frameIndex = 0;
const INTERVAL_MS = 5000;      // one frame every 5 seconds

while (recording) {
  const { value: frame } = await reader.read();
  if (!frame) break;

  encoder.encode(frame, { keyFrame: frameIndex % 60 === 0 });
  frame.close();
  frameIndex++;

  await sleep(INTERVAL_MS);
}

await encoder.flush();
const mp4Blob = muxer.finalize();
Enter fullscreen mode Exit fullscreen mode

A few subtle things worth calling out:

1. Capture fps ≠ playback fps

The encoder is configured at 20 fps. The capture loop sleeps 5 seconds between frames. The result is a perfectly valid MP4 where each "frame" represents 5 real-world seconds. Played at 20fps, 10 real hours play back in 6 minutes. Scrub-friendly.

2. Keyframe every N seconds, not every N frames

For scrubbing performance, you want keyframes close together. But every frame is already ~a single keyframe-worth of change (5 seconds apart). Forcing a keyframe every ~60 seconds of "wall clock" keeps seek-times fast without blowing up file size.

3. Frames must be closed

VideoFrame objects are backed by native memory. If you don't .close() them after .encode() returns, you will OOM the tab in about 10 minutes. Ask me how I know.

4. MediaStreamTrackProcessor is Chromium-only (for now)

Firefox and Safari don't ship MediaStreamTrackProcessor yet. On those you fall back to drawing the stream to a <canvas> and new VideoFrame(canvas). Slower, but works.

What this enables

Because files are small, the companion cloud (s.sum.pub) just stores them as regular objects and hands out share URLs. No transcoding pipeline, no CDN drama. Upload a 3 MB timelapse, share a link, done.

Concrete use cases I've heard from early users:

  • AI agent debugging — let Claude / Cursor / your agent run for 4 hours, review as a 2-minute timelapse
  • Quant backtests — record the run, share with the team
  • Long tutorials — a 3-hour course becomes a scrubbable index
  • Overnight builds/monitors — catch "when exactly did this break?" without SIEM

Try it

  • Recorder: re.sum.pub — pick an interval (1-60s), hit record
  • Cloud: s.sum.pub — upload, share public or private

Feedback very welcome — especially from anyone who's pushed WebCodecs further or has war stories about the same trade-offs.

Top comments (0)