DEV Community

Cover image for Rendering MP4 travel videos entirely in the browser with WebCodecs
xiazai77
xiazai77

Posted on

Rendering MP4 travel videos entirely in the browser with WebCodecs

Last month I shipped a web app that turns Google Maps location history into an MP4 travel video — route animating on a map, distance counter, music. The constraint I set myself: the video had to be rendered entirely in the browser. Location history is about the most sensitive file a user can hand you, and I didn't want a backend touching it at all.

Here's what that pipeline looks like, and the parts that surprised me.

The pipeline

  1. Parse Timeline.json (exported from Google Maps) into a list of timestamped points
  2. Draw each video frame on a <canvas> — map tiles, the route polyline so far, a moving marker, counters
  3. Encode frames with WebCodecs (VideoEncoder, H.264)
  4. Mux the encoded chunks into an MP4 container, add an AAC audio track

No ffmpeg.wasm, no server-side rendering. Modern browser APIs are enough.

Encoding: WebCodecs is the easy part

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

encoder.configure({
  codec: "avc1.42001f",   // H.264 Baseline — plays everywhere
  width: 1080,
  height: 1920,           // 9:16 vertical
  framerate: 30,
  bitrate: 8_000_000,
});

for (let i = 0; i < totalFrames; i++) {
  drawFrame(ctx, i);      // canvas drawing, fully synchronous
  const frame = new VideoFrame(canvas, { timestamp: (i * 1e6) / 30 });
  encoder.encode(frame, { keyFrame: i % 150 === 0 });
  frame.close();          // forget this and you'll OOM fast
}
await encoder.flush();
Enter fullscreen mode Exit fullscreen mode

Two things that cost me time:

  • VideoFrame.close() is not optional. Frames hold GPU-backed buffers; the GC won't save you at 30fps.
  • Backpressure matters. Check encoder.encodeQueueSize and await when it grows — otherwise long videos eat memory linearly.

Muxing: the part nobody tells you about

WebCodecs gives you encoded chunks, not a playable file. You still need an MP4 container. I use mediabunny for this, and it has a killer feature: it can take an AAC audio file and mux the track in without re-encoding. Background music adds zero encoding cost.

The real boss fight: the input format

The rendering was honestly the fun part. The painful part is Timeline.json itself:

  • Google has changed the export format several times; the current on-device export ("semanticSegments") looks nothing like the old Takeout format
  • Coordinates arrive as latitudeE7 integers in one variant and "geo:lat,lng" strings in another
  • A shocking number of users export a 0 KB file and think your app is broken — the causes range from Timeline never being enabled to picking the wrong file in the share sheet. I ended up writing a whole troubleshooting guide for empty Timeline.json exports, and it's one of the most-visited pages on the site
  • Same story for the export flow itself — it's different on Android and iPhone, so that's its own step-by-step guide

If you're building anything on top of Google location history: budget more time for input handling than for rendering. I'm not joking.

Result

The finished thing is Timeline Visualizer — free, no signup, runs in the browser. Drop in your Timeline.json, pick a date range and music, get a 9:16 MP4.

Happy to answer questions about WebCodecs quirks, the muxing setup, or the Timeline.json format zoo — I've collected far too much knowledge about all three.

Top comments (0)