浏览器
<video>标签自带缓冲,你一行代码都不用写。你要做的是读懂它的状态,画给用户看。
一、缓冲到底是谁在干活
往页面丢个 <video> 标签,给个 src:
<video src="https://example.com/big-buck-bunny.mp4" controls />
然后打开 DevTools → Network,你会看到一堆请求:
GET /big-buck-bunny.mp4 → 200 (前 2MB,读文件头)
GET /big-buck-bunny.mp4 → 206 Range: bytes=2097152-4194303
GET /big-buck-bunny.mp4 → 206 Range: bytes=4194304-6291455
...
这些请求不是你发的,是浏览器自己发的。 它每次只拉一小段(通常 2MB),播到哪儿就拉到哪儿,前面还有一小段预读。你从头到尾没写一行网络请求代码。
这叫 Range 分片 + 流式加载,是现代浏览器 <video> 标签的默认行为。前提是:服务器支持 Range 请求(nginx 默认支持,返回 206 Partial Content;如果服务器只支持 200,整文件拉下来才播,体验就是另一回事了)。
二、buffered 属性:把缓冲状态读出来,画成进度条
浏览器内部维护了一个 TimeRanges 对象——记录”已经下载好了哪些时间段”。JS 可以通过 video.buffered 读取:
const video = document.querySelector("video");
video.addEventListener("progress", () => {
const buf = video.buffered;
// buf 是一个数组,每个元素是 { start, end }
// 正常顺序播放 → 就一段:buf[0] = { start: 0, end: 已缓冲到多少秒 }
const last = buf.length - 1;
const bufferedPct = (buf.end(last) / video.duration) * 100;
// 把 bufferedPct 画成进度条上的灰色"已缓冲"区域
});
为什么取 length - 1 而不是第一段
用户拖动进度条跳到后面时,浏览器会抛弃旧缓冲区、从跳到的位置重新下载。这时 buffered 里可能不止一段:
buffered = [
{ start: 0, end: 20 }, // 之前播过的,已经没用了
{ start: 60, end: 90 }, // 拖动后新下的,这才是"当前缓冲"
];
length - 1 取的是最新一段的缓冲进度,而不是把旧的混在一起算。
三、三种文件来源,缓冲行为各不同
| 场景 | 协议 | 缓冲方式 | buffered 属性 |
拖动进度条 |
|---|---|---|---|---|
| 公网 HTTPS | https:// |
Range 分片请求 | 有值 | 正常 |
| 局域网 NAS | http://192.168.x.x |
Range 分片请求(和公网一样,只是延迟更低) | 有值 | 正常 |
| 本地磁盘 | file:///D:/video.mp4 |
操作系统文件系统读取 | 有值 | 几乎零延迟 |
局域网 NAS 本质上就是 HTTP 协议,只是网络延迟从公网的 50ms 变成局域网的 0.5ms。buffered 属性照常工作,进度条灰色区域照常画。
本地 file:// 走的是操作系统文件系统,不走网络栈,但 <video> 标签的缓冲管线完全一样——只是数据来源从”网络层”换成了”文件系统层”。你会看到 buffered 增长得极快(磁盘读取远超网络速度),拖动 seek 几乎零延迟。
四、一个极易踩的坑:MP4 的 moov atom
MP4 文件里有个叫 moov 的元数据块,存着编码格式、时长、关键帧索引。浏览器必须先读到它才能开始解码。
问题:很多手机拍的视频,moov 块写在文件末尾:
[ftyp header] [mdat 视频数据, 300MB] [moov 元数据, 200KB]
浏览器打开这个文件时:
- 读到 ftyp → 知道这是 MP4
- 接着是 300MB 的 mdat → 没拿到 moov,不知道解码参数
- 只能把整文件从头读到尾,找到末尾的 200KB moov
- 读完 320MB → 拿到 moov → 终于开始播
这就是”本地文件竟然一次性全加载”的真相。 不是协议问题,不是缓冲策略问题,是元数据放错了位置。
一行命令修复
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
-c copy 不重新编码,只把 moov 挪到文件头。320MB 文件几秒搞定,画质无损。
修复后:
[ftyp header] [moov 元数据, 200KB] [mdat 视频数据, 300MB]
浏览器打开 → 读头几百 KB → 立刻知道解码参数 → 后续 mdat 边播边读,seek 瞬间响应。
怎么判断文件有没有这个问题
ffprobe -v trace input.mp4 2>&1 | grep moov
看到 moov 的 offset 很大(文件末尾),就需要 faststart。或者更简单:文件一打开转圈很久才开始播,就是 moov 在末尾。
五、我们写的那行代码,到底干了什么
bufferedPct.value = (v.buffered.end(v.buffered.length - 1) / v.duration) * 100;
| 成分 | 值(举例) | 含义 |
|---|---|---|
v.buffered |
[{start: 0, end: 45}] |
缓冲区已下好 0~45 秒 |
.length |
1 |
一个连续缓冲段 |
.end(0) |
45 |
缓冲区最远端:45 秒 |
/ v.duration |
45 / 120 |
除以总时长 |
* 100 |
37.5 |
→ 进度条灰色区画到 37.5% |
这一行不是”创建缓冲”,是读浏览器的缓冲状态,画给用户看。
总结
- 缓冲是浏览器
<video>标签自带的,你代码不用管 buffered属性让你读缓冲状态,progress事件让你更新它- 远程 / 局域网 / 本地
file://— 三种来源管线一样,只是数据到达的速度不同 - 如果发现”本地文件一次性全加载才播”——不是缓冲策略问题,是 MP4 的 moov 原子在文件末尾。
ffmpeg -movflags +faststart一把修好。纯字节搬运,不重新编码,画质无损