← 返回信息流
AI 资讯Hacker News·11 天前

使用 FFmpeg 从 Mixbook 数据 API 重构电影

原标题:Reconstructing a Mixbook movie from its data API with FFmpeg

速览

本文介绍了一种利用 FFmpeg 工具结合 Mixbook 数据 API 来重构视频内容的方法。通过解析 API 返回的数据,可以自动化地重组视频片段。这一技术为视频内容的动态生成和个性化定制提供了新的解决方案。

AI 深度解读

从 API 数据重构 Mixbook 动画电影:一次逆向工程实录

背景

Mixbook 是一家提供照片书和定制视频服务的公司。作者收到一封来自 Mixbook 的邮件,通知其制作的动画项目(一部由照片组成并配有音乐的短片)已准备就绪。用户可以在浏览器中观看,也可以订购实体打印版,但官方并未提供直接下载视频文件的功能。

在 Mixbook 的帮助文档中明确指出不存在下载按钮。作者对此产生了好奇:为什么没有下载按钮?这不仅仅是一个功能缺失的问题,其背后的技术架构更为有趣。事实上,服务器上并不存在预渲染的视频文件。所谓的“电影”是在用户每次点击播放时,由浏览器逐帧实时构建生成的。

重要声明:本文所述的逆向工程仅针对作者自己的项目,通过其个人分享链接访问。所有内容仅涉及从提供无导出功能的服务中获取用户自己的记忆数据,未涉及任何他人的内容。

核心内容

第一道死胡同:页面 HTML 中无视频文件

最直观的思路是打开项目页面,在 HTML 源码中寻找视频 URL,但这行不通,原因有二:

  1. 编辑器 URL 是私有的:如果没有登录会话,直接请求编辑器页面会返回 403 Forbidden
  2. 公开分享链接中无视频:分享预览链接包含一个只读访问令牌(view key, vk)。虽然该页面能正常加载(HTTP 200),但在原始 HTML 中搜索 .mp4.m3u8videoUrl 或任何媒体相关的关键词,均无结果。这是一个 JavaScript 应用壳(Application Shell),视频是在页面启动后通过 API 调用动态构建的。

跟随 View Key 进入第二个应用

通过查看预览页面嵌入的配置,作者发现了一个关键的服务主机地址:memories.mixbook.com。这是一个独立的 Next.js 应用,被称为“Memory Explorer”。

当向该域名请求相同的路径时,返回的是真实的路由响应。响应内容是一个 React Server Components 载荷,其中指明了渲染页面的组件名为 AnimatedProject。这个组件在客户端水合(hydrate)后,会自行获取数据。因此,视频 URL 必然包含在该组件请求的数据中。作者下载了相关的 JavaScript 代码包并进行了分析。

阅读应用代码以发现 API

由于 Next.js 的代码块通常是压缩混淆的,直接阅读非常痛苦。作者采用了一种更聪明的方法:在代码中搜索特定的字符串模式。

在其中一个代码块中,字符串 animatedProject 出现了 22 次。通过分析上下文,作者找到了数据获取代码,这是一个 Redux Toolkit 的异步 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}

通过在同一 bundle 中查找环境配置块,作者找到了 apiBaseUrlhttps://www.mixbook.com。最终构造出的请求如下:

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

该请求返回 HTTP 200 状态码以及 174 KB 的 JSON 数据,其中详细描述了如何构建这部电影。

“电影”的真实形态

API 返回的并非渲染好的视频文件链接,而是电影的定义数据

  • 基本信息:名称、总时长(约 108.3 秒,按 24fps 计算为 2598.96 帧)。
  • 音乐轨道:包含音乐名称和音频文件 URL。
  • 片段(Segments):共 43 个片段。
  • 转场(Transitions):共 42 个转场效果。

每个片段包含一个完整的 Lottie 动画。这是一种 1920×1080 分辨率、24fps 的矢量动画,用户的照片作为资产嵌入其中。数据结构如下:

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

这就解释了为什么没有下载按钮:Mixbook Movie 本质上是 43 个 Lottie 动画、42 个转场和一首音乐轨,在浏览器播放时实时合成。服务器上从未存在过最终的 MP4 文件,因此用户无法下载,只能重新渲染。

这种架构对 Mixbook 而言非常合理:存储成本低、易于编辑、且与分辨率无关。但这留下了两条获取文件的路径:屏幕录制,或从原始素材自行重构。作者选择了后者。

提取原始素材

JSON 数据包含了所有必要信息。作者编写脚本遍历片段,按播放顺序收集所有照片资产 URL 并获取音乐轨道。所有资源均位于公开的 S3/CDN URL 上。

最终收集到的素材包括:

  • 42 张唯一照片(按顺序排列)
  • 1 首音乐轨道("Acoustic Breeze")
  • 108 秒的运行时长

重构版本 1:长度匹配的交叉淡入淡出幻灯片

首要目标是生成一个 1080p 的幻灯片视频,照片按顺序排列,使用交叉淡入淡出(crossfade)效果,并与音乐同步,总时长与原片一致(108.3 秒)。

关键计算: 假设每张照片显示时间为 $D$ 秒,连续照片之间的转场重叠时间为 $T$,照片总数为 $N$,总时长为 $TOTAL$。公式为: $$ TOTAL = N \cdot D - (N - 1) \cdot T $$ 解出 $D$: $$ D = (TOTAL + (N - 1) \cdot T) / N $$

代入数值($N=42, T=0.8, TOTAL=108.3$):

  • 每张照片持续时间 $D \approx 3.36$ 秒
  • 每张照片净推进步长 $step = D - T \approx 2.56$ 秒

FFmpeg 实现逻辑

  1. 将每张照片标准化为 1920x1080、30fps 的片段,添加黑边以保持比例。
  2. 使用 xfade 滤镜链连接片段。第 $n$ 个交叉淡入淡出的偏移量为 $n \cdot step$。
  3. 音乐轨道裁剪至总长度,并在最后 2 秒添加淡出效果。

输出结果时长为 108.3 秒,照片和节奏正确,音乐同步淡出。但这只是一个基线,静态照片配合溶解效果相比原片缺乏动感。

重构版本 2:肯·伯恩斯效应(Ken Burns Motion)

为了让幻灯片生动起来,作者引入了“肯·伯恩斯效应”,即对静止图像进行缓慢的缩放和平移。FFmpeg 的 zoompan 滤镜可以实现这一效果,但存在一个陷阱。

陷阱与修复zoompan 滤镜对于输入的每一帧都会输出 $d$ 帧

查看原文 →segar.me