我在 CaPilot 上碰到过这件事。pnpm tauri dev 里主题视频壁纸循环播放正常,打成 .deb 装上之后背景只剩主题底色。静图主题不受影响,壁纸层挂上了,是 <video> 没解出帧。
我先怀疑片子没打进包,或者解码器缺了。两边都排除完,才摸到根因:WebKitGTK 的 <video> 交给 GStreamer 解码。GStreamer 只打开 file:// 和真正的 http(s)://,不走 WebKit 自定义协议,也不吃 blob:。安装版最后是在进程内起一个 127.0.0.1 HTTP 服务给 <video> 用。Windows / macOS / tauri dev 都不走这条路。
下面是我在 Ubuntu + Wayland 上怎么查、走过哪些弯路、为什么最后选 HTTP。
现象
默认主题 quantum 的壁纸是 themes/wallpapers/capilot.mp4:H.264、2560×1440、约 6.1 MiB,moov 在文件末尾。开发态能播;安装版零帧。
本机环境:Ubuntu、Wayland、WebKitGTK 2.52、GStreamer 1.28(avdec_h264 存在)。.deb 已经声明了 gstreamer1.0-libav 和 gstreamer1.0-plugins-bad。
改之前的播放链路
主题 JSON wallpaper.file │ ▼Vite glob → hashed /assets/….mp4 (开发:http://localhost:1420,能播)bundle.resources → $RESOURCE/themes/wallpapers/….mp4 │ ▼ 打包 LinuxconvertFileSrc(abs) → asset://localhost/<urlencoded-path> │ ▼<video src=asset:// … muted loop autoPlay> │ ▼WebKitGTK → GStreamer playbinWindows 走 WebView2,asset:// 有 Range,能播。macOS WKWebView 同样。问题只在 Linux 打包 + WebKitGTK + GStreamer。
先排除「文件没打进包 / 没解码器」
安装包里文件在:
/usr/lib/CaPilot/themes/wallpapers/capilot.mp4GStreamer 自己能解:
gst-discoverer-1.0 file:///usr/lib/CaPilot/themes/wallpapers/capilot.mp4# Duration 15.084s, H.264 High@L5, 2560×1440
timeout 3 gst-play-1.0 --no-interactive --audiosink=fakesink --videosink=fakesink \ /usr/lib/CaPilot/themes/wallpapers/capilot.mp4# preroll 成功不是缺插件,也不是片子坏了。卡在 webview 喂给 GStreamer 的 URI。
capilot.mp4 的 box 顺序是 ftyp → mdat → moov(moov 在约 6.1 MiB 处)。渐进播放必须能 Range 读到文件末尾的索引,否则连第一帧都没有。faststart 片子(moov 在前)对 Range 没那么苛刻,但自定义协议一样过不了 GStreamer。
走过的弯路
当时每条路都说得通。探针打回来之后,才知道一帧都没有。
asset://
开发走 Vite HTTP(有 Range),打包走 tauri://localhost/assets/…(无 Range)。我改成 resolveResource + convertFileSrc → asset://。
Tauri 的 asset:// 有两个硬伤:
- 第一次 200 不带
Accept-Ranges。Range 头只在已经带Range:的请求上才写。WebKit 看第一响没有 Range 支持,就不会去要 moov。 - 每个 206 最多 1 MiB。即便开始 Range,也拿不到 6 MiB 处的 moov。
结果:零帧。静图仍走 asset:// 的 CSS background-image,图片主题不受影响。
fetch(asset://) → blob:
我想把整文件拉进内存,绕过 Range。
WebKitGTK 对自定义协议的 fetch() 经常返回空 body。即便 body 完整,GStreamer 也不播 blob:。探针结果是 readyState=0,超时,videoWidth=0。
IPC 读字节 → blob:
再用 Tauri IPC 读字节,保证 body 非空,然后 URL.createObjectURL。
IPC 能读到文件,URI 仍是 blob:,GStreamer 照样零帧。这里还有一个小坑:deb 装在 /usr/lib/CaPilot,会被 path_is_allowed 当成系统路径拒绝,需要给 $RESOURCE 开例外。
自定义协议 capilot-media://
自己实现 Range,首次 200 就带 Accept-Ranges,206 不截断。
协议本身是对的,WebKit 也把请求打过来了。探针返回:
"error": { "code": 4, "message": "" },"fetchError": "NotSupportedError: The operation is not supported."MEDIA_ERR_SRC_NOT_SUPPORTED。GStreamer 不打开 WebKit 自定义 scheme,回调写得再正确也没有画面。
另外:navigator.userAgent 在 WebKitGTK 上经常长得像 Safari,不能用来判断 Linux。后来改成「Linux 上 Rust 返回 URL,其它平台返回 null」。
CSP 漏端口
loopback HTTP 起来之后,第一次安装版仍零帧。日志有:
wallpaper http listening on 127.0.0.1:37955但 CSP 写成 media-src … http://127.0.0.1。不写端口只匹配 :80。随机端口被拦,<video> 静默失败。必须是 http://127.0.0.1:*。
截屏当不了验收
本机是 GNOME Wayland,app 是原生 Wayland 窗口。mss 只有 X11 后端,截出来是黑屏;没有 ydotool / wtype。截屏不能当验收。
我改用同一套 WebKitGTK 开隐藏窗口,JS 读 <video> 的 videoWidth / currentTime / error。出帧 = videoWidth > 0。
实测结果:
| 模式 | URL | 结果 |
|---|---|---|
file | file:///usr/lib/CaPilot/themes/wallpapers/capilot.mp4 | 出帧,videoWidth=2560,readyState=4 |
http | http://127.0.0.1:<port>/capilot.mp4(带 Range) | 出帧,日志 200 + 多次 206 |
blob | fetch(http) → blob: | 零帧,readyState=0 超时 |
custom | capilot-media://localhost/<encoded> | 零帧,MEDIA_ERR_SRC_NOT_SUPPORTED |
GStreamer 自己解 file:// 也成功。修复目标变成:安装版 <video src> 必须是 GStreamer 能打开的 URI。file:// 和真 HTTP 都行;自定义协议和 blob 不行。
我选 HTTP 而不是 file://,是因为:
- 资源在
/usr/lib/CaPilot/…,WebKitfile://对自定义源页面的跨协议限制不好控 - 用户自选壁纸路径各异,HTTP 一层统一鉴权(扩展名白名单 +
path_is_allowed+$RESOURCE) - 探针已经证明本机 HTTP + Range 能出帧
最终改法
只动 Linux 打包视频。tauri dev 仍用 Vite HTTP;Windows / macOS 仍用 asset://;静图仍用 asset://。
进程内 loopback HTTP
绑 127.0.0.1:0(随机端口,避免和本机服务打架),路径形如 /wallpaper/<percent-encoded-abs-path>:
- 始终
Accept-Ranges: bytes Range:→206,不截断(对比 Tauri asset 的 1 MiB 上限)- 无 Range →
200+ 整文件 - 鉴权与原来的 IPC 相同:扩展名白名单、
path_is_allowed、额外放行$RESOURCE - 单文件上限 80 MiB
- 非 Linux:
start()空操作,命令返回None
启动放在 GUI setup 里,仅 Linux。--daemon 不跑 GUI,不听这个端口。
命令:wallpaper_http_url { path } → Some("http://127.0.0.1:<port>/wallpaper/…") 或 None。
前端解析顺序
resolveWallpaperSrc 按环境选 URL:
- 开发态且有 Vite
wallpaperUrl→ 直接用 - 静图 →
asset:/// Vite URL - 视频且有本地绝对路径 →
invoke("wallpaper_http_url");Linux 返回 http URL,其它平台null - 回退
asset://(Windows / macOS)
Linux 判断不要走 navigator.userAgent。
CSP
media-src 'self' blob: asset: http://asset.localhost https://asset.localhost http://127.0.0.1:*;connect-src … http://127.0.0.1:*;http://127.0.0.1 没有 :* 时只匹配 80 端口。随机端口必须写通配。
现在的数据流(Linux 打包)
wallpaper.file / 用户自定义绝对路径 │ ▼resourceDir + join → /usr/lib/CaPilot/themes/wallpapers/capilot.mp4 │ ▼invoke wallpaper_http_url │ ▼http://127.0.0.1:37955/wallpaper/%2Fusr%2Flib%2F…%2Fcapilot.mp4 │ ▼<video src=http://127.0.0.1:…> │ ▼GStreamer 当普通 HTTP 打开(Range 到 moov)→ 出帧成功时应能在日志里看到:
wallpaper http listening on 127.0.0.1:<port>wallpaper http GET /usr/lib/…/capilot.mp4 range 0-… / 6121519没有 GET 行:前端没要到 URL,或 CSP 仍在拦。
平台对照
| 环境 | 视频 <video src> | 静图 |
|---|---|---|
pnpm tauri dev(任意 OS) | Vite http://localhost:1420/assets/….mp4 | 同上或 asset:// |
Linux .deb / AppImage | http://127.0.0.1:<port>/wallpaper/… | asset:// |
| Windows 打包 | asset://(WebView2) | asset:// |
| macOS 打包 | asset://(WKWebView) | asset:// |
tauri dev 和安装版的 localStorage 是两份(http_localhost_1420 vs tauri_localhost_0)。开发态开着不会抢安装版的壁纸 HTTP,但两个窗口容易点错。
以后再碰到「开发有、打包没有」
- 先
gst-play/gst-discoverer确认文件和主机解码器。 - 用同一套 WebKitGTK 探哪种 URI 能出
videoWidth。协议细节后改。 - 自定义 scheme 和
blob:在这套栈上基本没戏,除非 GStreamer 改了行为。 - 本机 HTTP 能出帧之后,检查 CSP 是否带端口通配、日志里有没有 GET / Range。
- Wayland 上截屏当不了回归。