前端视频缓冲技术


浏览器 <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]

浏览器打开这个文件时:

  1. 读到 ftyp → 知道这是 MP4
  2. 接着是 300MB 的 mdat → 没拿到 moov,不知道解码参数
  3. 只能把整文件从头读到尾,找到末尾的 200KB moov
  4. 读完 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%

这一行不是”创建缓冲”,是浏览器的缓冲状态,给用户看。

总结

  1. 缓冲是浏览器 <video> 标签自带的,你代码不用管
  2. buffered 属性让你缓冲状态,progress 事件让你更新
  3. 远程 / 局域网 / 本地 file:// — 三种来源管线一样,只是数据到达的速度不同
  4. 如果发现”本地文件一次性全加载才播”——不是缓冲策略问题,是 MP4 的 moov 原子在文件末尾ffmpeg -movflags +faststart 一把修好。纯字节搬运,不重新编码,画质无损

文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
 本篇
前端视频缓冲技术 前端视频缓冲技术
详解浏览器 video 标签自带的缓冲机制:buffered 属性的读取与使用、三种文件来源的缓冲行为差异、MP4 moov atom 位置对加载速度的影响及修复方法。
2026-07-29
下一篇 
serialport 本地编译失败怎么办:从环境到 electron-rebuild 全流程 serialport 本地编译失败怎么办:从环境到 electron-rebuild 全流程
serialport 是带 C/C++ 原生插件的 npm 包,安装时常遇到 node-gyp / MSBUILD 编译报错、Electron 下 ABI 不匹配等问题。本文讲清原生模块「预编译优先、本地编译兜底」的原理,以及 Windows 环境准备、手动 node-gyp 的 configure/build、Electron 场景的 electron-rebuild / electron-builder install-app-deps,并列出常见报错排查。
2026-07-28
  目录