本地资源库视频播放

2362 字
12 分钟
本地资源库视频播放

本地资源库视频播放:从 WebView 闪退到内置 ffmpeg 与 Range 封顶#

版本 0.8.3 · 本地资源库新增视频/音频播放、片段标记与导出,随后踩到了 WebView2 的解码深坑。

现象#

视频网格页停留一会儿后整个窗口闪退——大量视频封面加载不出来(只有少数小文件出了图),而终端里的 wails dev 进程还活着。打开视频播放也可能触发。

现象
现象

这个特征很关键:wails dev 是父进程(构建 + dev server),应用 exe 是它拉起的子进程。父进程活着、窗口消失 = 应用进程整个死掉了,而不是前端报错。

排查:三个证据#

1. Go 侧没有能杀死进程的路径。 逐一检查了打开视频触发的所有 Go 链路:视频原文件服务(http.ServeContent 流式 + Range,不整读内存)、片段列表、元数据回填、ffmpeg 检测——全是参数化 SQL 和有边界检查的解析,无 panic 点。

2. 事件查看器 + Crashpad 痕迹指向 WebView2。 应用程序日志里没有主程序的崩溃记录,但在 %APPDATA%\Roaming\emulsion-dev.exe\EBWebView\ 下挖到了时间线:

时间痕迹含义
09:39:03EdgeWebView 最后一条 INFO 日志WebView2 进程的生命迹象到此为止
09:39:19Local State 落盘浏览器进程非正常关闭时写状态
09:40Crashpad\watson_metadata 新建Crashpad 向 Windows 错误报告移交崩溃

主程序没崩、没有崩溃转储、只有子进程被 Crashpad 接管的痕迹——这是 Wails v2 的已知行为:WebView2 渲染进程崩溃 → 窗口销毁 → 应用静默退出。

3. 元凶是一批 DJI 无人机视频。 E:\图片\DJI\ 下 14 个视频共 8.1GB,从文件尾部探测到 hvc1 box——HEVC(H.265) 编码,4K 长时段录制,单个最大 2.28GB。

为什么会崩#

视频封面(poster)原本的实现是前端截帧:Go 解不了视频帧,所以网格里每张”未出图”的视频卡片挂载时,都会创建一个隐藏 <video preload="auto"> 加载原始文件、seek 到 1/3 处、画到 canvas 导出 JPEG 回传给后端缓存。并发限流 2,但队列会逐个消化所有视频。

于是:一排 2GB 的 4K HEVC 文件排队进 WebView 解码——Chromium 的硬解路径对这类流崩溃,渲染进程死亡,onError 根本来不及触发。能出图的少数几个是排在前面的小 H.264 文件。“停留一会儿才闪退”正是队列走到重型文件的时刻。

本质上:把批量视频解码放进了 UI 进程。

专业软件怎么解决#

共性不是用什么解码器,而是三条架构原则:解码永远不在 UI 进程里、优先硬解、缩略图预生成并缓存。

  • 资源管理器 / 照片应用:Media Foundation(IMFSourceReader)+ 系统编解码器,解码跑在 dllhost.exe 独立进程,崩了不连累 explorer。注意 MF 解 HEVC 也依赖系统装没装 HEVC 扩展——资源管理器能出图不代表 WebView2 能播,两边是不同的解码路径。
  • 剪映 / CapCut:自带 FFmpeg 系媒体引擎,DXVA/NVDEC 硬解,UI 只拿解码完成的帧,封面导入时后台预生成。
  • Jellyfin / Immich / digiKam(与图库场景最接近):清一色 ffmpeg 子进程生成封面,子进程天然崩溃隔离。

方案选型#

方案评价
ffmpeg 子进程(Go 侧生成封面)首选:业界标准、任何编码任何体积都稳、进程级崩溃隔离、跨平台
Media Foundation(Windows 底层)工程量大(COM 绑定)、仅 Windows、HEVC 仍取决于系统编解码器,性价比低
Windows Shell 缩略图缓存缓存不一定命中,跨平台无从谈起
继续 WebView 截帧 + 编码门禁只能作为 ffmpeg 缺席时的降级路径

新问题:用户(包括我自己)的机器上根本没装 ffmpeg。内置进安装包会增大体积,按需下载则要做下载器、多源回退、半安装状态。结论:视频是这个产品的重点功能,“按需”是伪节省,直接内置——而且更新器本来就下完整安装包,多 20MB 换开箱即用 + 版本一致性,划算。为避免与系统 ffmpeg 冲突,内置二进制命名为 emulsion-ffmpeg。

实现#

1. ffmpeg 解析器(优先级从高到低,进程内缓存一次,每个候选都要过 -version 探活):

环境变量 MO_GALLERY_FFMPEG → exe 同目录的 emulsion-ffmpeg → 系统 PATH

2. 封面生成接入 derivative 管线。 扫描预热本来就会把视频排进缩略图队列(此前被短路成 no-op),所以只需在 generateDerivative 的视频分支换成真渲染:

Terminal window
ffmpeg -ss <min(1s, 时长/3)> -i 源文件 \
-frames:v 1 -vf scale=... -q:v 4 -f image2 输出

-ss 放在 -i 前是输入寻址(按关键帧跳),耗时与文件体积无关;产物走和照片缩略图完全相同的缓存与记账(cache key = id + mtime + size,文件改了自动失效重生成)。信号量限并发 2、单次 45s 超时、临时文件原子改名。

