Tauri 桌面 App 自包含:内置静态 ffmpeg + 把 OpenCV 算法移植到 Rust
做了一个 Tauri 2 视频处理桌面 App,在开发机上运行完美:ffmpeg 转码、ffprobe 探测、Python sidecar 做镜头切分,一切正常。打包成 .dmg 发给测试同学,依赖自检全红——对方机器没装 ffmpeg,也没有 Python/opencv 环境,核心功能全废。
要做到"双击即用",就必须把运行时依赖塞进包。这条路上有两个特别反直觉的坑,踩完才明白为什么。
现象
- 生产
.dmg打开后,依赖自检显示 ffmpeg/ffprobe「未就绪」; resolve_bin("ffmpeg")返回裸名,走系统 PATH,用户机没有就直接报错;- Python sidecar(opencv/scenedetect)在没有
.venv的机器上完全不可用; - 开发机一切正常,所以这个问题在发布前很难被发现。
根因
Tauri 不会自动把系统里的 ffmpeg 带进包——它只管 bundle 你显式声明的资源。Command::new("ffmpeg") 本质上是在运行时找系统 PATH,打包后完全没有这个可执行文件。Python sidecar 依赖虚拟环境,而 cv2/numpy 原生 wheel 极难做可重定位,塞进 bundle 会带来几十上百 MB 的体积,且重定位极脆。
homebrew 的 ffmpeg 动态链接 /opt/homebrew/... 下一大堆 dylib(libavcodec、openssl、x264……)。用 install_name_tool 逐一重定位这几十个库,脆且费时,且每次 brew upgrade 都会破坏。正确做法是从静态构建拿二进制。
验证:otool -L ffmpeg | grep -v "/System/\|/usr/lib/" 应只剩二进制本身,无任何 /opt 路径。
scene/scdet 是硬切检测器,只看相邻帧的瞬时差异。对 AI 生成的连续 morph、慢变长镜头,逐帧差异都极小,不触发阈值,整段被当成一镜。自研的 OpenCV HSV 直方图 + Bhattacharyya 累积漂移检测器的灵魂正在于此——不能用 ffmpeg 内置滤镜简单替代。
正解
1. 拿静态自包含 ffmpeg
从 https://ffmpeg.martin-riedl.de/ 下载 macOS arm64 静态构建(~63 MB/个)。静态构建含 libx264(CPU 软编回退)+ h264_videotoolbox(GPU 硬编)+ scdet/select 滤镜,且无外部动态库依赖。
注意:该构建是 GPL(含 x264/x265),对外分发需遵守 GPL。二进制不要进 git(127 MB),放 src-tauri/binaries/,写个 fetch-ffmpeg.sh 在 CI 拉取。
2. 作为 resource 打包,运行时优先用包内
tauri.conf.json:
"bundle": {
"resources": {
"binaries/ffmpeg": "bin/ffmpeg",
"binaries/ffprobe": "bin/ffprobe"
}
}Rust 侧用 OnceLock 缓存路径,resolve_bin 优先走包内,dev 模式回退系统 PATH:
static BUNDLED_BIN_DIR: OnceLock<PathBuf> = OnceLock::new();
fn resolve_bin(name: &str) -> String {
if let Some(dir) = BUNDLED_BIN_DIR.get() {
let p = dir.join(name);
if p.exists() { return p.to_string_lossy().into(); }
}
name.to_string() // 回退系统 PATH(dev 模式)
}setup() 中优先读 resource_dir()/bin,找不到则回退 CARGO_MANIFEST_DIR/binaries(dev)。
3. 预签名 bundled 二进制(公证前置条件)
Tauri 不递归签 resources 里的二进制。必须在 tauri build 之前签好源文件(签名随文件复制进 bundle):
codesign --force --timestamp --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" src-tauri/binaries/ffmpeg
codesign --force --timestamp --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" src-tauri/binaries/ffprobe三个 flag 缺一不可:--timestamp(公证要求)、--options runtime(hardened runtime)、--force(覆盖已有签名)。验证:codesign -dvvv .../Resources/bin/ffmpeg 应显示 flags=0x10000(runtime) + Timestamp + 你的 TeamID。
静态构建无外部依赖,otool -L 干净;tauri build 前预签好源文件,签名随 copy 进 bundle;codesign --verify --deep --strict 通过,spctl --assess 显示 Notarized Developer ID。
4. 用 ffmpeg rawvideo 管道把 OpenCV 算法移植到 Rust
不需要 cv2,让 bundled ffmpeg 解码+缩放+采样,吐 rgb24 裸流到 stdout:
ffmpeg -v error -i <path> -vf "fps=3,scale=160:90" -f rawvideo -pix_fmt rgb24 -
Rust 用 read_exact(&mut buf)(buf = W×H×3 字节/帧)按帧对齐读取,UnexpectedEof 干净收尾。
OpenCV 的 Bhattacharyya 公式必须 1:1 复刻,才能让已调好的阈值无缝迁移:
- BGR→HSV(8-bit,H∈[0,180]):
V=max(r,g,b); ch=V-min; S=V==0?0:ch*255/V,H 按 OpenCV 定义做整数除法后/2存为 H180 - 直方图 bins 例
[16,16,8],之后做 L2 归一化(对齐cv2.normalize默认 NORM_L2) - HISTCMP_BHATTACHARYYA:
d = sqrt(1 - Σ√(h1ᵢ·h2ᵢ) / √(H̄1·H̄2·N²))
必做交叉验证:写 Python 脚本,对随机帧分别用 cv2 真实管线和「Rust 公式的 Python 重写版」算 Bhattacharyya,diff < 0.01(实测可达 < 0.003)即说明阈值可无缝迁移。
ffmpeg rawvideo 管道 → Rust 按帧读取 → HSV 直方图 + Bhattacharyya 累积漂移。cv2 交叉验证 max diff 0.003,阈值无需重调。最终 dmg 无 Python/scenedetect/opencv 依赖,体积可控。
一句话外卖
桌面 App 打包自包含的核心是「消灭隐式 PATH 依赖」:静态二进制进 resource、预签名、
resolve_bin优先走包内;Python sidecar 的算法迁移到 Rust 比塞整个 Python runtime 可靠得多,但必须对着 cv2 做数值交叉验证才能保证阈值可迁移。