把 mpv 装进身体里:NovaPlayer 的 libmpv 内置长征
跟随窗模式毛病不断,决定把播放内核编进应用。三版分诊包、真机全黑、最后出画的那一格,病灶不在解码,而在窗口是谁管的。
- 起点 — 跟随窗毛病结构性,决定把 libmpv 编进应用先做实验台
- 真机第一课 — 管线全绿唯独无画面,病灶不在解码
- 分诊 — 渲染模式 × 窗口安排逐格点测
- 决定性的一格 — 原生 Win32 窗口喂给 libmpv,出画了
- 定版 — Electron 外框 + 原生视频视图 + 透明覆盖层 UI
起点:跟随窗的账算不下去了
NovaPlayer 原本的播放方式是「跟随窗」:一个独立的 mpv 进程开个窗口,实时贴合主窗口的播放区。能用,但毛病是结构性的:点击被视频窗吞掉、窗口几何一对不齐就露馅、全屏要两边商量。学同类产品的做法——把 libmpv 编进应用、画面渲染进自有视图——成了迟早要走的一步。
先做独立实验台验证:Electron 进程内经 koffi 直调 libmpv 的 C API。在沙箱里同链路一次出画,模块自测 15/15 全过(seek、倍速、音量精确回读,轨道、外挂字幕、截图、结束归因一应俱全)。当时以为稳了。
真机第一课:全绿,但是全黑
实验包到用户真机(Intel 核显、125% DPI)上,三个渲染模式全黑:有声音、有进度、丢帧 0,播放管线每一项指标都正常,唯独没有画面。对照事实是:同一台机器上,独立运行的 mpv.exe 能正常出画。硬解、软解、渲染后端逐个换掉,黑依旧。
这说明病灶不在解码链路,而在「画面最后停在哪块窗口表面上」。
分诊:把两个变量拆开
第二版实验包把问题拆成正交的两维:横轴是渲染模式(gpu-next / gpu / Vulkan / OpenGL),纵轴是窗口安排(子窗嵌入 / 独立顶层窗 / 关掉 flip 呈现)。逐格点测、逐格留日志。顺路还抓出实验台自己的两个 bug:koffi 的类型定义在切换模式时重复注册会直接崩;Vulkan 后端起不来是因为这版构建里上下文名字叫 winvk,不叫 vulkan——选项写错了一个词,整格就从没真正测过。
决定性的一格:原生窗口托管
第三版只测三格,其中 E 格是新思路:不用 Electron 的窗口装画面,改由主进程自己建一个原生 Win32 窗口,把窗口句柄直接喂给 libmpv。
出画了。 测试彩条清晰、25fps、丢帧 0/0。
病灶至此定案:不是解码、不是驱动、不是渲染后端——是 Chromium 管理的窗口表面承载不了这条渲染路径。实验台其余所有组合皆黑,唯独绕开它就通。
定版
生产架构就此定为:Electron 外框 + 原生视频视图 + 透明覆盖层 UI。起播时「先黑一会儿再出画」的小毛病,用「首帧到达才亮出窗口」收尾。libmpv 的 DLL 本体 121MB,实验包 161,077,945 B——体积是这条路线明码标价的代价。
1.12 正式版按这套架构装配。但长征结束不等于太平——正式版的贴合问题,是下一篇的事。
演示代码块
下面两段代码不是正文,是用来演示代码块特效的(取自本博客自己的构建流程):语言标签、行号、复制按钮;超 12 行的默认折叠,点「展开」看全文。
# 博客日常:加文章 -> 重新生成 -> 部署
python3 build.py
python3 deploy.py
# 预览站同款流程(新项目)
python3 build_preview.py
python3 deploy_preview.pyimport re
def toc_sections(body_html):
"""从文章 HTML 里抽 h2 章节,供目录面板用。"""
heads = []
def _pick(m):
text = re.sub(r"<[^>]+>", "", m.group(1))
heads.append(text)
return '<h2 id="sec-%d">%s</h2>' % (len(heads), m.group(1))
body = re.sub(r"<h2>(.*?)</h2>", _pick, body_html)
return body, heads
if __name__ == "__main__":
html_out, sections = toc_sections(article_body)
print("%d 节:%s" % (len(sections), " / ".join(sections)))