视频流媒体架构图:上传、转码、封装与分发

从上传到播放的视频流媒体架构图:分片上传、转码队列、自适应码率阶梯、HLS 与 DASH 封装、DRM 和 CDN。

· 创始人兼开发者

发布于

视频流媒体架构图:上传、转码、封装与分发

“设计 YouTube”是一道很受欢迎的系统设计题,因为有意思的部分并不在人们以为的地方。Web 服务器很普通。真正的难点在于:把几个 GB 的数据从手机可靠地送进存储,把一次上传变成能在各种设备和网络上播放的十几个版本,再通过 CDN 分发这些字节,让你自己的服务器几乎碰不到它们。

本文按顺序走一遍这条流水线:上传、转码、封装、分发。最后给出一张可以改造的架构图,以及值得写在图上的决策。

是两个系统,而不是一个

视频平台其实是共享一个数据库的两个系统:

  • 摄取: 创作者上传,平台转码并封装。延迟以分钟计,没人在意。
  • 播放: 观众按下播放,第一帧必须在几秒内出现,而且不能卡顿。

把它们画成两条泳道。它们按不同的信号扩容(每小时上传量与同时在线观众数),出故障的方式也不同。

上传:绝不经过你的 API 服务器

视频文件很大,移动网络会断。由此得出两条规则:

  1. 直接上传到对象存储。 上传 API 对创作者做认证,创建视频记录,并返回预签名 URL。客户端把字节直接发送到存储桶。你的 API 从不代理几个 GB 的数据。
  2. 支持断点续传。 使用分片上传,连接断开时只需重试失败的那一片。例如 Amazon S3 分片上传 会独立上传各个分片,并在客户端完成上传时把它们组装起来。

上传完成后,存储事件(或客户端的完成调用)会把一个转码任务放进队列。

转码:一次上传,生成一套自适应码率阶梯

播放器会根据带宽切换清晰度,所以每个视频都要编码多次:这就是 自适应码率(ABR)阶梯。典型的阶梯从 240p 或 360p 左右一直到 1080p 或 4K,每一级都有自己的码率。

转码非常吃 CPU,而且极易并行:

  • 在关键帧处把源文件切成几秒长的片段。
  • 把每个片段、每个版本作为独立任务,在一组工作进程上编码,通常使用 FFmpeg。
  • 把结果拼接起来,或者直接保留为分片。

这样,一部两小时的电影只需几分钟就能转码完成,而不是几个小时;失败的任务也只需重试一个片段。用视频、版本和片段作为任务的键,让任务保持幂等,重试时覆盖而不是产生重复。

所有内容共用一套阶梯会浪费码率:静止的幻灯片演示所需的码率远低于一场足球比赛。Netflix 推广了按片编码(per-title encoding),根据每个视频的复杂度为它选择阶梯。你可以先用固定阶梯,以后再改为按片编码。

封装:HLS、DASH 与 CMAF

播放器不会下载一个巨大的文件。它们读取一个列出各版本和分片的清单,然后通过普通 HTTP 获取每段几秒长的分片。

格式 标准 常见播放器
HLS RFC 8216 Apple 设备,以及通过 hls.js 等播放器的大多数浏览器
MPEG-DASH ISO/IEC 23009-1 Android、智能电视,以及通过 dash.js 和 Shaka Player 的浏览器
CMAF ISO/IEC 23000-19 一套分片 MP4,同时用于 HLS 和 DASH 清单

CMAF 正是封装器重要的原因:有了它,你只需存一套分片、写两份清单,而不是每个视频都存两份。短分片起播更快、切换清晰度更早;长分片的请求数更少。Apple 的 HLS 制作规范 给出了分片时长、关键帧和码率阶梯的具体建议。

付费内容还要加上 DRM。封装器用通用加密(CENC)对分片只加密一次,各平台的 DRM 系统(Widevine、FairPlay、PlayReady)再通过许可证服务器向播放器发放许可证。

分发:活儿由 CDN 来干

分片是静态文件,这正是关键所在:CDN 在离观众很近的地方缓存它们,一个热门视频几乎完全由边缘缓存提供。源站是存放各版本的存储桶,通常放在源站护盾(origin shield)后面,这样 50 个边缘节点的缓存未命中,到了存储那里只变成一次请求。

播放 API 保持精简:检查观众是否有权观看该视频,返回清单 URL(私有内容则带签名),并记录播放事件供分析使用。

