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
- Parse
Timeline.json(exported from Google Maps) into a list of timestamped points - Draw each video frame on a
<canvas>— map tiles, the route polyline so far, a moving marker, counters - Encode frames with WebCodecs (
VideoEncoder, H.264) - 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();
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.encodeQueueSizeand 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
latitudeE7integers 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)