My Eight-Year Quest to Digitize 45 Videotapes (Part Two)

Michael Lynch

历时八年数字化45盘录像带之旅(下)

原文由 Michael Lynch 发布,订阅该博客

上篇中,我讲述了自己如何历经艰辛,将家中旧录像带转为数字格式并逐一拆分成独立片段的经历。处理完所有片段后,我希望浏览这些视频的体验能像在 YouTube 上查找视频一样简单。但这些视频承载着家人的私人回忆,真正的 YouTube 又太过公开。我需要一种既好用又安全的分享方式。

第三步:分享

ClipBucket,那个号称开源 YouTube 克隆却根本装不上的项目

我尝试的第一个方案是ClipBucket,它自称是一款可自行托管的开源 YouTube 克隆。

GitHub 上的 ClipBucket 仓库

ClipBucket 是一款用户可自行托管的 YouTube 开源克隆(理论上)。

令人费解的是,ClipBucket 竟然没有提供任何安装说明。我只好参考一份第三方指南,用服务器配置管理工具 Ansible 将安装过程自动化

困难之一在于,ClipBucket 的安装脚本完全就是坏的。当时我还是谷歌员工,没法给一个 YouTube 克隆提交补丁,但我提交了一份缺陷报告,里面已经把修复方法写得很清楚。几个月过去了,他们始终没有承认这个问题。相反,每发布一个新版本,还会引入更多导致安装失败的错误。

ClipBucket 采用的是咨询服务模式——代码免费发布,向需要部署帮助的客户收费。慢慢地我才意识到,靠付费安装支持赚钱的公司,大概对让用户自助部署并不怎么上心。

MediaGoblin,更现代的替代方案

在 ClipBucket 上折腾了几个月、屡屡受挫后,我重新审视了市面上的可选方案,找到了MediaGoblin

MediaGoblin 官网首页

MediaGoblin 是一个可自行托管的媒体分享平台。

MediaGoblin 有很多值得称道的地方。与 ClipBucket 那让人不敢恭维的 PHP 不同,MediaGoblin 是用 Python 写的,而我对这门语言相当熟悉。它提供了命令行界面,让视频上传的自动化变得很容易。最棒的是,MediaGoblin 提供了 Docker 镜像,彻底省去了安装时的各种猜测和折腾。

Docker 是一项能让开发者为应用构建自包含运行环境、实现随处运行的技术。我在许多项目中都重度依赖它。

重新打包 MediaGoblin 的 Docker 镜像,竟然如此之难

我本以为有了 MediaGoblin 的 Docker 镜像,部署会变得轻而易举。结果并非如此。

我需要的两个功能在预构建镜像里都没有:

  • 身份验证
    • MediaGoblin 默认是公开访问的,我需要一种方式来防止陌生人访问网站。
  • 转码
    • 每次上传视频时,MediaGoblin 都会尝试重新编码以获得最佳的流媒体效果。对于本身就适合流媒体播放的视频,这一步不仅会降低画质,还会浪费计算资源。
    • MediaGoblin 提供了跳过转码的配置选项,但现有的 Docker 镜像却无法进行配置。

没关系,Docker 镜像是开源的,所以我可以自己重新构建

遗憾的是,这个 Docker 镜像已经无法基于当前的MediaGoblin 仓库构建成功。我尝试将其同步到上一次构建成功的对应版本,但同样失败了。即便我用的是完全相同的代码,MediaGoblin 的外部依赖也已经悄然变化,导致构建失败。几十个小时后,在一次次忍受 MediaGoblin 长达 10 多分钟的构建过程之后,我终于让它跑了起来。

几个月后,同样的事情又发生了。在过去两年里,MediaGoblin 频繁变动的依赖已经好几次弄坏了我的构建,就在写这篇文章时又出现了一次。我最终自己 fork 了 MediaGoblin把所有依赖都硬编码为明确的版本号。换句话说,我不再含糊地声明 MediaGoblin 兼容 celery >= 3.0 的任意版本,而是让它依赖于 celery 的4.2.1 版本,因为我已经在这个版本上测试过 MediaGoblin。看起来 MediaGoblin 需要一套可复现构建的机制,但我还没来得及去做。

总之,经过数小时的折腾,MediaGoblin 终于到了可以在 Docker 中构建和调整的地步。接下来,跳过不必要的视频转码以及添加 Nginx 来做身份验证就变得顺理成章了。

第四步:托管

在本地机器上让 MediaGoblin 通过 Docker 跑起来之后,下一步就是把这套环境部署到云服务器上,好让家人也能访问这些视频。

