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

Michael Lynch

我历时八年将45盘录像带数字化的经历(下篇)

上篇中,我讲述了自己将老家庭录像采集为数字格式并拆分成独立场景的艰苦历程。处理完所有片段后,我希望浏览它们的体验能像在 YouTube 上查找视频片段一样简单。但这些视频是我家人的私人回忆,真正的 YouTube 太过公开。我需要一种既便于使用又安全可靠的分享方式。

第三步:分享

ClipBucket:你实际上无法安装的开源 YouTube 克隆

我尝试的第一个解决方案是ClipBucket。它宣称自己是一个可以自行托管的开源 YouTube 克隆。

ClipBucket 在 GitHub 上的代码仓库

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

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

其中一部分困难在于,ClipBucket 的安装脚本彻底坏掉了。当时我还是一名Google 员工,不能为一个 YouTube 克隆项目提交补丁,但我提交了一份错误报告,其中的问题本应让修复方式一目了然。几个月过去了,他们始终没有承认这个问题。相反,他们在每次发布新版本时都引入更多破坏性错误。

ClipBucket 采用咨询模式运营——他们免费发布代码,再向需要部署帮助的客户收费。慢慢地我才意识到,一家靠付费安装支持赚钱的公司,可能并不太热衷于让用户自行部署。

MediaGoblin:更现代的替代方案

在被 ClipBucket 折磨了几个月后,我重新评估了可用的方案,找到了 MediaGoblin

MediaGoblin 的主页

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

MediaGoblin 有不少优点。ClipBucket 使用难看的 PHP,而 MediaGoblin 是用 Python 编写的,这是我非常熟悉的语言。它还提供了一个命令行界面,便于自动化上传视频。最棒的是,MediaGoblin提供了一个 Docker 镜像,这样就不必再猜测如何安装。

Docker 是一种技术,允许开发者为应用构建一个可以在任何地方运行的自包含环境。我在许多项目中都高度依赖它。

重新制作 MediaGoblin Docker 镜像的意外难度

我原以为 MediaGoblin 的 Docker 镜像会让部署变得轻而易举。事实嘛,并没有那么简单。

预构建镜像中缺少我需要的两个功能:

  • 身份验证
    • MediaGoblin 默认是公开的,因此我需要一种阻止陌生人访问网站的方法。
  • transcoding(转码)
    • 每当你上传视频时,MediaGoblin 都会尝试重新编码,以实现最佳串流效果。对于本来就适合串流的视频,这一步会降低画质并浪费处理资源。
    • MediaGoblin 提供了跳过转码的配置选项,但现有的 Docker 镜像无法配置。

这没问题。Docker 镜像是开源的,所以我可以自己重新构建

可惜,这个 Docker 镜像已经无法基于当前的 MediaGoblin 代码仓库构建。我尝试将它同步到与上次成功构建相匹配的版本,但同样失败了。即使我使用的是完全相同的代码,MediaGoblin 的外部依赖也已经悄悄发生了变化,导致构建失败。经过几十个小时的努力,在一次又一次等待 MediaGoblin 那超过10分钟的构建过程后,我终于让它运行起来了。

几个月后,同样的事情再次发生。在过去几年里,MediaGoblin 依赖项的不断变化已经多次破坏我的构建,包括我写这篇文章时又发生了一次。最后,我创建了自己的 MediaGoblin 分支,将所有依赖项硬编码为明确的版本。换句话说,MediaGoblin 不再含糊地声明可以兼容 celery >= 3.0 的任何版本,而是依赖 celery 的4.2.1 版本,因为我已经针对该版本测试过 MediaGoblin。MediaGoblin 似乎需要一种可复现构建机制,但我还没有着手实现。

不管怎样,经过数小时的挣扎,MediaGoblin 终于达到了可以在 Docker 中构建和调整的状态。从那以后,跳过不必要的视频转码添加 Nginx来实现身份验证就很简单了。

第四步:托管

MediaGoblin 在我的本地机器上通过 Docker 运行起来后,下一步就是将这套配置部署到云服务器上,让家人能够访问视频。

MediaGoblin 与视频存储问题

