MP4 在 <video> 里,必须全量下载才能起播吗?—— moov、Range 与被误解的 FastStart

0 阅读8分钟

抛出问题: MP4 在 <video> 里,到底需不需要下载完再播?

答案是:不一定。取决于两件事:moov 放在文件的哪里,以及服务器是否支持 Range 请求。(moov 是 MP4 的元数据容器, 下文会介绍)

本文用实验, 逐一说清楚这两件事各自的作用——以及你可能一直误解的 FastStart 的真实价值。

最小必要知识

MP4 是什么?

MP4 是视频的封装格式, 遵循 MPEG-4 Part 14 协议。

MP4 的存储结构上有三个核心: ftypmdatmoov

  • ftyp : 声明格式, 兼容的规范, 内容对播放本身没影响, 但不能缺。
  • mdat : 真正的音视频数据。是文件里最大的 box , 但它是一段裸数据(没有索引), 单独拿到**mdat** 浏览器不知道哪个字节是第几帧、时间戳是多少、从哪里开始解码。
  • moov : 全局“目录”。记录所有视频的元信息(时长、轨道、编解码, 以及最关键的每一帧在mdat里的字节偏移量和时间戳)。浏览器要先拿到moov, 才知道怎么读**mdat**、从哪里开始解码。

所以 moov 的位置非常重要, 理论上, 默认moov 在文件尾,浏览器不拿到结尾就不知道怎么解析前面的数据; 如果 moov 在文件头,浏览器下载开头就能开始播;

image.png

为什么 moov 在末尾?

传统上大部分 MP4 视频资源的 moov 都是在末尾的, 也是有一定的历史原因。

  • 采集设备(相机)内置编码器, 录制时实时压缩每一帧到mdat中, moov里记录的是是每一帧的字节偏移量和时间戳, 这些信息等所有帧都编码完毕, 才最终确定, 所以自然而然的放到最后一帧中。
  • 早期, 视频是本地播放的, 读文件可以随机寻址(系统调用层面直接跳到最后读moov信息)。moov放到哪里无所谓。

Web 兴起后, HTTP 下载变成顺序读取, moov在末尾的代价才暴露出来。

HTTP/1.0 200 和 HTTP/1.1 206 和 Range 请求头

HTTP/1.0 1996年发布, 定义了响应成功的默认状态, 服务器返回 200 状态码 。

HTTP/1.1 1997年发布, 定义了服务器处理 Range 请求头, 返回 206 状态码。(虽然是更新了0.1, 但实际上是个大版本迭代。)

# Range请求头格式, 代表告知服务器返回文件的哪一部分(单位:字节)
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, …, <range-startN>-<range-endN>
Range: <unit>=-<suffix-length>

早期, 服务器和 CDN 只实现了 HTTP/1.0 的部分,200 是默认行为,Range 支持是需要额外实现的——这也解释了为什么至今还有服务器不支持 Range

本文的术语约定

接下来, 我会将moov后置的 MP4 资源, 叫做“传统 MP4”, 将moov前置的 MP4 叫做“FastStart MP4”, 注意我后面还会提到结构完全不同的“fmp4(Fregment MP4)” , 注意不要混淆。

本文中的快速起播,指浏览器无需下载完整文件、拿到 moov 元数据后即可解码首帧开始播放。与之对应的是全量下载完后起播——视频数据必须完整到达才能播放。

实验过程

实验目标

调查 MP4 在 是否是必须下载完才能播。

实验准备

  1. 一个约 60MB 的 MP4 视频资源的两个版本。
    • 传统MP4 : bigmp4-moov-end.mp4 moov 在尾部。
    • FastStart MP4 : bigmp4-moov-first.mp4 moov 在头部, 由 ffmpeg 转码。
  2. 一个 HTTP 服务, 可以提供 200 和 206 的 MP4 资源的响应, 由 node 提供。
  3. mp4box.js, 用于识别 MP4 封装格式内部的具体结构。
  4. 一个 chromium 内核的浏览器。

接下来实验的展开是: 两个变量 × 两种状态 = 四种组合的播放器行为

200206 Range
传统 MP4??
FastStart MP4??

为了简化实验和消除无关变量, 不再专门启动一个 HTML 页面挂 <video>, 而是直接在地址栏输入 MP4 URL来代表这个过程。

地址栏输入 MP4 URL,Chrome 会创建一个 MediaDocument,在内部构建一个真实的 <video> 元素,用 <source src="你的URL"> 挂上去。走的是完全相同的 HTMLVideoElement 管线,Range 请求、moov 解析行为与手写 <video src="..."> 没有任何区别。

Chromium 源码 media_document.cc

auto* media = MakeGarbageCollected<HTMLVideoElement>(*GetDocument());
    media->setAttribute(html_names::kControlsAttr, g_empty_atom);
    media->setAttribute(html_names::kAutoplayAttr, g_empty_atom);

auto* source = MakeGarbageCollected<HTMLSourceElement>(*GetDocument());
    source->setAttribute(html_names::kSrcAttr,
                         AtomicString(GetDocument()->Url()));// ← 输入的 URL 
    

实验一: 仅 200, 无 Range 支持, 传统 MP4 vs FastStart MP4

Step1 : 传统 MP4: 下载完才能起播(2RTT)

