Reconstructing a Mixbook movie from its data API with FFmpeg

msegar1 pts0 comments

Reconstructing a Mixbook Movie with ffmpeg - Matt Segar

Reconstructing a Mixbook Movie with ffmpeg

Mixbook emailed me to say my animated project was ready: a short movie built from my photos and set to music. I could watch it in the browser or order a printed copy. I could not download it. There is no download button, and Mixbook's help docs say one does not exist.

I wanted the file. Getting it became a small reverse-engineering exercise, because the reason there's no download button is more interesting than a missing feature. There is no video file to download. Your browser builds the movie frame by frame every time you press play.

One caveat up front: this was my own project, reached through my own shareable view link. Everything below is about getting your own memories out of a service that offers no export. None of it touches anyone else's content.

The First Dead End: No File in the Page

The obvious move is to open the project page and look for a video URL in the HTML. That fails for two reasons.

First, the editor URL is private. Fetching it without a logged-in session returns 403 Forbidden:

https://www.mixbook.com/memories/edit?pid=123456 → 403

Second, the public share link contains no video either. The shareable preview URL carries a view key (vk), the access token for read-only viewing:

https://www.mixbook.com/memories/preview?pid=123456&vk=YOUR_VIEW_KEY

That page loads fine (HTTP 200, ~110 KB), but grepping the raw HTML for .mp4, .m3u8, videoUrl, or anything media-shaped returns nothing. It's a JavaScript application shell. The browser builds the video after the page boots and makes its API calls.

So I left the HTML and went after the app's code.

Following the View Key Into a Second App

The preview page's embedded config listed a handful of service hosts. One stood out:

{ "baseUrl": "https://memories.mixbook.com" }

memories.mixbook.com is a separate Next.js application, Mixbook's "Memory Explorer." Requesting the same path against it, instead of www, returns a real route:

https://memories.mixbook.com/memories/preview?pid=123456&vk=... → 200

The response is a React Server Components payload. It names the component that renders the page:

6:I[7305, [ ... "page-ce08bafe9614bffc.js" ], "AnimatedProject"]

An AnimatedProject component, hydrated client-side, fetches its own data. Any video URL would sit in whatever that component requests, so I downloaded the JavaScript bundles it references and read them.

Reading the App's Own Code to Find the API

Minified Next.js chunks are painful to read, so I didn't read them. I grepped them for the shapes I wanted. The string animatedProject appeared 22 times in one chunk. Pulling the surrounding context gave me the data-fetching code, a Redux Toolkit async thunk:

let h = (0, n.hg)("animatedProject/fetchAnimatedProject", async (t, e) => {<br>let { projectId: i, viewKey: n } = t,<br>a = o().auth.token,<br>u = "".concat(s.l.apiBaseUrl, "/api/v2/my/animated_projects/").concat(i);<br>return n && (u += "?".concat(new URLSearchParams({ vk: n }))),<br>(await r.L.get(u, { token: a })).data.data;<br>});

The endpoint:

{apiBaseUrl}/api/v2/my/animated_projects/{projectId}?vk={viewKey}

The last unknown was apiBaseUrl. The same bundle carried an environment config block:

production: { apiBaseUrl: "https://www.mixbook.com", ... }

Putting it together and calling it with my view key:

curl -s "https://www.mixbook.com/api/v2/my/animated_projects/123456?vk=YOUR_VIEW_KEY"

HTTP 200, and 174 KB of JSON describing how to build the movie.

The Real Shape of a "Movie"

This is where I understood the problem differently. I expected the API to return a link to a rendered file. It returned the movie's definition instead:

data<br>├── name: "Our Trip to the Coast"<br>├── durationInFrames: 2598.96 # ≈ 108.3 s at 24fps<br>├── musicTrack<br>│ ├── name: "Acoustic Breeze"<br>│ └── audioFile: "https://mixbook-user-content.s3.amazonaws.com/..."<br>├── segments: [ 43 items ]<br>└── transitions: [ 42 items ]

Each of the 43 segments holds a full Lottie animation , a 1920×1080, 24fps vector animation with my photo embedded as an asset:

"position": 0,<br>"durationInFrames": 170,<br>"lottieAnimation": {<br>"w": 1920, "h": 1080, "fr": 24,<br>"assets": [<br>{ "id": "position_0_photo_...",<br>"p": "https://media.mixbook.com//photos//.png",<br>"meta": { "type": "photo" } },<br>{ "id": "position_0_comp_0", "nm": "COMP_00_INTRO", "layers": [ ... ] }

That answers the download question. A Mixbook Movie is 43 Lottie animations, 42 transitions, and a music track, composited in your browser each time it plays. A finished MP4 never exists on their servers, so you have nothing to download and something to render.

The architecture makes sense for them: cheap to store, editable, and resolution-independent. It also leaves two paths to a file. I could screen-record the playback, or reconstruct the movie myself from its raw materials. I took the second one.

Pulling the Raw Materials

The JSON has everything. A short script walks the segments, collects...

mixbook movie https memories page data

Related Articles