1547 字
8 分钟
Linux 打包后视频壁纸零帧:WebKitGTK 不走自定义协议

我在 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-libavgstreamer1.0-plugins-bad

改之前的播放链路#

主题 JSON wallpaper.file
Vite glob → hashed /assets/….mp4 (开发:http://localhost:1420,能播)
bundle.resources → $RESOURCE/themes/wallpapers/….mp4
▼ 打包 Linux
convertFileSrc(abs) → asset://localhost/<urlencoded-path>
<video src=asset:// … muted loop autoPlay>
WebKitGTK → GStreamer playbin

Windows 走 WebView2,asset:// 有 Range,能播。macOS WKWebView 同样。问题只在 Linux 打包 + WebKitGTK + GStreamer。

先排除「文件没打进包 / 没解码器」#

安装包里文件在:

/usr/lib/CaPilot/themes/wallpapers/capilot.mp4

GStreamer 自己能解:

Terminal window
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 + convertFileSrcasset://

Tauri 的 asset:// 有两个硬伤:

  1. 第一次 200 不带 Accept-Ranges。Range 头只在已经带 Range: 的请求上才写。WebKit 看第一响没有 Range 支持,就不会去要 moov。
  2. 每个 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结果
filefile:///usr/lib/CaPilot/themes/wallpapers/capilot.mp4出帧,videoWidth=2560readyState=4
httphttp://127.0.0.1:<port>/capilot.mp4(带 Range)出帧,日志 200 + 多次 206
blobfetch(http)blob:零帧,readyState=0 超时
customcapilot-media://localhost/<encoded>零帧,MEDIA_ERR_SRC_NOT_SUPPORTED

GStreamer 自己解 file:// 也成功。修复目标变成:安装版 <video src> 必须是 GStreamer 能打开的 URI。file:// 和真 HTTP 都行;自定义协议和 blob 不行。

我选 HTTP 而不是 file://,是因为:

  • 资源在 /usr/lib/CaPilot/…,WebKit file:// 对自定义源页面的跨协议限制不好控
  • 用户自选壁纸路径各异,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:

  1. 开发态且有 Vite wallpaperUrl → 直接用
  2. 静图 → asset:// / Vite URL
  3. 视频且有本地绝对路径 → invoke("wallpaper_http_url");Linux 返回 http URL,其它平台 null
  4. 回退 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 / AppImagehttp://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,但两个窗口容易点错。

以后再碰到「开发有、打包没有」#

  1. gst-play / gst-discoverer 确认文件和主机解码器。
  2. 用同一套 WebKitGTK 探哪种 URI 能出 videoWidth。协议细节后改。
  3. 自定义 scheme 和 blob: 在这套栈上基本没戏,除非 GStreamer 改了行为。
  4. 本机 HTTP 能出帧之后,检查 CSP 是否带端口通配、日志里有没有 GET / Range。
  5. Wayland 上截屏当不了回归。
Linux 打包后视频壁纸零帧:WebKitGTK 不走自定义协议
https://fuwari.vercel.app/posts/linux-packaged-video-wallpaper/
作者
Hachi
发布于
2026-08-21
许可协议
CC BY-NC-SA 4.0
如何用第一性原理和对抗性审查指挥 Agent Debug
A2A、ACP 与 MCP 不在同一层