BiliDesk:Tauri + libmpv 桌面客户端——WBI 签名、DPAPI 会话与弹幕转 ASS

约 7 分钟阅读桌面应用

文章目录9 节

BiliDesk 是用 Tauri + React + libmpv 写的 Windows B 站桌面客户端。本文讲它怎么在 Rust 侧实现 WBI 签名、怎么用 DPAPI 保护登录态、怎么把弹幕转成 ASS 交给 mpv,以及为什么封面要走自定义协议。

用"网页壳"做 B 站客户端很省事,但一旦要内嵌播放器、要自己管弹幕、要把登录态安全地落在本地,就必须把关键逻辑搬到原生侧。BiliDesk 的选择是 Tauri 2 + React + libmpv:界面用 Web 技术,签名、会话、播放器控制、弹幕转换全部在 Rust。

一、先解决"播放器怎么装"

桌面端打包最容易被忽略的是外部二进制依赖。BiliDesk 用 libmpv,并把获取过程写成脚本 scripts/fetch-mpv.sh / .ps1:

  • 固定上游与版本:zhongfly/mpv-winbuild tag 2026-08-21-49418246f3,资产名 mpv-dev-lgpl-x86_64-20260821-git-49418246f3.7z;
  • 校验 SHA-256(317dfd9e…0510e0,用 sha256sum / shasum 校验)后才解压;
  • 想换版本必须同时提供 MPV_RELEASE / MPV_ASSET / MPV_SHA256 三个环境变量,避免"只改了 tag 导致校验失效";
  • 已经存在时跳过,除非 MPV_FORCE=1。

这条规则的意义在于可复现:CI 和本地拿到的是同一个二进制,libmpv_search_starts_in_vendor 这类测试才有意义。

二、WBI 签名:把"能跑"变成"可验证"

B 站的 Web 接口对部分端点要求 WBI 签名(w_rid)。这类算法的常见问题是:写完只能靠"接口没报错"来验证。BiliDesk 的做法是把官方公布的示例作为单元测试,并且把"去掉非法字符"这一步单独覆盖——签名是纯函数,纯函数就该有断言。

配套的请求侧约束:

  • 分区/排行等接口按"官方 PC 端契约"构造 URL,并对响应做校验,把鉴权失败、风控、限流等业务码原样保留(而不是统一吞成"请求失败");
  • logout 会清掉 SESSDATA、bili_jct、DedeUserID、DedeUserID__ckMd5、sid 这几项,不留下半套凭据。

三、会话保护:魔法头 + DPAPI

session.json 不是明文 cookie 转储:

  • 文件以 BILIDESK_SESSION_V1\0 作为魔法头;
  • 内容用 Windows DPAPI(CryptProtectData,CRYPTPROTECT_UI_FORBIDDEN)加密,绑定当前用户;
  • 首次读取到旧的明文文件时自动迁移成加密格式——迁移而不是报错,这决定老用户升级后的体验;
  • PRIVACY.md 把边界写清楚了:无遥测、无广告、无云同步;DPAPI 只防"文件被拷走",不防同用户下的恶意程序,也不防运行时内存读取;数据在 %APPDATA%\com.bilidesk.desktop,卸载后可能保留。

本地数据用 SQLite(bilidesk.db):history、settings、play_progress、schema_migrations,历史迁移版本 HISTORY_MIGRATION_VERSION = 1。老的 history.json 会被导入并改名为 history.json.migrated——保留原件,这在数据迁移里是低成本高价值的习惯。

四、封面与图片:为什么不用 <img src="https://...">

直接让 WebView 加载 B 站 CDN 图片,等于把用户的 Referer/IP 直接暴露给第三方,也会让 CSP 变得很松。BiliDesk 改走自定义协议:

  • Rust 侧注册 biliimg 协议作为封面代理,media.rs 里维护主机白名单(只允许 B 站系 CDN,测试 allows_bili_cdns / rejects_other_hosts),并对响应大小设 8 MiB 上限(超出报"封面过大");
  • 前端按平台拼 URL:Windows WebView2 下用 http://biliimg.localhost/img?u=…,其他情况用 biliimg://localhost/…;
  • CSP 因此可以收紧到 img-src 只允许该主机、connect-src 只留 ipc:、object-src 'none'、frame-ancestors 'none'。

"所有远程图片都经过自己的白名单代理"是一条很好复用的模式:可缓存、可限流、可限大小、可审计。

五、弹幕 → ASS → mpv

弹幕不是叠加在 WebView 上,而是转成 ASS 字幕交给 mpv 渲染(bili/danmaku.rs):

  • 默认字体 48、行数 12、不透明度 1.0、显示区域 1.0、加粗;行高按 font + 8;显示区高度按 play_y * display_area 折算;
  • 宽度估算用 字符数 × font × 0.7,用来决定滚动轨道的碰撞与占用;
  • 生成 Scroll / Top / Bottom 三种 ASS 样式,字体族 Microsoft YaHei;不透明度同时映射到 ASS 的 alpha 值和阴影距离((1-opacity)*255、*96),保证半透明时仍然有可读性。

弹幕偏好(字号 28–72、行数 4–20、不透明度 0.1–1.0、显示区域 0.25–1.0)在写入和读取两侧都做钳制(player_set_danmaku_prefs 与加载路径都校验)。只在前端限制等于没限制——配置文件是可以手改的。

六、分区/发现与状态

  • 分区 tab 的合并是显式设计过的:?tab=ranking 与 ?tab=region 合并,OGV 分区 id 13/167/168 回落到"动画",rid=0 表示全站,不做分页;
  • 播放进度、音量、倍速、主题、弹幕开关等落到 settings,默认值在 Rust 侧集中定义(theme=light、danmaku=true、font=42、rows=10、volume=80、speed=1);
  • 播放器命令做了作用域取消:切换视频时旧的加载作用域会被取消,避免"上一个视频的响应把新视频的状态覆盖"。

七、验证

  • 前端:11 个 vitest 文件(media、errors、regions、region-feed、hotkeys、discover-tabs、watch、wheel-navigation、version、releaseVersions…),例如 media.test.ts 断言按 UA 选择 URL 方案、空值直接透传;
  • Rust:13 个模块内 #[cfg(test)] 约 23 个测试,覆盖 WBI 示例与非法字符、弹幕密度/区域/透明度、媒体白名单、分区排行 URL 与响应校验、历史往返与损坏 JSON 再导入、播放器作用域取消、libmpv 定位;
  • CI:rust-audit(rustsec audit)之外,quality job 在 windows-latest 上跑"下载固定 libmpv → check:release → build → test → npm audit --audit-level=high → cargo fmt --check → clippy -D warnings --all-targets --all-features → cargo test --locked";tag 触发的安装包 job 支持可选证书签名。

八、限制

不做直播流、不下载缓存、不破解会员、不做区域绕过;上游 Web 接口变化可能影响功能;没有配置证书时安装包未签名;设计文档(docs/superpowers/specs/)明确了"UI 不直接请求 B 站域名"的边界,但也承认区域/发现页的完整契约仍依赖第三方接口的稳定性。

小结

这类"第三方客户端"项目的技术含量不在界面,而在三件事:外部二进制可复现(版本 + 哈希 + 显式覆盖变量)、凭据用系统能力保护并显式写出防护边界、把算法(WBI)与渲染(弹幕 ASS)变成可断言的纯逻辑。

仓库:sakuradairong/bilidesk

搜索文章

按 / 打开搜索,Esc 关闭;多个关键词用空格分隔,搜索在本地完成。