使用媒体源扩展按需流媒体

介绍 MSE

媒体源扩展 (MSE) 是所有主流浏览器中都可用的 Web API 的一个新补充。此 API 允许在浏览器中直接进行视频 自适应码率流,无需插件。以前我们可能使用 RTSP (实时流协议) 和 Flash 等专有解决方案,现在我们可以使用更简单的协议(如 HTTP)来获取内容,以及使用 MSE 来平滑地拼接不同质量的视频片段。

所有支持 HTMLMediaElement 的浏览器(如音频和视频标签)已经对媒体资产的后续片段进行字节范围请求。问题是,它取决于每个浏览器对媒体引擎的实现来决定何时以及获取多少。在没有暂停、间隙、闪烁、点击或爆音的情况下,拼接或交付不同质量片段的平滑播放也很困难。MSE 为我们提供了在应用程序级别获取和播放内容的更细粒度控制。

为了开始流式传输,我们需要弄清楚如何将我们的资产转码为浏览器媒体引擎的有效字节格式,确定 MSE 提供的哪些抽象,以及弄清楚如何指示浏览器播放它们。

拥有多个分辨率的内容使我们能够在保持恒定视口大小的情况下切换它们。这被称为上采样,是视频游戏中实时渲染以满足所需帧时间的常用技术。通过切换到更低质量的视频分辨率,我们可以满足带宽限制,但以保真度为代价。保真度损失会导致诸如混叠之类的伪像,其中曲线看起来参差不齐且块状。Netflix 订阅者在观看高峰时段经常可以看到这种技术。

我们不需要像 RTSP 这样的高级协议来处理带宽估计,我们可以使用更简单的网络协议(如 HTTP)并将高级逻辑向上提升到应用程序逻辑中。

转码

我推荐的工具,ffmpegBento4,都是免费的开源软件 (FOSS)。ffmpeg 是我们的转码瑞士军刀,而 Bento4 是一个用于处理 mp4 的优秀工具集合。虽然我喜欢 webm/vp8-9/opus 等无许可证编解码器,但当前浏览器对这些容器和编解码器的支持相当糟糕,因此在本帖中,我们将只处理 mp4/h.264/aac。我正在使用的这两个工具都是命令行实用程序;如果您在资产管道中有不错的 GUI 工具想要推荐给我们的读者,请在下面的评论中告诉我们。

我们将从某个文件的母版开始,最后将其转码为多个分辨率较小的文件,然后将分辨率较小的完整文件分割成一堆小文件。一旦我们拥有了一堆小文件(想象一下将你的视频分割成一堆 10 秒的片段),客户端就可以使用更高级的启发式方法来获取首选的下一个片段。

MSE multiple resolutions

我们分辨率较低的母版资产副本

适当的碎片化

在使用 mp4 和 MSE 时,了解 mp4 文件的结构非常有用,这样元数据就会分散在容器的各个部分中,并且分散在实际的音频/视频流中,而不是聚集成一堆。这在 ISO BMFF 字节流格式规范,第 3 节 中有说明

“ISO BMFF 初始化片段 在本规范中定义为一个文件类型框 (ftyp) 后面跟着一个电影头框 (moov)”。

这一点非常重要:简单地将 ffmpeg 转码到 mp4 容器中不会得到预期的格式,因此在尝试使用支持 MSE 的浏览器播放时会失败。要检查你的 mp4 是否已正确碎片化,可以在你的 mp4 上运行 Bento4 的 mp4dump

如果你看到类似的内容

  $ ./mp4dump ~/Movies/devtools.mp4 | head
  [<span class="red">ftyp</span>] size=8+24
    ...
  [<span class="red">free</span>] size=8+0
  [<span class="red">mdat</span>] size=8+85038690
  [<span class="red">moov</span>] size=8+599967
    ...

