本文主要介绍 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 之后,演进决策改成:

  1. 正式默认内核继续是 libmpv;服务端与各端生产契约暂不因 Android 实验而整体迁版本。
  2. Android 可以增加本地播放内核偏好:默认 libmpv;Media3 ExoPlayer 仅作为本地实验内核,最小 engine 未就绪前 UI 隐藏或禁用。
  3. 实验期不提供 Auto不做自动 fallback。用户显式选 ExoPlayer 后,若本地 gate 拒绝当前 descriptor,直接展示拒绝原因,释放已创建的服务端 playback session,并清理本地播放状态。
  4. 实验期可以继续创建既有 playback session 拿 URL 与 descriptor,但不得把 libmpv 专用字段(sidecar 形态、mpv delay、native 选轨控制等)解释成 ExoPlayer 能力契约。
  5. 面向多内核的下一版能力 schema 后置:至少要等 ExoPlayer 最小内核、HTTPFILE / HLS 选轨、PGS overlay、设备运行时 decoder 证据过线后再设计;且应是并行新增,不能强迫 Web / Windows / Android libmpv 路线同步迁移。
  6. 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 / SUPlibmpv:可显示路径,但 seek 稳定性不足时标 degraded;ExoPlayer:不能把 .sup 直接当成开箱即用的完整字幕能力,需自研 envelope / segment / timeline / overlay
HLS WebVTT rendition更适合诊断对照,不宜当作唯一产品字幕语义
能力建模禁止单一布尔 supportsPgs;必须区分呈现路径与质量等级

音频与软硬解

类型口径
内嵌多音轨两内核都可能覆盖;以实际 track selection 为准
独立外置音频 URLlibmpv 可走挂载;ExoPlayer 不作为产品能力(应 remux / 标准多音轨输出)
非 DV 设备上的 DV P5libmpv 软解可修正色彩,但是 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 上一份。

相关阅读

同系列还可对照:

来源

本文改写自 Octans 项目决策层文档中关于 Android 播放内核首版选型与双内核实验边界的说明,并吸收后续外置音轨等能力收口结论。表述面向公开技术选型,不绑定内部仓库路径与未发布版本号。