抛出问题: MP4 在 <video> 里,到底需不需要下载完再播?
答案是:不一定。取决于两件事:moov 放在文件的哪里 ,以及服务器是否支持 Range 请求 。(moov 是 MP4 的元数据容器, 下文会介绍)
本文用实验, 逐一说清楚这两件事各自的作用------以及你可能一直误解的 FastStart 的真实价值。
最小必要知识
MP4 是什么?
MP4 是视频的封装格式, 遵循 MPEG-4 Part 14 协议。
MP4 的存储结构上有三个核心: ftyp、mdat、moov 。
ftyp: 声明格式, 兼容的规范, 内容对播放本身没影响, 但不能缺。mdat: 真正的音视频数据。是文件里最大的 box , 但它是一段裸数据(没有索引), 单独拿到**mdat** 浏览器不知道哪个字节是第几帧、时间戳是多少、从哪里开始解码。moov: 全局"目录"。记录所有视频的元信息(时长、轨道、编解码, 以及最关键的每一帧在mdat里的字节偏移量和时间戳 )。浏览器要先拿到moov, 才知道怎么读**mdat**、从哪里开始解码。
所以 moov 的位置非常重要, 理论上, 默认moov 在文件尾,浏览器不拿到结尾就不知道怎么解析前面的数据; 如果 moov 在文件头,浏览器下载开头就能开始播;

为什么 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 在 是否是必须下载完才能播。
实验准备
- 一个约 60MB 的 MP4 视频资源的两个版本。
- 传统MP4 :
bigmp4-moov-end.mp4moov 在尾部。 - FastStart MP4 :
bigmp4-moov-first.mp4moov 在头部, 由ffmpeg转码。
- 传统MP4 :
- 一个 HTTP 服务, 可以提供 200 和 206 的 MP4 资源的响应, 由 node 提供。
- mp4box.js, 用于识别 MP4 封装格式内部的具体结构。
- 一个 chromium 内核的浏览器。
接下来实验的展开是: 两个变量 × 两种状态 = 四种组合的播放器行为
| 200 | 206 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
c++
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 字节的位置。

服务不处理 Range, 直接返回 200 , 出现了两条请求:
- 第一条请求试图请求全量 Range, 然后快速停止了。
- 第二条请求开始从偏末尾位置请求Range, 属于一条持续下载的 200 请求,视频无法播放, 直到下载完毕。

Step2: FastStart MP4: 快速起播(1RTT)
MP4 的 moov 位置在前面, 该测试文件中 moov 位于第32字节。

服务器同样不处理 Range, 这次仅出现一条请求:
- 浏览器请求全量的Range, 但是服务没处理, 一条持续下载的 200 请求, 但是却在刚下载前一段资源后就已经可以播放了

实验一的初步结论
| GET 200 | |
|---|---|
| 传统 MP4资源(moov 后置) | 2次请求, 下载完再播 |
| FastStart MP4资源(moov 提前) | 1次请求, 快速起播 🌟 |
实验二: 206 & Range 支持, 传统 MP4 vs FastStart MP4
Step1: 传统 MP4: 快速起播(3RTT)
实验现象: 浏览器触发三次不同的 Range 请求, 且能快速起播

| 请求 | Range | 目的 | 为什么结束 |
|---|---|---|---|
| ① | 0- |
探测 box 结构;下载mdat数据+解析box头 | 发现 moov 在末尾,abort |
| ② | 64290816- |
取 moov 元数据 | moov 读完,正常结束 |
| ③ | 32768- |
从 32KB 对齐点恢复 mdat, 源码写死了固定值:chromium.org-32kb block | 持续下载,播放进行中 |
Step2: FastStart MP4: 快速起播(1RTT)
实验现象: 仅有一条全量的Range的请求, 且能快速起播

实验二的初步结论
| GET 206 + Range | |
|---|---|
| 传统 MP4资源(moov 后置) | 3次请求, 快速起播 |
| FastStart MP4资源(moov 提前) | 1次请求, 快速起播 |
实验结论 🌟🌟🌟
在 chromium147(2026.04) 内核的浏览器上, 传统 MP4 和 FastStart MP4 的行为表现不同
| GET 200 | GET 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 服务 |

参考资料
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status/206
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Range
https://www.rfc-editor.org/rfc/rfc9110#name-206-partial-content
https://www.flashedgecdn.com/blog/round-trip-time-rtt/
https://mdn.org.cn/en-US/docs/Web/HTTP/Guides/Evolution_of_HTTP
------ 研究于 2026.04.26