有很多平台可以接收应用的 Docker 镜像,并将其托管在一个公开可访问的 URL 上。问题在于,除了 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 响应传递给最终用户之前,对所有响应执行搜索和替换。Nginx 会替换 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小时,但我的服务器却全天候运行。我每月支付15美元,租用一台99.7%的时间都处于闲置状态的服务器。

2018年底,Google 发布了 Cloud Run。它的杀手级功能是能够快速启动 Docker 容器,以便响应 HTTP 请求。这样,服务器可以处于待机模式,只有有人访问 URL 时才运行。对于我这种访问频率很低的应用,这让成本从每月15美元降到了每年几美分。

由于一些我已经记不清的原因,Cloud Run 无法运行我的 MediaGoblin 镜像。但 Cloud Run 的存在提醒了我,Heroku 提供类似的免费服务,而且他们的工具比 Google 的用户友好得多。

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

Google Cloud Platform 开具的0.77美元账单

整套方案的成本每月只有0.77美元。

给准备尝试这件事的人的建议

显然,这个过程花了我很长时间,但我希望这篇文章能帮其他人省下数字化和分享家庭录像所需工作量的80%到90%。下一节有一份关于我的解决方案具体细节的详细教程,但这里先给出一些数字化和分享家庭录像的一般建议:

  • 在原始采集和剪辑阶段尽可能多地记录metadata(元数据)。
    • 录像带上的标签通常包含有价值的信息。
    • 记录每个片段来自哪盘录像带,以及在录像带中的顺序。
    • 注意片段中任何能够提示录制日期的线索。
  • 考虑将原始采集外包给专业人士。
    • 你自己要达到视频数字化公司的质量,极其困难且成本高昂。
    • 但要避开一家名为 EverPresent 的公司(如果你想知道详情,可以给我发邮件)。
  • 如果自己采集,要购买充足的磁盘空间。
    • 未压缩的视频采集文件,每分钟标准清晰度视频约占用100到200 MB。
    • 我把所有内容都存储在自己的10 TB Synology DS412+ 上。
  • 以与应用无关的格式记录元数据。
    • 片段描述、时间码、日期等。
    • 如果将它们保存在特定应用的格式中(更糟糕的是直接丢弃),那么一旦决定采用不同的方案,就无法复现这项工作。
    • 剪辑时观看视频,你会看到许多有用的元数据。不记录下来就会丢失。
      • 视频里发生了什么?
      • 里面有哪些人?
      • 是什么时候录制的?
  • 标记你最喜欢的片段。
    • 说实话,大多数家庭录像都相当无聊。
    • 我会给喜欢的片段加上“精选”标签,想看有趣的视频时就浏览这些片段。
  • 尽快构建端到端的完整解决方案。
    • 我当时试图先采集完所有录像带,再剪辑完所有录像带,等等。
    • 我真希望一开始就只拿一盘录像带,完成分享它所需的全部工作。这样我就能看到流程早期的决定会如何影响最终结果。
  • 尽量减少转码。
    • 每次剪辑或重新编码片段,都会降低画质。
    • 以尽可能高的质量采集原始素材,然后将每个片段只转码一次,转换为浏览器能够原生播放的格式。
  • 分享视频片段时,采用尽可能简单的方案。
    • 现在回头看,对于生成网页来展示一组不会变化的视频文件这一并不复杂的场景,MediaGoblin 是过于复杂的工具。
    • 如果重新开始,我会使用 HugoJekyllGridsome 这样的静态网站生成器。
  • 制作混剪视频。
    • 混剪视频是汇集多段家庭录像中精彩瞬间的有趣方式。
    • 混剪的关键在于音乐。The National 的《Slow Show》非常适合混剪,似乎还没有其他人意识到这一点。

我的完整流程教程

如果你想了解我是如何具体完成这一切的,我制作了一份教程,展示了从头到尾的完整工作流程。其中包括复现我这套流程所需的全部源代码和命令。


插图:Loraine Yow(洛林·尤)。

特别感谢我的家人,感谢他们允许我分享其中一部分片段和静帧,感谢他们最初记录下这一切,也感谢他们在整个过程中给予我的支持。

原文由 Michael Lynch 发布

本文章由 openai/gpt-5.6-luna 进行翻译