本文主要介绍 Octans Android 客户端播放内核从「LGPL libmpv 单内核」到「默认 libmpv + Media3 ExoPlayer 本地实验」的选型与演进过程,以及 HTTPFILE / HLS、字幕、硬解等有证据支撑的能力边界。讨论的是决策可被 supersede 的工程取舍,而不是一套永恒正确的播放器教条。
媒体库客户端和「普通视频 App」面对的约束并不一样。Octans 的目标是尽量保住 DirectPlay / HTTPFILE 主链,让真实样片(高码率容器、复杂字幕、高级音频、HDR / Dolby Vision 组合)能在客户端本地打开,而不是默认压成 HLS-only 或一律服务端烧录。Android 上可选的播放路径大致是 Media3 / ExoPlayer、libmpv / FFmpeg 系 native 内核,以及二者组合;每一条路线都会牵动许可证、APK 体积、系统集成、选轨语义和跨端协议。
下面按真实决策时间线说明:首版为什么先锁死单内核,以及大约两周后为什么又把「禁止双内核实验」这条长期约束打开。
背景:Android 媒体客户端的播放约束
对 Octans 这类私有媒体库客户端,播放栈至少要同时回答几类问题:
- 交付形态:优先 HTTPFILE(带签名 URL 的 Range 直开),HLS 作为转码 / DirectStream fallback,而不是反过来。
- 字幕多样性:外挂 ASS / SRT / WebVTT、位图类 PGS / SUP、内嵌轨与 sidecar 轨并存;HLS 上还要处理 timeline 与
mpvSubtitleDelay一类补偿。 - 编解码与显示:设备是否支持 HDR / Dolby Vision 硬解,非 DV 设备上 Profile 5 是否偏色,软解是否只在高性能机上可接受。
- 状态一致性:起播、pause / resume、seek、选轨、进度上报、session 释放、错误诊断最好走同一套语义;多内核并行时很容易拆成两套生命周期。
- 许可证与制品边界:客户端消费可审计的 LGPL runtime,与服务端 full / GPL FFmpeg 制品分离,避免把服务端组件拖进闭源 App。
初始化阶段把 Media3 当作默认主线、把 mpv 当高风险增强,在「先定 App 边界、少引入 native 构建链」时是合理的。一旦验收标准变成「真实媒体库样片能 DirectPlay」,缺口会集中暴露在外置 PGS、复杂容器与音轨、以及部分 HDR / DV 显示路径上。继续在 Media3 主线上叠 sidecar PGS stack、libass overlay、服务端 burn-in 或隐式双内核 fallback,播放状态、seek、选轨和进度上报很容易分裂。
因此首版问题可以收敛成一句:是优先平台集成成本,还是优先一套能覆盖复杂样片的播放内核。
阶段一:为何首版选 LGPL libmpv 单内核
约 2026-06 初,Android 首版播放主线被固定为:
Kotlin / Jetpack Compose 原生客户端
+ LGPL libmpv 单一播放内核
+ 受控 Android native runtime artifact(按 ABI 发布)
+ DirectPlay / HTTPFILE 主链
+ HLS fallback
+ HLS 侧完整 sidecar 字幕(配合 subtitle delay 补偿)
UI、ViewModel 与 use case 不直接调 mpv C API,而是经过统一的 playback engine 抽象;App 只消费独立 runtime 仓产出的 LGPL 制品,不在 App 仓维护 native 源码构建链,也不复用服务端 full FFmpeg。
当时拒绝的备选
| 方案 | 首版结论 | 主要原因 |
|---|---|---|
| Media3 / ExoPlayer 单内核 | 拒绝作主线 | 平台集成最轻,但外置 PGS、部分高规格音轨、非 DV 设备上 DV P5 显示等会持续扩大 DirectPlay 缺口 |
| Media3 主线 + libmpv fallback | 拒绝作主线 | 看似降风险,实际把状态机、错误码、选轨、进度与恢复策略拆成两套;个人主线阶段复杂度高于先把一条受控 libmpv 主线做实 |
| LGPL libmpv 单内核 | 接受 | 用一套内核覆盖复杂字幕、MKV、HDR / HLG / DV 与高级音频组合,并与 Windows 端 LGPL runtime 治理经验对齐 |
收益
- DirectPlay 优先:不为了「更像标准 Android 播放器」把首版默认压成 HLS-only。
- 能力红线可验证:外置 PGS / SUP、非 DV 设备上的 DV P5 软解等,在对照与 smoke 中确实拉开差距;复杂 ASS 与普通路径则未必有决定性体验差,不必夸大。
- 链路单一:播放页、选轨、seek、进度 / heartbeat、错误诊断与日志语义集中在一套 engine 上。
- runtime 治理可复用:独立 LGPL runtime 制品、许可证、source offer / SBOM 等边界已经在其他桌面端演进过,Android 扩展 ABI 轨道比在 App 内另起一套 native 构建更可控。
代价
- 引入 NDK 依赖、LGPL 合规与审计、APK 体积与 JNI / Surface 稳定性成本。
- 系统侧 MediaSession、音频焦点、Cast、部分平台解码器行为等,不能假设「选了 libmpv 就免费获得 Media3 级集成」。
- 外置 PGS 只能写成「当前更可能显示的路径」,不能写成完整稳定产品能力;seek 后残留或提前显示在桌面 mpv 上也能复现时,不宜假装是客户端预处理就能彻底修掉。
- DV P5 软解属于 CPU / native 内存敏感 能力:高性能设备上长时间播放与多次 seek 可以成立,低性能或低内存设备应优先走服务端转码 / tone mapping,而不是无条件承诺本地软解。
首版能力口径(有据部分)
复核与 smoke 收口后,当时可以写进产品口径、且仍被后续决策保留为「libmpv 主线事实」的包括:
- HTTPFILE 签名 URL:起播、pause / resume、短 seek / 长 seek、Range seek、telemetry 与同 token session 释放可跑通。
- 服务端转码 HLS:非零 window 起播、同窗 seek、pause / resume、连续播放,以及 sidecar 字幕 + delay 补偿路径。
- HLS 正式字幕产品路径:
HLS 主 URL + complete / raw sidecar + 字幕 delay,而不是依赖 mpv 对 HLS WebVTT rendition 的内部sid映射(rendition 更适合诊断对照)。 - HTTPFILE 选轨语义:
ffmpegStreamIndex → mpv track-list 的 ff-index → id → vid/aid/sid;HLS 视频 / 音频切换走服务端 session recreate / supersede,不在当前 mpv handle 里假装源媒体轨号仍然有效。 - ABI:正式主线以
arm64-v8a为主;后续补过 32 位 runtime 验证后,universal APK 可同时携带多 ABI,但 32 位仍宜写成「已验证设备范围内 / 实验性支持」,高规格 4K / 高码率 REMUX 直开另议。
需要强调的是:这份单内核决策后来被明确 supersede。被替代的是「长期禁止 Media3 / 双内核实验」这一约束;不是否定 libmpv 已通过的 HTTPFILE / HLS / 选轨事实,也不是立刻把默认内核改成 ExoPlayer。
阶段二:为何演进到双内核实验与分工
约 2026-06 中下旬,在 libmpv 已完成首轮电影播放 MVP、生产契约仍是 native/libmpv 形态的能力 schema 之后,演进决策改成:
- 正式默认内核继续是 libmpv;服务端与各端生产契约暂不因 Android 实验而整体迁版本。
- Android 可以增加本地播放内核偏好:默认 libmpv;
Media3 ExoPlayer仅作为本地实验内核,最小 engine 未就绪前 UI 隐藏或禁用。 - 实验期不提供 Auto,不做自动 fallback。用户显式选 ExoPlayer 后,若本地 gate 拒绝当前 descriptor,直接展示拒绝原因,释放已创建的服务端 playback session,并清理本地播放状态。
- 实验期可以继续创建既有 playback session 拿 URL 与 descriptor,但不得把 libmpv 专用字段(sidecar 形态、mpv delay、native 选轨控制等)解释成 ExoPlayer 能力契约。
- 面向多内核的下一版能力 schema 后置:至少要等 ExoPlayer 最小内核、HTTPFILE / HLS 选轨、PGS overlay、设备运行时 decoder 证据过线后再设计;且应是并行新增,不能强迫 Web / Windows / Android libmpv 路线同步迁移。
- PGS / SUP 不能压成一个
supportsPgs=true布尔值;能力模型必须区分 libmpv sidecar、ExoPlayer overlay、服务端 burn-in、不支持、degraded 等呈现路径。
为什么不再「长期单内核到底」
长期只保留 libmpv 最简单,也能继续覆盖复杂字幕、软解与样片矩阵,但会把 Android 系统媒体栈在设备支持范围内的 HDR / Dolby Vision 硬解与显示链路收益 长期关在门外,也无法用产品数据回答:在设备本来就能硬解的场景里,ExoPlayer 是否其实是更合适的默认路径。
反过来,立刻把 ExoPlayer 设为默认或上 Auto 同样被拒绝:HTTPFILE 目标轨是否可靠选中、外置 PGS overlay 能否产品化、复杂 ASS / 外置音轨 / 高级音频与 passthrough 如何 gate、运行时 decoder 是否真的落入硬件路径——这些在决策当时都还不够硬。过早默认化会放大跨仓库协议面,并把 session 生命周期与 fallback 事务提前复杂化。
因此接受的是中间态:Android 本地实验先行,跨端协议后置。保留 libmpv 正式主链承接复杂字幕、既有选轨与软件兜底;用显式、可拒绝的 ExoPlayer 通道去收集系统硬解与 Surface 路径证据。gate 失败明确报错并释放 session,比自动静默 fallback 更容易定位问题,也避免把实验失败伪装成「播起来了」。
后续补强的一条边界:外置音轨
双内核打开实验通道之后,外置音轨又单独收过一刀:Media3 官方 sideload 入口主要面向字幕配置,并没有与 audio-add 对等的产品级外置音频 API;用底层 MergingMediaSource 拼主视频 + 独立音频 URL 在真机上选中、override、extractor 识别与恢复都不稳定。于是产品口径变为:
- libmpv:继续走既有外置音轨挂载能力。
- ExoPlayer 实验路径:拒绝「主视频 URL + 再挂一个独立音频 sidecar URL」的客户端原生模式;内嵌多音轨或 HLS / DASH manifest 声明的音轨仍可讨论;若未来要在 Exo 路径播带外置音的片源,更合理的是服务端 remux / package 成单容器或标准多音轨输出。
- 若以后出现 Auto,无可用的标准输出时默认 libmpv,不要把 ExoPlayer 送进必败 gate。
这再次说明:双内核不是「两个播放器功能并集自动取 max」,而是按路径声明能力、按 gate 拒绝、按证据升格。
能力边界:只写有据结论
下面汇总跨两份决策与后续边界文档后,仍适合对外表述的分工。未写进证据的项目保持「未承诺」。
交付与选轨
| 场景 | libmpv(正式默认) | Media3 ExoPlayer(实验) |
|---|---|---|
| HTTPFILE 直开 | 主链;按 ff-index 映射选轨 | 需单独证明目标视音频轨可靠选中;未过线前不得当默认 |
| HLS | 可播;视音频切换偏服务端 session 重建;字幕走 sidecar + delay | 同样不能复用源媒体 vid/aid/sid 语义;能力契约与 libmpv 字段隔离 |
| 进度 / session | 与现有 native 契约对齐 | gate 失败必须释放服务端 logical session 与本地状态 |
字幕
| 类型 | 口径 |
|---|---|
| 外挂 ASS / SRT(HTTPFILE 与 HLS sidecar) | libmpv 主线已按 complete ASS / raw VTT 等路径验收;HLS 依赖 delay 补偿 |
| 外置 PGS / SUP | libmpv:可显示路径,但 seek 稳定性不足时标 degraded;ExoPlayer:不能把 .sup 直接当成开箱即用的完整字幕能力,需自研 envelope / segment / timeline / overlay |
| HLS WebVTT rendition | 更适合诊断对照,不宜当作唯一产品字幕语义 |
| 能力建模 | 禁止单一布尔 supportsPgs;必须区分呈现路径与质量等级 |
音频与软硬解
| 类型 | 口径 |
|---|---|
| 内嵌多音轨 | 两内核都可能覆盖;以实际 track selection 为准 |
| 独立外置音频 URL | libmpv 可走挂载;ExoPlayer 不作为产品能力(应 remux / 标准多音轨输出) |
| 非 DV 设备上的 DV P5 | libmpv 软解可修正色彩,但是 CPU / 内存敏感;Media3 默认路径在对照中出现过明显偏色,不能当成已解决 |
| 设备原生 HDR / DV 硬解 | 正是打开 ExoPlayer 实验通道的主要动机之一;需运行时 decoder 证据,不能写进未验证承诺 |
| 客户端 runtime | 始终是 LGPL 受控制品;禁止把服务端 full FFmpeg 拖进 Android 本地播放依赖 |
架构示意(演进后)
用户 / 设置:播放内核偏好(默认 libmpv;Exo 实验项可隐藏)
│
▼
PlaybackEngine 抽象
├─ LibMpvPlaybackEngine ──► native LGPL runtime ──► HTTPFILE / HLS(正式)
└─ Media3ExoPlaybackEngine ──► 本地 gate ──► 通过则起播 / 拒绝则报错并释放 session
│
▼
既有服务端 playback session(生产契约仍以 libmpv 形态为主)
· 实验期:descriptor 当输入事实,不当作 Exo 能力声明
· 多内核并行契约:证据足够后再设计,且与旧契约并行而非强迁
对同类产品的通用启示
- 先锁一条可验收主线,再谈抽象并集。双内核的状态机、协议字段和失败语义成本,往往高于「先把复杂样片在单内核上跑通」。
- 把「默认内核」和「允许研究的内核」写成不同约束。禁止实验会让平台收益永久门外;过早默认化会把未证明能力扩散成跨端合同。
- 能力用路径描述,不用布尔广告。PGS、DV、外置音轨都出现过「能显示 / 能出声 ≠ 产品可用」。
- gate 失败要显式,fallback 要可审计。静默切内核会污染进度、字幕和用户对「是否硬解成功」的判断。
- 客户端 LGPL 与服务端 full 运行时分离,从第一天就划清,比事后拆依赖便宜。
- 决策文档写清 supersedes / superseded_by。首版「禁止双内核」是阶段性边界,不是物理学定律;后续读者应看到演进关系,而不是互相打架的两份「当前真理」。
注意事项与局限
- 本文是公开向的技术选型 / 架构演进说明,不是发布说明,也不承诺具体版本时间表或设备白名单。
- 真机样片矩阵、温度、PSS、掉帧等数字会随设备、系统版本与 runtime 构建变化;文中只保留「高性能设备软解可接受 / 低端应转码」这类分级口径,不把单次 benchmark 写成普适性能表。
- Media3 实验路径上的 HTTPFILE 选轨、PGS overlay、运行时硬解证据等,在演进决策写入时仍属待证明项;未过线前不得在对外能力表里写成已支持。
- Auto、跨端并行能力 schema、fallback 事务属于后置设计,本文不展开未落地协议细节。
- 竞品(双内核播放器、增强 ExoPlayer 等)只适合作为问题意识参考,不能替代本项目的许可证、契约与真机证据。
到这里,可以把 Android 播放内核演进压缩成三句话:首版用 libmpv 单内核换 DirectPlay 与复杂样片的一致性;在主线可验收后,用本地、显式、可拒绝的 ExoPlayer 实验去验证系统硬解收益;默认内核、生产契约与能力广告始终跟证据走,并允许下一份决策名正言顺地 supersede 上一份。
相关阅读
同系列还可对照:
- 媒体库播放排障:从会话创建到 HLS / DirectPlay / 字幕链路:服务端会话、HLS window 与字幕控制面的分场景定位。
- 媒体内容产品 + 管理后台混合前端的 UI 基座选型:Web 端自有 UI 边界;多端策略是「Web 做基线、原生各走平台 UI」。
来源
本文改写自 Octans 项目决策层文档中关于 Android 播放内核首版选型与双内核实验边界的说明,并吸收后续外置音轨等能力收口结论。表述面向公开技术选型,不绑定内部仓库路径与未发布版本号。