直播是另一条流水线

直播复用封装和 CDN,但替换了摄取部分。编码器通过 RTMP 或 SRT 等协议把流推送到摄取服务器,服务器实时转码出整套阶梯,每隔几秒写出新的分片。延迟来自分片时长加上播放器的缓冲;HLS 和 DASH 的低延迟模式会在分片完成之前就交付其中一部分,以接近几秒的延迟。把直播和点播作为两条独立的流水线,共享 CDN、播放器和元数据。

架构图

创作者获取上传 URL,把分片上传到原始文件存储桶,存储桶把转码任务放入队列。工作进程编码各个版本;封装器把分片写入另一个存储桶,并把视频标记为就绪。观众获取清单,通过 CDN 播放,并获取 DRM 许可证。

13 个组件 · 12 个连接

在 Archboard 中打开
图表内容
  • Creator
  • Upload API
  • Raw uploads
  • Transcode jobs
  • Transcode workers
  • Packager
  • Renditions
  • Video metadata
  • Viewer
  • Playback API
  • CDN
  • DRM license server
  • 1. upload URL
  • 2. multipart
  • 3. done
  • ladder
  • segments
  • ready
  • manifest URL
  • cache miss
  • license
一个分为摄取流水线和播放路径的视频平台。上传直接进入存储,队列驱动转码生成自适应码率阶梯,封装器写出 HLS 和 DASH 分片,观众通过 CDN 观看。

这张图的读法如下:

  • 上方泳道,摄取: 创作者从上传 API 获取上传 URL(1),把分片直接上传到原始上传存储桶(2),上传完成后转码任务进入队列(3)。工作进程编码整套阶梯,封装器把分片写入版本存储桶,并在元数据存储中把视频标记为就绪。
  • 下方泳道,播放: 观众向播放 API 请求清单 URL,然后从 CDN 获取分片播放。缓存未命中时从版本存储桶读取。
  • DRM 画成点线,因为只有受保护的内容才需要它:播放器从许可证服务器获取许可证。
  • Markdown 块 展示了一个示例阶梯。你的数字应该来自你的内容和观众的带宽。

图上应该画什么

  • 字节怎么流动。 画出上传和分片流向存储和 CDN,而不是经过你的 API。
  • 任务单元。 “每个版本、每个片段一个任务”能让评审者明白重试和并行是怎么工作的。
  • 格式与保护。 写明 HLS、DASH 或 CMAF,如果用了 DRM 也要写上。
  • 就绪信号。 封装完成之前视频无法播放;标出这个状态存在哪里。

同样的“队列加工作进程”结构也出现在 事件驱动架构一文 中。在云的一侧,AWS 多区域架构图 介绍了如何从多个区域提供服务,无服务器架构图 则展示了存储事件如何触发函数,而不是使用一组工作进程。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 添加 C4 模型节点包中的两个 人员 节点,分别表示创作者和观众。
  3. 为上传 API、转码工作进程、封装器和播放 API 添加 容器 节点,再添加一个 数据库 存放视频元数据。
  4. 添加云节点包中的两个 对象存储 节点和一个 CDN,数据节点包中的一个 队列 用于转码任务,以及一个 外部系统 表示 DRM 许可证服务器。
  5. 把摄取画在上方泳道、播放画在下方泳道,在连线上标出上传步骤的编号,再添加一个基础节点包中的 Markdown 块,写上你的码率阶梯。

这个示例共用 13 个图表节点,在 Free 套餐的限制之内。

常见问题

什么是自适应码率阶梯? 一个视频被编码成的一组版本,每个版本有自己的分辨率和码率。播放器选择观众网络能承受的最佳版本,并随着带宽变化而切换。

应该用 HLS 还是 DASH? 通常两个都用。用 CMAF 分片封装一次,写一份 HLS 清单和一份 DASH 清单,这样无需存两份,就能同时覆盖 Apple 设备和其他播放器。

为什么要直接上传到对象存储? 大文件经过你的 API 服务器会占用连接和内存,连接一断,上传就得从头再来。预签名的分片上传直接进入存储,并能逐片续传。

转码如何并行? 在关键帧处把源文件切成片段,把每个片段、每个版本作为独立任务编码,然后拼接或封装结果。

视频需要 CDN 吗? 只要观众不是极少数,就需要。分片是静态文件,CDN 会从离观众近的缓存响应重复请求,而不是每次都访问你的存储。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。