纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器

纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器

全程无后端、无框架、无 Electron,一个 index.html + 一个 app.js,用 File System Access API 把浏览器变成了能播几十 GB 本地片源的播放器。本文复盘完整思考过程与 5 个实战大坑,代码可复用。

一、事情是怎么开始的

家里攒了几百 GB 的片源,MKV/TS/FLV 混着放。用播放器软件看吧,界面丑、倍速别扭、还老记不住看到哪;传到网盘再在线看吧,上传几小时、会员还死贵。

然后我盯着 Chrome 想:浏览器不是能播视频吗?直接拖进去不就完了?

结果现实很骨感:<video> 标签确实能播,但------

  • 拖一个 4K MKV 进去,要么直接转圈,要么内存飙到几个 GB;
  • TS/FLV 这种格式 Chrome 根本不认;
  • 想拖动进度条,光标一松,画面卡成 PPT。

于是决定自己写一个「本地视频流式播放器」,纯前端,核心目标就三个:

  1. 不整载入内存:几十 GB 的文件,读多少播多少;
  2. 格式通吃:MP4/MKV/MOV 原生播,TS/FLV 用 mpegts.js 转封装;
  3. 能随便拖:进度条任意位置跳转,和在线视频一样顺滑。

成品长这样(截图是初始界面,左侧媒体库 + 右侧播放区 + 底部真实数据状态栏):

二、整体架构:一个页面,三条播放链路

先说结论,架构就一句话:原生格式走 URL.createObjectURL(File),TS/FLV 走 mpegts.js + 自定义 FileLoader,m3u8 走内置下载器,全部基于 File System Access API 的文件句柄按需读盘。

这里有个反直觉的知识点必须先说清楚:

URL.createObjectURL(file) 并不会把整个文件读进内存。File 对象是磁盘的懒加载引用,浏览器按需读盘。所以原生格式路径(MP4/MKV)天然就是流式的,你只管把 blob URL 丢给 <video>。

真正麻烦的是 TS/FLV 。mpegts.js 是为 HTTP 设计的,默认用 RangeLoader 走 fetch,但 file:// 根本没法 fetch。于是要给 mpegts.js 写一个自定义 FileLoader,把磁盘文件伪装成"网络流"喂给它。大坑从此开始。

三、坑 ①:分片必须 188 字节对齐,否则解析错乱

TS 流的基本单位是 188 字节的 TS 包 ,每个包以 0x47 开头。mpegts.js 会按 188 字节网格去切包解析。

我的第一个版本:直接 file.slice(offset, offset + 512KB) 读分片,然后传给 mpegts.js。结果------花屏、卡死、时长乱跳,而且不是偶发,是必现。

排查半天,问题出在一个数学细节上:

js 复制代码
// ❌ 512 * 1024 = 524288,不是 188 的整数倍
// 524288 ÷ 188 = 2788.76...,第 2788 个 TS 包被拦腰截断
const CHUNK_SIZE = 512 * 1024;

被截断的半截包,mpegts.js 会当成一个"新包"去解析,整个 PES 载荷从此错位,解析全乱。

解法:把分片大小对齐到 188 的整数倍:

js 复制代码
// ✅ 512KB 向下取整对齐:524100 字节 = 2788 个完整 TS 包
const CHUNK_SIZE = Math.floor(512 * 1024 / 188) * 188;

同源第二坑:chunk 必须是 ArrayBuffer,不是 Uint8Array

对齐只是第一层。修完还是偶尔乱码,最后挖到一个更阴的坑:

mpegts.js 内部用 new Uint8Array(chunk, offset, len) 切包。这个三参数构造的 offset 参数只对 ArrayBuffer 源生效;如果你传的是 Uint8Array,它会无视 offset 整份拷贝,切出来的全是错位数据。

js 复制代码
// ✅ 必须传 ArrayBuffer
const chunk = await file.slice(offset, end).arrayBuffer();

这个坑的可怕之处在于:不报错、不崩溃,只是悄悄给你错位数据,表现出来就是偶发花屏。遇到这种"玄学 bug",先怀疑类型转换,别急着怀疑算法。

四、低内存:三道刹车,防止浏览器把自己撑爆

MSE 路径的读盘循环很容易写成"无脑狂读"。我加了三道刹车,让内存占用稳定在一个播放窗口内:

① 预读门控:缓冲超前超过 20 秒就暂停读盘,等播放消耗再继续:

js 复制代码
_paceAllowed() {
  const v = S.video;
  if (!v || !v.buffered || !v.buffered.length) return true; // 尚无缓冲,需要起步数据
  const end = v.buffered.end(v.buffered.length - 1);
  return (end - v.currentTime) < READAHEAD_SEC;             // READAHEAD_SEC = 20
}

② mpegts lazyLoad:缓冲超 30s 自动挂起加载,恢复阈值 5s。

③ 后向缓冲清理 :已播过 20s 的缓冲自动丢弃(autoCleanupMaxBackwardDuration: 20)。

效果就是:几十 GB 的文件,播放中内存始终只有几百 MB。底部状态栏实时显示 读取: xx MB / 缓冲: xx s / 内存: xx MB,这是我调内存时盯得最多的数字。