3. 前端降级护栏。 canvas 截帧降为”无 ffmpeg 时的兜底”:先问后端有没有 ffmpeg(有就完全不截)、加 500MB 体积上限、preload="metadata"。播放器 preload 也从 auto 降为 metadata。

4. 打包内置。 NSIS 模板加一行 File "..\..\bin\emulsion-ffmpeg.exe";macOS 构建后拷进 .app/Contents/MacOS/ 再签名(universal 用 lipo 合并双架构);Linux 进 tar/deb/rpm/AppImage 的 usr/bin。CI 新增 ffmpeg-bundle job:从源码编译 LGPL 精简构建——只保留 H.264/HEVC 解码、scale 滤镜、mjpeg 编码和各封装器的流复制(片段导出用 -c copy,不需要编码器),裁掉一切无关组件,体积从完整版的 160MB+ 降到十几 MB,也规避了 GPL 分发义务。一个细节:wails build -clean 会清空 build/bin 把预先放好的 ffmpeg 删掉,CI 里去掉 -clean(全新 checkout 本来就无需清理)。

验证#

对那个曾经稳定复现闪退的 2.28GB HEVC DJI 文件实测:Go 侧 1.2 秒生成 512×288 封面,全程不经过 WebView。回归测试中应用不再闪退,DJI 文件夹的封面逐个出图。

第二轮:内置 ffmpeg 之后,仍然慢、仍然闪退#

封面问题解决后,播放本身又暴露两个症状:按下播放到真正起播要等好几秒;播放大文件时再次闪退(甚至重启应用后预览模态自动恢复那个视频,形成”重启即闪退”的循环)。

排查:一层层排除#

  1. Go 服务器洗清嫌疑。写了个复刻线上行为(http.ServeContent + 相同响应头)的独立服务器实测:Range 请求头部/尾部均 1.5ms 响应;同一 2.28GB 文件,Chrome 浏览器直连 308ms 拿到元数据、309ms 可播放。curl 打真实 dev 链路(wails → vite 404 回落 → Go),206/Accept-Ranges/Content-Length/Content-Range 全部正确——代理链也没有破坏响应头。
  2. 给资产处理器加临时请求追踪(Range 头/状态/字节/耗时逐请求落日志),拿到了决定性的请求序列:
bytes=0- → 206 571,932,089 字节 2.004s ← 整个 572MB 顺序读光
bytes=571899904- → 206 32,185 字节 1ms ← 这才是 moov(在文件末尾)
bytes=1048576- → 206 570,883,513 字节 503ms ← 又从头读一遍取播放数据
  1. 浏览器对照。同一 URL、同一链路下:Chrome 151 与 WebView2 都以开区间 bytes=0- 起手,但 151 会在 demuxer 需要跳转时掐断连接、改用尾部 Range 取 moov、655ms 起播;WebView2(Chromium 152,新媒体缓存栈)从不掐断,把开区间响应当顺序流喝到 EOF。

根因#

DJI 等录制设备的 moov 索引写在文件末尾。WebView2 152 的媒体栈对开区间 Range 响应不主动中断,于是整个文件被顺序读完才见到 moov——几百 MB = 起播等几秒;2.28GB = 渲染进程被灌输撑死。慢和闪退是同一个 bug 的两种表现。

修复:服务端封顶开区间响应#

服务器无法改变客户端行为,但可以让”喝”变得无害:capOpenEndedMediaRange() 把 bytes=N- 改写为 bytes=N-(N+4MB-1) 再交给 ServeContent。HTTP 完全合法——服务器返回的字节可以少于请求的范围。效果:

  • 旧式客户端行为不变(本来就在分段请求);
  • 152 式客户端每次最多拿到 4MB,moov 跳转必然变成新的 Range 请求;

实测同一 2.28GB 文件:bytes=0- 响应从 2.04GB 降至 4MB,尾部 moov 请求 0.22s 返回。对比 faststart 重封装方案(每视频多占一倍磁盘),零成本。

小结#

  • WebView2/Chromium 的解码能力 ≠ 系统能力,HEVC 等”部分视频”会把渲染进程直接打死,且 JS 层无法兜底——批量解码永远不要放进 UI 进程;
  • 闪退排查时”窗口消失但父进程存活”是子进程/渲染进程崩溃的指纹,事件查看器 + Crashpad 痕迹可以坐实;
  • ffmpeg 子进程是图库类应用视频封面的标准答案,内置 LGPL 精简构建让它在体积上也不再是负担;
  • 媒体服务器对开区间 Range(bytes=N-)必须封顶响应:你无法预知客户端的媒体栈会不会把整个实体当顺序流读完,moov-at-end 的文件遇上这种行为就是”起播慢 + 大文件崩溃”;
  • 排查此类问题的工具组合拳:事件查看器/Crashpad 痕迹 → 独立服务器复刻对照 → 请求序列日志 → 新旧浏览器版本对照。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

本地资源库视频播放
https://shaio.top/posts/03mogallery/desktop/260911本地资源库视频播放/
作者
沙海
发布于
2026-09-11
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
沙海
Hello, I'm Shai.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
84
分类
15
标签
48
总字数
67,035
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.12.2
文章许可
CC BY-NC-SA 4.0

文章目录