文章
Pepper 退役之后,播放器桥换成了独立进程
旧客户端通过 Pepper 插件把 mpv 嵌进 Electron。记录把播放桥换成独立 helper 进程的过程,包括消息协议、代际身份、退出路径与验收边界。
播放器桥是这个客户端里最早需要改的地方。旧实现走的是 Pepper 插件的老路,渲染进程里加载一个 application/x-mpvjs 插件,由它把 libmpv 嵌进页面。这条路在浏览器世界里已经没有维护者,客户端只是因为运行时足够老才能继续用。升级运行时的计划定下来之后,桥的替换变成了前置工作,和升级并行做完了。
换掉它还有两个具体原因。一次缓存数值排查里,页面显示的缓存上限变成了负数,最后确认原生侧设置是正确的,桥在回传数值时按 int32 截断了;另一次 ready 状态抖动诊断里,长尾没有复现,但旧链路里确实存在一个 ready 监听注册晚于事件、而且没有超时兜底的静态竞态。两件事指向同一个结论,桥需要自己的错误边界和明确的身份模型。
新桥的形状
替换的目标是让 mpv 继续在窗口里播放,同时把播放器从渲染进程搬出去。新的形态是一个独立进程 ete-mpv-helper.exe,由 Electron 主进程管理,libmpv 以 DLL 的形式链接在 helper 内部。上层的播放管理器、会话和上报链路不需要知道自己面对的是哪种桥,渲染侧端点保留原有的调用方式。
Emby Web / libmpv.js
-> restricted Electron IPC adapter
Electron main native-helper service
-> private inherited stdin/stdout pipes
ete-mpv-helper.exe
-> bundled mpv-1.dll
-> gpu-next / d3d11 / d3d11va
-> helper-owned WS_CHILD HWND
independent transparent main BrowserWindow
-> existing Emby HTML UI / OSD / input above video host
窗口归属是这套设计里最需要想清楚的部分。Electron 主进程创建一个无边框、不进任务栏、不可聚焦的视频宿主窗口,helper 的子窗口只挂到这个宿主上;主窗口继续负责界面、OSD、键鼠和焦点。宿主跟随主窗口的移动、缩放、最大化、恢复、全屏和最小化,主窗口销毁时,宿主和 helper 一起销毁。画面最终呈现的位置由宿主掌握,不需要渲染进程里的插件自己猜。
helper 用 C++17 写,工具链是 MSYS2 UCRT64 GCC 16.1.0,静态链接 libgcc 和 libstdc++,链接的 libmpv 固定在 mpv v0.41.0-920-gdd5d17d32,客户端 API 2.5。
有界的消息协议
helper 和主进程之间走一条私有的标准输入输出管道,帧格式是 4 字节小端长度前缀加 UTF-8 JSON,协议版本为 1。每一帧、接收缓冲、解码队列和两个方向的写队列都有上限,非法 UTF-8、超长帧、未知版本或类型、队列溢出全部按 fail closed 处理。握手阶段要核对协议版本、helper 版本、libmpv 版本、能力集、队列上限和原生表面,有一项不符就不进入 ready。
链路里有四个身份。渲染端点的 endpointId,helper 每次启动分配且不复用的 helperInstanceId,每次媒体加载的单调代际 generationId,以及每个请求的 requestId。旧端点的命令和销毁请求不可能作用到替换它的新实例上。
事件归属是这类桥最容易出错的地方,处理规则写得很细。加载事件通过 playlist_entry_id 绑定到具体代际;属性观察用 reply_userdata 对号;一条 FILE_LOADED 只有在原生状态里恰好存在一个唯一的、已映射的媒体身份时才被接受,否则隔离丢弃,不猜当前代际。B 成为当前目标之后,A 的事件、响应和错误恢复都不能再改变 B。
错误分成两层。协议、身份和 schema 层面的错误直接 fail closed;已经通过主进程白名单校验、但被 libmpv 拒绝的命令,返回一个带代际的非致命 operation-error,不终止 helper,也不让当前代际失效。这个区分是有意保留的,否则一次被拒绝的次要设置就能打断正在进行的播放。
崩溃与退出
helper 崩溃、管道关闭或协议失败,会原子地终止它名下的全部未完成请求,迟到的响应只记录、不生效。下一次播放会创建新的 helper 实例,身份与上一个不同。旧桥不会作为兜底自动启用,显式请求旧模式会得到一个确定性的拒绝。父进程被强制结束时,继承的管道读到 EOF,helper 自己退出。
写方向同样有界。高频属性按 helper、代际和属性名合并,关键帧耗尽预算时 fail closed,主进程侧的队列上限是 128 帧或 256 KiB,两者取先到者。
构建与运行时排除
helper 只从目标提交的 Git blob 现场编译,不读工作区的脏文件;编译输入里的 mpv 头文件来自固定提交并核对哈希。构建记录绑定源码 blob 哈希、头文件、编译器版本和参数、libmpv、协议版本以及 helper 自身的哈希和大小。运行时归档会把旧的插件产物排除掉,安装包里不再有 mpv-win32-x64.node,也没有 Pepper/PPAPI 相关载荷。
验证与边界
结构类验证覆盖了换片、连播、加载中停止和崩溃重建,每种各二十轮;父进程被杀后的清理、两万次属性突发、1 MiB stderr 和各类畸形帧的 fail closed 行为、文件级 User-Agent 隔离也都跑过。长跑样本连续播放 603.2 秒,工作集峰值约 102.8 MiB,首尾增长约 0.76 MiB。
真实 Emby 客户端的验收结论是完成。STRM 的原生回退、网盘直链命中、远程控制、单次下一集、会话与上报、状态统计和断点续播位置都通过了真实环境验证。同时保留了几条诚实的边界。普通文件媒体的验收在这个环境里标为不可用,因为媒体库整体是 STRM;两个几乎同时发出的远程下一集没有送达 WebSocket,定位在服务器语义这一层,记录为非阻塞;渲染层还有两个 ReferenceError 留作后续项。安装器的完整安装运行在发布门槛里补上了,HDR 和真实混合 DPI 场景仍然没有覆盖。
替换完成后,运行时里的 Pepper/PPAPI 桥退役,helper 成为唯一的生产播放路径,失败时不自动回退。这些变化随 v0.2.0 发布。
Sources
[1] https://github.com/hope140/EmbyTheaterEnhanced Emby Theater Enhanced 仓库 [2] https://github.com/hope140/EmbyTheaterEnhanced/releases/tag/v0.2.0 v0.2.0 Release,Native Helper 替换 Pepper/PPAPI [3] https://github.com/hope140/EmbyTheaterEnhanced/blob/main/docs/NATIVE_HELPER_BRIDGE.md 播放桥的实现契约