五、坑 ②:TS 没有全局索引,时长得自己探

TS 是纯流式容器,文件里根本没有时长字段,播放器无从得知总长度 ------ 进度条直接废掉,续播也做不了。

我的方案:读文件头和文件尾各 2MB,解析 PAT → PMT 找到视频 PID,再算首末视频 PES 的 PTS 差值 = 时长。

js 复制代码
// 头尾 PTS 差即时长(TS 时间戳 90kHz)
const diff = tailPes[tailPes.length - 1].pts - firstPts;
return { duration: diff / 90000, videoPid, firstPts };

顺带一提:这个探测结果不只服务时长。videoPid 和 firstPts 是下一篇"任意位置 seek"的地基,一鱼三吃。

FLV 就简单了,直接在头部 AMF 里找 onMetaData.duration 字段读出来。

另外还有个体验细节:几千集的目录,不可能给每个文件都探测时长。我用了 IntersectionObserver + 并发 3 + 结果缓存到 IndexedDB(4000 条上限),列表滚到哪、才探测哪,超大目录零卡顿。

六、彩蛋:报错也要"有文化"------从字节流里嗅探真实编码

浏览器播不了视频时,默认报错是"解码失败"四个字。但用户真正的困惑是:文件是不是坏了?

于是我加了个"法医"功能:读取文件头 2MB,扫描 00 00 01 起始码定位 SPS NAL,用 Exp-Golomb 解码出 profile 和色深,然后给出精确诊断:

  • 命中 10bit H.264 → "Chrome/Edge 内置解码器仅支持 8bit H.264,请用 VLC(文件本身完好,非损坏非加密)"
  • 命中 HEVC → "浏览器通常不支持 HEVC 解码,请用 VLC"
  • 嗅探不到 → 回退通用提示(MP4 的 SPS 在 avcC box 里,没有起始码,嗅不到很正常)

一个小功能,却把"这破浏览器不行"的抱怨,变成了"你的文件是 10bit 压的,浏览器没救了"的确定性结论。

七、还有一堆"人味"细节

  • 兼容模式 :Firefox/Safari 没有 File System Access API,自动回退到 <input type="file" webkitdirectory>,只是少了句柄持久化;
  • 授权持久化 :navigator.storage.persist() 申请持久化存储,句柄授权跨浏览器重启依然有效;
  • 首次手势恢复 :页面加载时无用户手势不能 requestPermission,就监听首次点击/按键,在手势内自动重新授权并打开上次的文件夹 ------ 实现"重开网页自动回到上次的片库";
  • 历史续播:IndexedDB 存播放进度 + 缓存文件句柄,点一下"继续播放"直接接着看,不需要重新选文件;
  • 倍速/音量持久化:localStorage 记住你的 1.5x 和音量,重启不丢。

八、总结

这篇文章讲了浏览器播本地大视频的完整方案,核心可复用技巧:

  1. File 对象是磁盘懒加载的 ,createObjectURL 不会整载入内存,大胆用;
  2. TS 分片必须 188 对齐,chunk 必须传 ArrayBuffer(传 Uint8Array 会静默错位);
  3. 低内存三件套:预读门控 + lazyLoad + 后向缓冲清理;
  4. TS 时长 = 头尾 PTS 差,顺手还能拿到 PID 和 firstPts;
  5. 错误信息要"可诊断":SPS 嗅探把"无法播放"变成"10bit 请用 VLC"。

下一篇讲讲最硬核的部分:TS 文件任意位置拖动 ------ mpegts.js 对 TS 的 seek 是空操作,我是怎么用"CBR 估计 + IDR 定位 + PAT 回扫 + 子流重建"实现等效 m3u8 跳分片的,还有顺手写出来的 m3u8 下载器(12 线程池 + 广告过滤)。

如果这篇文章对你有帮助,欢迎收藏备用。你平时遇到过哪些"浏览器播视频"的玄学坑?评论区聊聊。

(项目源码为本地单页应用:index.html + app.js(约 2800 行)+ mpegts.js,纯原生 JS,无构建依赖。)

相关推荐
溪语流沙1 小时前
【Web全栈进阶】FastAPI工程化:APIRouter拆分 + 配置 + 依赖注入
java·前端·fastapi
Shirley~~1 小时前
Three.js的基础概念
前端·3d
deli0071 小时前
黏菌没有大脑,3000 个 Physarum 粒子跑 12973 步自己铺出了迷宫最短路
前端
凌云若寒1 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
嘟嘟同学和妮妮同学1 小时前
用原生 JS 做了一个中国历史帝皇梳理的可视化站(7 朝 77 帝 + Leaflet 地图)
前端
黄权光1 小时前
【微信小程序】uni-app + Vue3 + Vite 的小程序项目实现「进入指定范围才能打卡」的考勤功能
前端
西柚小萌新1 小时前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js
逐米时代2 小时前
远程运维诊断减少到场率
java·服务器·前端
Java后端的Ai之路2 小时前
03_React_JSX
前端·react.js·前端框架