
2026/05/24 9:31
FFmpeg を用いて Mixbook のデータ API からムービーを再構築する
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本稿の核心メッセージは、開発者が Mixbook のシステムを逆方向に解析することに成功し、アニメーションされたプロジェクト動画を独立した MP4 ファイルとしてダウンロードできるようにした点にある。これにより、ユーザーは自身のデジタル記憶に対してローカルの所有権を取得できる。Mixbook は動画ファイルを直接提供するのではなく、React アーキテクチャを採用しており、JSON 源からデータをストリーミングし、ブラウザ上でアニメーションを描画している。エンジニアたちは、「Memory Explorer」API を通じてこの内部データを抽出し、FFmpeg や Python スクリプトなどのツールを活用することで、これらの映画を忠実に再現した。モーション、音楽、テキストのすべてを正確に複製することができた。再構築プロセスは 4 つの段階を経て進化したが、それは以下の通りである:V1 ではクロスフェードスライドショーを作成し、V2 ではアップスケールを適用してケン・バーンズ効果を付与し、V3 ではフォントフィルターによる制限に対処するために PNG レンダリングを用いてイントロ/アウトロのテキストを復元し、V4 ではスライドショーのリズムに合わせるためにタイトルタイミングを調整した。最終的な出力は、モーションとテキストが概ね適切であるものの、技術的な課題によりフレームごとのベクター easing や正確なオリジナルタイポグラフィには欠ける 4K MP4 である。その結果、ピクセル単位の完全に同一の複製を得るためにはスクリーンレコーディングという手法だけが唯一の方法となる。このブレイクスルーは、プラットフォーム側がダウンロードを阻止することを意図した設計選択を回避するものであり、ユーザーにより良いメディアコントロールを提供しつつ、直接ファイルアクセスなしでロウなアニメーションアセットのエクスポートにおける現状の限界も浮き彫りにしている。
本文
MixBook 動画ファイルをダウンロードできない時の逆算術:JSON データから再構築する完全ガイド
MixBook 上でアニメーションプロジェクトの準備が完了しましたが、ブラウザでの再生や印刷版注文は可能で、ダウンロード機能は一切実装されていません。
この状況は単なる機能制限ではなく、「レンダリング即し(オンデマンド)」アーキテクチャを採用していることを意味しています。完結した MP4 ファイルが存在せず、代わりに JSON データ構造から再生時に動的にコンポジット(合成)されているのです。
本稿では、この「ダウンロードできない」壁を突破し、リバースエンジニアリング的手法で動画を再構築するプロセスを解説します。
重要:免責事項
以下の手順は、自身の共有リンクを通じてアクセスした独自プロジェクトを対象としたメモリの回収方法についてのみ記述しています。第三者のコンテンツや外部への悪影響を与える意図は全くありません。
1. ダウンロード不能な理由と最初の行き詰まり
直感的なファイル探索では以下のように失敗します。
- 編集用 URL は非公開
- ログインしていない状態でアクセスすると
を返します。403 Forbidden
https://www.mixbook.com/memories/edit?pid=123456 → 403 エラー - ログインしていない状態でアクセスすると
- 共有プレビュー URL にも動画ファイルなし
- 視聴用キー(
)を付与したプレビューリンクでは、HTML のみ返ってきます。生 HTML にvk
や.mp4
は含まれません。videoUrl
https://www.mixbook.com/memories/preview?pid=123456&vk=YOUR_VIEW_KEY → 純粋な HTML ページ(約 110KB) - 視聴用キー(
ページ自体は正常に読み込まれますが、これは JavaScript アプリケーションのシェル に過ぎず、動画データはクライアントサイドで構築されているためです。
2. レア・エンハンスメント:独立した Next.js アプリへのアクセス
プレビューページの埋め込み設定から、以下のサービスホストが確認できました。
{ "baseUrl": "https://memories.mixbook.com" }
これらは MixBook メインサイトとは異なるドメインで、実際のルートに到達できます:
https://memories.mixbook.com/memories/preview?pid=123456&vk=... → 200 OK
このリクエストには React サーバーコンポーネントのペイロードが含まれており、そこから
というコンポーネントが特定されました。このコンポーネントは独自にデータをフェッチします。AnimatedProject
3. API エンドポイントの特定
Next.js バundles の検索(grep)により、データ取得ロジックのソースコードが見付かりました。
// Redux Toolkit async thunk の一部 let h = (0, n.hg)("animatedProject/fetchAnimatedProject", async (t, e) => { let { projectId: i, viewKey: n } = t, a = o().auth.token, u = "".concat(s.l.apiBaseUrl, "/api/v2/my/animated_projects/").concat(i); return n && (u += "?".concat(new URLSearchParams({ vk: n }))), (await r.L.get(u, { token: a })).data.data; });
より:
- API エンドポイント
{apiBaseUrl}/api/v2/my/animated_projects/{projectId}?vk={viewKey} - 呼び出しテスト(cURL)
curl -s "https://www.mixbook.com/api/v2/my/animated_projects/123456?vk=YOUR_VIEW_KEY"
これにより、約 174 KB の JSON データが返され、動画の「定義」が取得できました。
4. 動画の真の構造:コンポジットされたメディア
API は完成済み動画を返すのではなく、再生に必要な定義データのみを返します。
-
プロジェクト定義
data ├── name: "Our Trip to the Coast" ├── durationInFrames: 2598.96 # 24fps で約 108.3 秒 ├── musicTrack # オーディオファイル URL (S3) ├── segments: [ 43 items ] # セグメントリスト(動画単位) └── transitions: [ 42 items ] # トランジションリスト -
各セグメントの内容
- Lottie ベクターアニメーション (
)1920x1080, 24fps - フォン assets に写真(
)やコンポジットレイヤーが含まれます。photo - 写真 URL: S3/CDN で公開されています。
- Lottie ベクターアニメーション (
結論:サーバー上には MP4 ファイルは存在しません。 ダウンロードすべきものはないため、ブラウザの「再生」動作を真似て自分で再構築するしかありません。
5. 素材抽出スクリプト
必要な情報はすべて API から取得可能です。以下の Python スクリプトで写真と音楽のリストを作成します。
import requests import json # データ取得 data = requests.get("YOUR_API_URL", params={"vk": "YOUR_VIEW_KEY"}).json() segments = data["data"]["segments"] photos = [] for s in segments: for a in s["lottieAnimation"].get("assets", []): # 写真タイプのみ収集 if a.get("meta", {}).get("type") == "photo" and a["p"].startswith("http"): photos.append(a["p"]) print(f"{len(photos)} 枚の写真と音楽トラックを収集しました。")
6. 再構築プロセス v1~v4
Stage 1: スライドショーの時間計算(クロスフェード)
108.3 秒という目標時間で、42 枚の写真を表示するには、以下の算術が必要です。
- パラメータ
(写真数): 42N
(トランジション時間/重なり): 0.8 秒T
(目標時間): 108.3 秒TOTAL
- 計算式
D = (TOTAL + (N - 1) * T) / N # 各写真あたりの表示時間(約 3.36 秒) step = D - T # クリップの移動量(約 2.56 秒)
FFmpeg スクリプト構成:
- 写真を
にスケール・ノーマライズ。1920x1080
フィルタを連鎖させ、xfade
でオフセットを調整。step- 音楽トラックに合わせてフェードアウト。
Stage 2: ケンバーンズ効果(Ken Burns Effect)の適用
静止画に「生き気」を持たせるには、FFmpeg の
zoompan フィルターを使用します。
- ジッター回避
- 単なる
はカクつきが発生するため、zoompan
で最初のフレームのみを読み込ませ、そこから滑らかに動きを生成させます。select='eq(n\,0)'
- 単なる
- アップスケール
- ズーム前の写真を 4K にアップスケーリングしておくことで、クロップしてもシャープな画像を保ちます。
- アニメーションの制御
select='eq(n\,0)', # 入力フレームを最初の 1 フレームに固定 zoompan=z='...':x='...':y='...':d=101:s=1920x1080:fps=30 # d を全フレーム数(約 101)に設定
Stage 3: テキストとタイトルカードの復元
API レスポンスから抽出されたテキスト(イントロ、アウトロ)を表示するには、
ffmpeg drawtext は利用できないため別のアプローチを取ります。
- フォントの問題
- MixBook の独自フォント(Proxima Nova)がない場合、
などを使用します。Arial Bold
- MixBook の独自フォント(Proxima Nova)がない場合、
- 描画手法:PNG コンポジット
- Python
でテキストとドロップシャドウを含む透過 PNG を作成。Pillow - FFmpeg 上で
フィルタで合成。overlay
[1:v]fade=t=in:st=0.3:d=0.5:alpha=1,fade=t=out:st=2.0:d=0.5:alpha=1[ti]; [0:v][ti]overlay=0:0:enable='between(t,0.2,2.6)'[v1]; - Python
Stage 4: タイミングの微調整
元ネタのタイトル表示時間は 5 秒ですが、実際の再構築ではスライドショーのペースに同期させる必要があります。
- 解決策
- タイトルフェードアウト時間を
(約 2.56 秒) より短く設定し、次の写真がクロスフェード入りする直前に消えます。step
"[1:v]fade=t=in:st=0.3:d=0.5:alpha=1,fade=t=out:st=2.0:d=0.5:alpha=1[ti]" - タイトルフェードアウト時間を
7. 再現可能か?の比較表
| 要素 | 再現度 | 備考 |
|---|---|---|
| 写真、順序、タイミング | ✅ 完璧 | API から直接取得・制御可能 |
| 音楽+エンディングフェード | ✅ 完全 | オリジナル音声ファイル使用 |
| クロスフェード効果 | ✅ 実現可能 | フィルタを使用 |
| ケンバーンズ動き | ⚠️ 近似値 | 正確なキーフレームではないが滑らかな実装 |
| タイトル+オウトラードテキスト | ✅ 実現可能 | フォント代用、シンプルフェード処理 |
| 厳密な Lottie アニメーション | ❌ 不可能 | ブラウザ内でのレンダリング動作は再現不可 |
8. まとめと教訓
MixBook の「機能欠落」は、実はデータ駆動型・オンデマンド型アーキテクチャを示しています。
- ページにファイルがなくても、アプリを読み解け。
- クライアントバundles を検索し、
という関数名から API の存在を特定しました。animatedProject
- クライアントバundles を検索し、
- 「ダウンロード不可」は「コンポジット可能」の裏返し。
- JSON デフォニションさえ取得できれば、同じ環境で再構築できます。
- API をインターフェースとして扱い、欠落部分を自分で補完せよ。
が使えないなら Pillow で描画し、オーバーレイ処理を行うなど、手段は無限に広がります。drawtext
このようにして午後間もなく、動き・音楽・タイトルを全て備えた 1080p MP4 ファイルが手元に生まれました。MixBook が提供しなかった「ダウンロード機能」は、私自身が構築したのです。