那么你的 mp4 无法播放,因为 [ftyp] “原子” 并没有紧跟着 [moov] “原子”。正确碎片化的 mp4 看起来像这样——

  $ ./mp4fragment ~/Movies/devtools.mp4 devtools_fragmented.mp4
  $ ./mp4dump devtools_fragmented.mp4 | head
  [<span class="lime">ftyp</span>] size=8+28
    ...
  [<span class="lime">moov</span>] size=8+1109
    ...
  [<span class="orange">moof</span>] size=8+600
    ...
  [<span class="orange">mdat</span>] size=8+138679
  [<span class="orange">moof</span>] size=8+536
    ...
  [<span class="orange">mdat</span>] size=8+24490
    ...
  ...

——其中 mp4fragment 是另一个 Bento4 实用程序。正确碎片化的 mp4 具有紧跟着 [moov] 的 [ftyp],然后是后续的 [moof]/[mdat] 对。

使用 -movflags frag_keyframe+empty_moov 标志在用 ffmpeg 转码到 mp4 容器时,可以跳过对 mp4fragment 的需要,然后用 mp4dump 检查

  $ ffmpeg <span class="hljs-attribute">-i</span> bunny<span class="hljs-built_in">.y4m</span> -movflags frag_keyframe+empty_moov bunny<span class="hljs-built_in">.</span>mp4
创建多个分辨率

如果我们想要切换分辨率,我们可以将我们已碎片化的 mp4 传递到 Bento4 的 mp4-dash-encode.py 脚本中,以获得我们视频的多个分辨率。这个脚本将启动 ffmpeg 和其他 Bento4 工具,因此请确保它们都可以在你的 $PATH 环境变量中找到。

$ python2.7 mp4-dash-encode.py -b 5 bunny.mp4
$ ls
video_00500.mp4 video_00875.mp4 video_01250.mp4 video_01625.mp4 video_02000.mp4
分割

现在我们有 5 个不同版本的视频,它们具有不同的比特率和分辨率。为了能够在播放过程中根据我们不断变化的有效带宽轻松地在它们之间切换,我们需要对这些副本进行分割并生成一个清单文件来简化客户端上的播放。我们将创建一个 媒体演示描述 (MPD) 样式的清单文件。这个清单文件包含有关片段的信息,例如获取必需片段的阈值有效带宽。

Bento4 的 mp4-dash.py 脚本可以接受多个输入文件,执行分割,并发出大多数 DASH 客户端/库 理解的 MPD 清单。

$ python2.7 mp4-dash.py --exec-dir=. video_0*
...
$ tree -L 1 output
output
├── audio
│   └── und
├── stream.mpd
└── video
    ├── 1
    ├── 2
    ├── 3
    ├── 4
    └── 5

8 directories, 1 file

现在我们应该有一个文件夹,里面包含已分割的音频和已分割的不同分辨率的视频。

MSE & 播放

对于像音频或视频标签这样的 HTMLMediaElement,我们只需将 URL 分配给元素的 src 属性,浏览器就会处理获取和播放。对于 MSE,我们将使用 XMLHttpRequests (XHR) 自行获取内容,将响应视为 ArrayBuffer(原始字节),并将媒体元素的 src 属性分配给指向 MediaSource 对象的 URL。然后,我们可以将 SourceBuffer 对象附加到 MediaSource。

MSE 工作流程的伪代码可能如下所示

let m = new MediaSource
m.o<span class="hljs-function"><span class="hljs-title">nsourceopen</span> = <span class="hljs-params">()</span> =></span>
  let s = m.addSourceBuffer(<span class="hljs-string">'codec')</span>
  s.o<span class="hljs-function"><span class="hljs-title">nupdateend</span> = <span class="hljs-params">()</span> =></span>
    <span class="hljs-keyword">if</span> (numChunks === totalChunks)
      m.endOfStream()
    <span class="hljs-keyword">else</span>
      s.appendBuffer(nextChunk)
  s.appendBuffer(arrayBufferOfContent)
video.src = URL.createObjectURL(m)

