统一后台播放宿主设计
背景
当前项目同时存在两条独立的音乐播放链路:
LocalMusic.ets 页面内播放器链路
- 页面持有
mIjkMediaPlayer
- 页面直接负责起播、暂停、切歌、进度控制
- 页面直接负责 AVSession 更新
BackgroundAudioPlaybackHost.ets 后台宿主链路
- 卡片冷启动控播走后台宿主
- 强杀后卡片播放由后台宿主恢复队列并起播
这两条链路同时存在,直接导致以下问题:
- 进入 app 后,页面播放条和后台宿主状态不一致
LocalMusic 本地播放和卡片后台播放会双播
- AVSession / 卡片 / 页面显示不是同一真源
- “强杀后卡片播放可用但不拉前台”和“进入 app 后页面无缝接管”无法同时满足
目标
统一为唯一播放链路,满足以下行为:
- 唯一播放器永远是后台独立宿主,不再由
LocalMusic 持有另一套本地播放器。
- 音乐卡片、后台控播、进入 app 后的页面控播,全部走同一条播放路径。
- 强杀 app 后,点击卡片播放可以直接播放,不拉起 app 前台。
- 点击卡片进入 app 时,页面只能“接管显示和控制”,不能重新
doPlay()、不能重建播放器。
- 页面上的当前歌曲、播放状态、进度、时长、封面、AVSession 播放条,都以后台宿主为唯一真源。
- 彻底消除双播。
非目标
本次第一轮统一不追求一次性重构所有复杂播放分支。
以下内容不作为第一阶段必须完成项:
- 所有投播分支的彻底统一
- 所有特殊格式的完整播放能力迁移
- 所有歌词、均衡器、HiCar、画中画等衍生能力的一次性全量迁移
第一阶段优先解决“普通音乐主链路”的统一,包括:
- 卡片控播
- 后台起播
- 进入 app 后状态同步
- 页面控播
- AVSession / 播放条
- 双播消除
现状结论
当前架构中:
PlaybackCoordinator 只是一个运行时转发器
- 真正的播放 runtime 仍然由
LocalMusic 页面注册
EntryAbility 在无 runtime 时,才退回 BackgroundAudioPlaybackHost
因此系统实际是:
- 卡片冷启动时走后台宿主
- 页面活着时优先走
LocalMusic runtime
这不是“同一路径的不同入口”,而是“两个播放器并存”。双播是架构必然结果,不是单点 bug。
方案选择
方案 A:彻底单宿主化
做法:
BackgroundAudioPlaybackHost 成为唯一播放器和唯一 runtime
LocalMusic 只做 UI 与状态展示,不再自己持有和控制播放器
优点:
- 从根上消除双播
- 卡片、后台、页面全部统一
- AVSession、卡片、页面状态可天然统一
缺点:
方案 B:双播放器保留,增加接管/互斥
做法:
- 保留
LocalMusic 和后台宿主
- 在进入 app 时尝试从后台宿主迁移到页面播放器
优点:
缺点:
- 本质仍有两条链路
- 容易继续出现双播、状态漂移、AVSession 不一致
- 不符合“真正完全独立且统一路径”的目标
方案 C:卡片走后台,进入 app 后迁回页面播放器
做法:
- 后台宿主仅服务卡片冷启动
- 页面打开后重新交回
LocalMusic
优点:
缺点:
结论
采用方案 A:彻底单宿主化。
目标架构
1. 唯一播放内核
BackgroundAudioPlaybackHost 成为整个应用唯一播放内核,负责:
- 持有唯一
IjkMediaPlayer
- 起播 / 暂停 / 停止 / 上一首 / 下一首 / seek
- 持有当前播放队列、当前索引、当前歌曲、播放模式
- 统一记忆播放恢复
- 统一 AVSession 更新
- 统一卡片更新
- 统一后台任务管理
- 统一发布当前播放快照
2. 统一控制入口
MusicPlaybackController / PlaybackCoordinator 继续作为控制入口,但只保留“转发到唯一宿主”的职责:
- UI 点击播放/暂停
- 卡片按钮控制
- AVSession 回调控制
- 页面快捷键和其他入口控制
所有控制命令最终都落到后台宿主,不再由 LocalMusic 自己直控播放器。
3. 页面层降为纯 UI 壳
LocalMusic 改为播放界面层:
- 读取后台宿主发布的播放快照
- 展示当前歌曲、进度、时长、播放状态、封面、歌词
- 将页面上的播放/暂停/上一首/下一首/seek 操作转发给控制层
- 保留页面展示逻辑和非播放器 UI 逻辑
LocalMusic 不再:
- 创建本地
IjkMediaPlayer
- 直接
doPlay()
- 直接
startPlayOrResumePlay()
- 直接
playNext() / playPrevious() 驱动底层播放器
- 直接写 AVSession
4. 单一状态真源
后台宿主维护唯一播放状态,并通过统一状态桥暴露给页面消费。
状态真源至少包括:
- 当前队列
- 当前索引
- 当前歌曲
- 是否播放中
- 当前进度
- 总时长
- 封面
- 播放模式
页面打开时先拉取一次完整快照,再持续监听状态变化。
关键职责划分
BackgroundAudioPlaybackHost
新增或收拢职责:
- 对外暴露完整
PlaybackRuntime
- 对外暴露当前播放快照
- 对外提供状态变化通知
- 成为唯一 AVSession 写入点
- 成为唯一卡片同步点
PlaybackCoordinator
调整为:
- 默认 runtime 直接绑定后台宿主
- 不再依赖
LocalMusic 注册 runtime
- 全局始终只有一个 runtime
MusicPlaybackController
保持:
变化:
LocalMusic
拆除职责:
- 自己持有播放器
- 自己写 AVSession
- 自己维护另一套播放状态
保留职责:
- 页面展示
- 页面交互
- 歌单展示
- 歌词展示
- 各类 UI 面板和辅助功能
数据流
统一后的数据流如下:
- 用户通过卡片 / 页面 / AVSession 发起控制命令。
- 命令进入
MusicPlaybackController。
MusicPlaybackController 转发给 PlaybackCoordinator。
PlaybackCoordinator 将命令转发给 BackgroundAudioPlaybackHost。
BackgroundAudioPlaybackHost 执行实际播放器操作。
BackgroundAudioPlaybackHost 更新:
LocalMusic 订阅并渲染该快照。
页面不再反向拥有播放状态,也不再反向驱动播放器。
进入 app 的行为设计
当后台宿主已经在播,用户从卡片进入 app 时:
- 只能打开页面 UI
- 页面必须直接读取后台宿主当前快照
- 不允许再次
doPlay()
- 不允许重建
IjkMediaPlayer
- 不允许重新起播同一首歌
也就是说,进入 app 是“显示接管”,不是“播放器迁移”。
AVSession / 播放条设计
AVSession 的写入必须统一收口到后台宿主。
这样可保证:
- 卡片播放时已有 AVSession
- 进入 app 后系统播放条不会断链
- 页面显示与系统播放条一致
LocalMusic 页面内部原有的 AVSession 初始化、状态更新、回调绑定逻辑,需要拆成两类:
- 必须保留的控制入口:转发到统一宿主
- 不再允许保留的状态写入:页面自己的 AVSession 更新
双播消除原则
双播的根因是存在两套底层播放器。
要彻底消除双播,必须同时满足:
- 页面层不再自行持有播放器并起播。
- 所有播放命令只走后台宿主。
- 进入页面时只同步状态,不重新起播。
只做其中任意一项都不够。
分阶段迁移策略
阶段 1:统一运行时入口
目标:
PlaybackCoordinator 永远绑定后台宿主
- 卡片和页面的播放控制都进入后台宿主
结果:
- 所有新的播放命令先统一到一个 runtime
- 先把“入口分叉”问题收口
阶段 2:页面改为消费宿主状态
目标:
LocalMusic 页面上的播放状态、进度、歌曲、封面改为读取宿主快照
- 页面进入时不再自己补建状态
结果:
- 进 app 后能看到正确播放条
- 页面与卡片、AVSession 状态一致
阶段 3:移除页面底层播放器职责
目标:
- 删除
LocalMusic 内部本地播放器核心控制逻辑
- 删除页面自己的 AVSession 写入路径
结果:
阶段 4:逐项迁回复杂能力
目标:
- 将歌词、随机播放、CUE、记忆播放、均衡器等能力逐步接回唯一宿主
原则:
文件级改造范围
第一阶段主要涉及:
entry/src/main/ets/playback/BackgroundAudioPlaybackHost.ets
entry/src/main/ets/controller/PlaybackCoordinator.ets
entry/src/main/ets/controller/MusicPlaybackController.ets
entry/src/main/ets/view/LocalMusic.ets
- 必要时新增:
entry/src/main/ets/playback/BackgroundPlaybackStateStore.ets
entry/src/main/ets/playback/BackgroundPlaybackSnapshot.ets
风险与控制
风险 1:页面状态空白
原因:
控制:
风险 2:复杂播放分支回归
原因:
控制:
风险 3:AVSession 回调回流冲突
原因:
- 页面和后台宿主同时写 AVSession 时容易互相打架
控制:
测试策略
第一阶段至少验证以下场景:
- 强杀 app 后点击卡片播放,可直接播放,不拉前台。
- 播放中点击卡片进入 app,页面直接显示当前歌曲、进度、播放条。
- 进入 app 后点击播放/暂停、上一首、下一首,实际控制同一后台宿主。
- 页面播放过程中不会双播。
- 系统播放条状态与页面状态一致。
- 卡片状态、页面状态、AVSession 状态一致。
成功标准
当满足以下标准时,认为第一阶段完成:
- 代码中只存在一套实际音乐播放器主链路
- 卡片和页面控播落到同一宿主
- 强杀后卡片播放可用
- 进入 app 不重新起播
- 不出现双播
- 页面播放条与 AVSession 正常显示