MediaGoblin 与视频存储难题

有不少平台可以直接接收应用的 Docker 镜像,并将其托管到一个可公开访问的网址上。麻烦在于,除了 MediaGoblin 应用本身,还有 33 GB 的视频文件需要分享。把它们硬编码进 Docker 镜像倒是可行,但既笨拙又难看——哪怕只改一行配置文件,也得重新部署 33 GB 的数据。

用 ClipBucket 的时候,我是用gcsfuse 解决这个问题的,这是一个能让操作系统把 Google Cloud Storage 上的目录当作本地文件路径来挂载的工具。我把视频文件放到 Google Cloud Storage 上,再用 gcsfuse 让 ClipBucket 以为它们就是本地文件。

不同之处在于,ClipBucket 运行在完整的虚拟机里,而 MediaGoblin 运行在 Docker 容器中。在 Docker 下挂载云存储文件要复杂得多。我花了几十个小时才解决各种坑,还为此写了一篇专门的博文

MediaGoblin + Docker + gcsfuse 架构图

将 MediaGoblin 与 Google Cloud Storage 集成的初始架构,记录在我的2018 年博文

经过数周的努力,终于让所有组件协同工作起来。无需改动 MediaGoblin 的任何代码,我就成功“骗”过它,让它把媒体文件读写到 Google Cloud Storage 上。

唯一的问题是,这让 MediaGoblin 慢到了无法使用的地步。加载首页的视频缩略图要足足 20 秒。看视频时如果快进,MediaGoblin 会卡住整整 10 秒才恢复播放。

根本原因在于,视频和图片文件到用户手里要走一条漫长而迂回的路径:从 Google Cloud Storage 经由 gcsfuse 到 MediaGoblin,再到 Nginx,最后才到用户浏览器。gcsfuse 是主要的瓶颈,它本来就不是为速度优化的。它在项目主页上就明确警告过延迟很高:

来自 gcsfuse GitHub 仓库的延迟警告

gcsfuse 文档中关于性能缓慢的警告

理想情况下,浏览器应该直接从 Google Cloud Storage 获取文件,绕过所有中间层。但如何在不深入 MediaGoblin 代码、不添加复杂的 Google Cloud Storage 集成逻辑的情况下做到这一点呢?

Nginx 的 sub_filter 技巧

好在,我找到了一个简单但有点不太优雅的解决方案。我在 Nginx 的 default.conf 文件中添加了这样一条过滤规则

sub_filter "/mgoblin_media/media_entries/" "https://storage.googleapis.com/MY-GCS-BUCKET/media_entries/";
sub_filter_once off;

在我的架构中,Nginx 作为终端用户与 MediaGoblin 之间的代理。上述指令会让 Nginx 在将 MediaGoblin 的所有 HTML 响应转发给用户之前,先做一次查找替换。它会把所有指向 MediaGoblin 上媒体文件的相对路径,替换成 Google Cloud Storage 的 URL。

例如,MediaGoblin 生成的 HTML 是这样的:

<video width="720" height="480" controls autoplay>
  <source
    src="/mgoblin_media/media_entries/16/Michael-riding-a-bike.mp4"
    type="video/mp4"
  />
</video>

Nginx 会把响应改成这样:

<video width="720" height="480" controls autoplay>
  <source
    src="https://storage.googleapis.com/MY-GCS-BUCKET/media_entries/16/Michael-riding-a-bike.mp4"
    type="video/mp4"
  />
</video>

整体架构如下:

MediaGoblin + Docker + Nginx 重写响应至 GCS 的架构图

Nginx 重写来自 MediaGoblin 的响应,使客户端可以直接从 Google Cloud Storage 获取媒体文件。

这个方案巧妙之处在于完全无需修改 MediaGoblin 的代码。仅仅两行 Nginx 指令,就让 MediaGoblin 和 Google Cloud Storage 无缝集成在一起,尽管两者彼此完全无感知。

注意:此方案要求 Google Cloud Storage 上的文件设置为公开可读。为降低未授权访问的风险,我使用了一个又长又随机的存储桶名称(例如 mediagoblin-39dpduhfz1wstbprmyk5ak29),并确保该存储桶的访问控制策略禁止未授权用户列出目录内容。

最终成果

到此,我已经有了一套完整可用的方案。MediaGoblin 在 Google Cloud Platform 上的独立容器中稳定运行,这意味着我不需要频繁打补丁或升级。整个流程都已自动化且可复现,推送改动或回滚到旧版本都很容易。

家人很喜欢这种轻松浏览视频的体验。有了 Nginx 的性能优化技巧,浏览速度就跟看 YouTube 一样流畅。