这里有一个技巧可以获取文件的大小:使用 HTTP HEAD 方法进行 XHR。对 HEAD 请求的响应将具有内容长度标头,指定响应的正文大小,但与 GET 不同,它实际上没有正文。你可以使用它来预览文件的大小,而无需实际请求文件内容。我们可以简单地对视频进行细分,并在播放当前片段 80% 时获取下一个视频片段。这里有一个 该方法的演示 以及对 代码 的介绍。

注意:你需要使用最新的 Firefox 开发者版 浏览器来查看演示并测试代码。有关更多信息,请参阅下面的“兼容性”部分。来自 WebPlatform.org 文档的 MSE 简介 是另一个值得参考的资源。

我的 演示 有点简单,并且存在一些问题

  • 它没有显示如何在播放期间正确处理跳转。
  • 它假设带宽是恒定的(始终在播放上一个片段 80% 时获取下一个片段),但实际上并非如此。
  • 它从仅加载一个片段开始(可能最好先获取前几个片段,然后等待获取其余片段)。
  • 它不会在不同分辨率的片段之间切换,而是只获取一种质量的片段。
  • 它不会删除片段(MSE API 的一部分),虽然这在内存受限的设备上很有帮助。不幸的是,这要求你在向后跳转时重新获取内容。

这些问题都可以通过使用客户端上的更智能的逻辑来解决,这些逻辑可以利用 动态自适应 HTTP 流 (DASH)

兼容性

跨浏览器编解码器支持目前是一个混乱的故事;我们可以使用MediaSource.isTypeSupported检测编解码器支持。你传递isTypeSupported一个你想要播放的容器的 MIME 类型字符串。mp4 目前具有最佳的兼容性。显然,对于使用 Blink 渲染引擎的浏览器,MediaSource.isTypeSupported需要指定完整的编解码器字符串。为了找到这个字符串,你可以使用 Bento4 的 mp4info 工具。

./mp4info bunny.mp4| grep Codec
    Codecs String: avc1.42E01E

然后在我们的 JavaScript 中

if (MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')) {
// we can play this
}

—— 其中 mp4a.40.2 是低复杂度 AAC 的编解码器字符串,这是 mp4 容器中使用的典型音频编解码器。

一些浏览器目前也对某些域进行了白名单,以测试 MSE,或过分积极地缓存 CORS 内容,这使得测试非常困难。在测试时,请咨询你的浏览器以了解如何禁用 白名单CORS 缓存

DASH

使用我们之前创建的 MPD 文件,我们可以获取一个用 JavaScript 实现的高质量 DASH 客户端,例如 Shaka Playerdash.js。这两个客户端都实现了大量功能,但可能需要更多测试,因为各种浏览器的媒体引擎之间存在一些细微差异。像 Shaka Player 这样的高级客户端使用 三个到十个样本的指数移动平均值 来估计带宽,甚至可以让你指定自己的带宽估计器。

如果我们使用 跨域资源共享 (CORS) 启用,并向任一 DASH 客户端指向 http://localhost:<port>/output/stream.mpd,我们应该能够看到我们的内容正在播放。在 Shaka 中启用视频循环,或者在 dash.js 中点击 +/- 按钮,应该可以让我们看到内容质量的变化。为了获得更显著的变化,尝试编码比我们演示的五个更少的比特率。

Shaka Player in Firefox Dev Edition

Firefox 开发版中的 Shaka Player

dash.js running in Firefox Developer Edition

Firefox 开发版中的 dash.js

总结

在这篇文章中,我们了解了如何通过预处理和转码来准备用于按需流媒体的视频资产。我们还简要了解了 MSE API,以及如何使用更高级的 DASH 客户端。在接下来的文章中,我们将探讨使用 MSE API 进行直播内容流媒体,敬请关注。我建议你使用 Firefox 开发版来测试 MSE;我们正在努力改进我们的实现。

以下是一些探索 MSE 的额外资源


一条评论

  1. 大卫

    使用这个 api 进行直播内容流媒体有什么消息吗?

    2015 年 7 月 30 日 下午 3:23

本文的评论已关闭。