moov 在末尾, 约在第 60M 字节的位置。

image.png

服务不处理 Range, 直接返回 200 , 出现了两条请求:

  1. 第一条请求试图请求全量 Range, 然后快速停止了。
  2. 第二条请求开始从偏末尾位置请求Range, 属于一条持续下载的 200 请求,视频无法播放, 直到下载完毕。

image.png

Step2: FastStart MP4: 快速起播(1RTT)

MP4 的 moov 位置在前面, 该测试文件中 moov 位于第32字节。

image.png

服务器同样不处理 Range, 这次仅出现一条请求:

  • 浏览器请求全量的Range, 但是服务没处理, 一条持续下载的 200 请求, 但是却在刚下载前一段资源后就已经可以播放了

image.png

实验一的初步结论

GET 200
传统 MP4资源(moov 后置)2次请求, 下载完再播
FastStart MP4资源(moov 提前)1次请求, 快速起播 🌟

实验二: 206 & Range 支持, 传统 MP4 vs FastStart MP4

Step1: 传统 MP4: 快速起播(3RTT)

实验现象: 浏览器触发三次不同的 Range 请求, 且能快速起播

image.png

请求Range目的为什么结束
0-探测 box 结构;下载mdat数据+解析box头发现 moov 在末尾,abort
64290816-取 moov 元数据moov 读完,正常结束
32768-从 32KB 对齐点恢复 mdat, 源码写死了固定值:chromium.org-32kb block持续下载,播放进行中

Step2: FastStart MP4: 快速起播(1RTT)

实验现象: 仅有一条全量的Range的请求, 且能快速起播

image.png

实验二的初步结论

GET 206 + Range
传统 MP4资源(moov 后置)3次请求, 快速起播
FastStart MP4资源(moov 提前)1次请求, 快速起播

实验结论 🌟🌟🌟

在 chromium147(2026.04) 内核的浏览器上, 传统 MP4 和 FastStart MP4 的行为表现不同

GET 200GET 206 + Range
传统 MP4资源(moov 后置)下载完才能播(2RTT)快速起播(3RTT) 🌟
FastStart MP4资源(moov 提前)快速起播(1RTT) 🌟快速起播(1RTT) 🌟

传统 MP4 资源只有在服务端仅支持 200 的情况下, 才会下载完才能播的情况(开篇的疑问结论已有答案 ✅)。

而 FastStart MP4 , 做到 1 条请求快速起播。

额外校验:

  • 我快速尝试了在 Safari 26.1 上检验上述结论, 得出“快速起播的组合关系在Safari上结论是一致的”, 只是请求 RTT 机制略有不同, 不过这对于生产选型已经够用了。
  • 在低版本 Chrome 40(2015 release) 上, 传统MP4 搭配上 206 + Range 依然可以做到快速起播。(使用 Browserstack + Window 8 真机验证)

启发

MP4 的生产最佳实践

使用支持 206 和 Range 的服务端, 可以稳妥地做到快速起播。(eg: 现代CDN/自建服务本身等)

将传统 MP4 转化为 FastStart MP4 的价值是**,** 不仅能快速起播, 还能近一步节省 RTT。尤其是弱网场景下RTT收益更明显。

以国内 4G 网络典型 RTT 约 50–100ms 为例,moov 后置 + Range 比 FastStart + Range 多出 2 次额外请求,理论增加起播延迟 100–200ms;弱网(RTT 200ms+)下可达 400ms 以上

探索 < video > 的边界

上述研究只解决了一个具体问题:moov 位置 + 服务端 Range 支持对起播体验的影响。

<video> 标签本身还有更根本的限制:

  • 无法处理无限增长的流:直播内容没有固定文件大小,moov 探测逻辑从根上不适用
  • 无法运行时切换码率:ABR 需要在不同清晰度分片之间动态切换,<video> 不暴露这个控制接口
  • 无法自定义网络策略:预加载时机、P2P 分发、CDN 调度,对 <video> 来说是黑盒

所以, 企业生产实践上, 会有更多的场景需要处理, 所以解法N:

  • fmp4(Fragment MP4) + MSE:将 moov 打散为分片,配合 MediaSource API 增量推送
  • HLS / DASH:流媒体协议, 原理是将视频切成独立分片(fmp4/TS),分段请求,支持 ABR 和直播
  • FLV:tag 结构天然流式,每个 tag 自带类型和时间戳,无需 moov,可用于 HTTP-FLV 直播

这些限制不是「等 Chrome 更新」能解决的,它们是 <video> API 设计边界的一部分。

实验工具 & 实验代码

备注
静态站, 演示206下的 FastStart mp4 和 传统MP4为了方便大家快速看到206下的不同 MP4 效果(见下图)
mp4box.js online快速查看当前 MP4 文件的内部结构
ffmpeg音视频转码工具, 传统 MP4 可以快速转化为 FastStart MP4
ffmpeg -i input.mp4 -movflags faststart output.mp4
Github 200/206 Node server极简的 200/206 服务

image.png

参考资料

mp4ra.org/

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

www.rfc-editor.org/rfc/rfc9110…

ossrs.io/lts/zh-cn/a…

www.flashedgecdn.com/blog/round-…

mdn.org.cn/en-US/docs/…

—— 研究于 2026.04.26