浏览界面是这样的:

MediaGoblin 浏览界面

我家家庭视频分享服务器的浏览界面

点击缩略图后,会进入这样的页面:

MediaGoblin 播放视频的截图

在媒体服务器上观看单个片段

经过数年的努力,终于为家人实现了当初设想的那种类似 YouTube 的视频浏览体验,成就感十足。

番外:把成本降至每月 1 美元以下

家庭录像这种东西,往往几个月才看一次。我们全家一年加起来也就访问这个网站大约 20 小时,但我的服务器却 7×24 小时都在运行。我每月要付 15 美元,服务器却有 99.7% 的时间都在空转。

2018 年底,谷歌发布了Cloud Run。它的杀手锏是能够以足够快的速度启动 Docker 容器来响应 HTTP 请求。这让服务器可以在待机状态下等待,只有当有人访问你的网址时才运行。对于像我这种访问频率极低的应用,这能把成本从每月 15 美元降到每年几美分。

由于一些我已记不清的原因,Cloud Run 无法运行我的 MediaGoblin 镜像。不过,Cloud Run 的出现提醒了我,Heroku 也提供类似的免费服务,而且它们的工具比谷歌的要好用得多。

有了免费的应用服务器,我唯一的开销就是数据存储。谷歌标准区域存储的价格是每 GB 2.3 美分,视频合集占 33 GB,所以我每月只需支付 0.77 美元。

来自 Google Cloud Platform 的 0.77 美元账单

整套方案的成本仅为每月 0.77 美元。

给准备尝试者的建议

整个过程显然花了我很长时间,但我希望这篇文章能帮后来者省去 80% 到 90% 的数字化与分享家庭录像的工作量。下一节有我这套方案的详细操作指南,涵盖各种技术细节,不过这里先给出一些关于数字化和分享家庭录像的通用建议:

  • 在原始采集和剪辑阶段尽可能多地记录元数据。
    • 录像带上的标签往往包含宝贵信息。
    • 记录每个片段来自哪盘带子、顺序如何。
    • 留意片段中关于录制日期的任何线索。
  • 考虑将原始采集工作外包给专业机构。
    • 你很难在质量上匹敌专业的视频数字化公司,而且自己做的成本也极高。
    • 但要避开一家叫 EverPresent 的公司(想了解详情可以给我发邮件)。
  • 如果自己采集,请准备充足的磁盘空间。
    • 未压缩的标清视频采集结果约为每分钟 100–200 MB。
    • 我把所有东西都存在了 10 TB 的Synology DS412+ 上。
  • 用与应用无关的格式记录元数据。
    • 比如片段描述、时间码、日期等。
    • 如果保存在特定应用的格式里(更糟的是直接丢掉),一旦换方案就无法复现这些工作。
    • 观看视频剪辑时会发现很多有用的元数据,不记录下来就会丢失。
      • 视频里发生了什么?
      • 里面有谁?
      • 是什么时候录制的?
  • 标记你最喜欢的片段。
    • 说实话,大多数家庭录像都挺无聊的。
    • 我会给最喜欢的片段打上“精选”标签,想看有趣视频时就直接浏览这些。
  • 尽早搭建端到端完整方案。
    • 我当时是先把所有带子全部采集完,再统一剪辑,以此类推。
    • 要是能先拿一盘带子把分享所需的全流程跑通就好了,这样能更早看清前期决策对最终成果的影响。
  • 尽量减少转码次数。
    • 每次编辑或重新编码都会降低画质。
    • 以尽可能高的质量采集原始素材,然后只转码一次,转成浏览器可直接播放的格式。
  • 用最简单的方案来分享视频片段。
    • 回过头看,MediaGoblin 对于“生成网页来展示一组固定不变的视频文件”这个并不复杂的场景来说,过于笨重了。
    • 如果重来一次,我会用静态网站生成器,比如 HugoJekyllGridsome
  • 做一个混剪。
    • 把多段家庭录像中的精彩瞬间剪成混剪视频很有意思。
    • 混剪的关键在于配乐。The National 的《Slow Show》做混剪配乐非常棒,但好像还没什么人发现。

端到端完整流程详解

如果你想了解我是如何一步步实现这些细节的,我写了一份教程,完整展示了从头到尾的全部工作流,其中包含了复现整个过程所需的所有源代码和命令。


插图:Loraine Yow。

特别感谢家人的支持——感谢他们起初记录下这一切,允许我分享其中的部分片段与画面,并在整个过程中给予我莫大的支持。

本文章由 muse-spark-1.2-contributor 进行翻译

评论