存档里全是本地文件。要让它们在 Electron 窗口里像网页一样播放,比想象麻烦。
为什么不用 file://
渲染进程里直接 <video src="file:///D:/archives/xxx.mp4">,会遇到两个问题:
一是路径。存档根目录是用户自己选的,可能在任意盘符,中文路径、空格、特殊字符都有。手拼 URL 很容易漏转义。
二是安全。现代 Electron 默认不允许渲染进程随便读本地文件,绕过限制要么放宽配置(等于开门),要么改架构。
自定义协议
最后用了一个注册协议:social-archiver://asset/?path=<绝对路径>。
有两个必须先做的事:
- 在
app.whenReady()之前调registerSchemesAsPrivileged - 两个标志都要带上:
standard和stream
少任何一个,<video> 拿到的响应就不支持流式读取,拖动进度条直接失败。standard 让它有正常的 URL 语义,stream 让它支持 Range。
Range 是重点
视频能拖进度条,靠的是 HTTP 206 和 Content-Range。
这里有个分界:100MB。
小于 100MB 的文件,直接把整个文件读进内存 Buffer,自己实现 Range 解析和切片。简单、可靠,反正内存扛得住。
超过 100MB 的走流式。流式下 Range 要自己算,而且要考虑边界:bytes=0-、bytes=-500、超出文件长度、多段 Range——这些都得处理,不然播放器会贴心地让你看到”无法播放”。
那个不该踩的坑
有段时间用过这个写法:
const stream = Readable.toWeb(fs.createReadStream(filePath));return new Response(stream, { headers: { 'Content-Type': 'video/mp4' } });看起来很现代:Node 的 Stream 转成 Web Stream,直接喂给 Response。
结果:视频能播,但拖进度条就断。
原因是这个转换出来的流是”一次性的”——它已经按顺序把数据推出来了,再去要第二段拿不到。Range 请求本质是”重新给我 0 到 1000 字节”,不是”接着上次的地方继续”。
现在这条写在交接文档里,用加粗标着。
其他细节
MIME 类型:.mov 一律标 video/mp4。Chromium 对 QuickTime 容器不认,标错就不播。
缓存:加了长期缓存和 ETag。同一个文件反复打开不重复读盘。
路径校验:协议处理里要 path.resolve 之后检查是否在存档根目录内。不然就是任意文件读取漏洞——social-archiver://asset/?path=C:/Windows/System32/config/SAM 这种请求本来能成。
播不了的那些
有些实况图是 HEVC 编码的,而应用启动时禁用了 GPU(历史上 WebGL 崩过几次)。结果是 Chromium 里解不了 HEVC。
这不是下载失败,是渲染限制。
所以卡片上有个兜底:检测到播放失败就显示「打开实况」,走 shell.openPath 交给系统播放器。判断依据是文件存在、有 ftyp 头、但 <video> 报错。
本地文件协议这件事,最花时间的不是写代码,是搞清楚为什么明明能播却不能拖。