|
@@ -0,0 +1,402 @@
|
|
|
|
|
+# 统一后台播放宿主设计
|
|
|
|
|
+
|
|
|
|
|
+## 背景
|
|
|
|
|
+
|
|
|
|
|
+当前项目同时存在两条独立的音乐播放链路:
|
|
|
|
|
+
|
|
|
|
|
+- `LocalMusic.ets` 页面内播放器链路
|
|
|
|
|
+ - 页面持有 `mIjkMediaPlayer`
|
|
|
|
|
+ - 页面直接负责起播、暂停、切歌、进度控制
|
|
|
|
|
+ - 页面直接负责 AVSession 更新
|
|
|
|
|
+- `BackgroundAudioPlaybackHost.ets` 后台宿主链路
|
|
|
|
|
+ - 卡片冷启动控播走后台宿主
|
|
|
|
|
+ - 强杀后卡片播放由后台宿主恢复队列并起播
|
|
|
|
|
+
|
|
|
|
|
+这两条链路同时存在,直接导致以下问题:
|
|
|
|
|
+
|
|
|
|
|
+- 进入 app 后,页面播放条和后台宿主状态不一致
|
|
|
|
|
+- `LocalMusic` 本地播放和卡片后台播放会双播
|
|
|
|
|
+- AVSession / 卡片 / 页面显示不是同一真源
|
|
|
|
|
+- “强杀后卡片播放可用但不拉前台”和“进入 app 后页面无缝接管”无法同时满足
|
|
|
|
|
+
|
|
|
|
|
+## 目标
|
|
|
|
|
+
|
|
|
|
|
+统一为唯一播放链路,满足以下行为:
|
|
|
|
|
+
|
|
|
|
|
+1. 唯一播放器永远是后台独立宿主,不再由 `LocalMusic` 持有另一套本地播放器。
|
|
|
|
|
+2. 音乐卡片、后台控播、进入 app 后的页面控播,全部走同一条播放路径。
|
|
|
|
|
+3. 强杀 app 后,点击卡片播放可以直接播放,不拉起 app 前台。
|
|
|
|
|
+4. 点击卡片进入 app 时,页面只能“接管显示和控制”,不能重新 `doPlay()`、不能重建播放器。
|
|
|
|
|
+5. 页面上的当前歌曲、播放状态、进度、时长、封面、AVSession 播放条,都以后台宿主为唯一真源。
|
|
|
|
|
+6. 彻底消除双播。
|
|
|
|
|
+
|
|
|
|
|
+## 非目标
|
|
|
|
|
+
|
|
|
|
|
+本次第一轮统一不追求一次性重构所有复杂播放分支。
|
|
|
|
|
+
|
|
|
|
|
+以下内容不作为第一阶段必须完成项:
|
|
|
|
|
+
|
|
|
|
|
+- 所有投播分支的彻底统一
|
|
|
|
|
+- 所有特殊格式的完整播放能力迁移
|
|
|
|
|
+- 所有歌词、均衡器、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
|
|
|
|
|
+
|
|
|
|
|
+保持:
|
|
|
|
|
+
|
|
|
|
|
+- 播放逻辑入口
|
|
|
|
|
+- 纯逻辑解析
|
|
|
|
|
+- 命令分发
|
|
|
|
|
+
|
|
|
|
|
+变化:
|
|
|
|
|
+
|
|
|
|
|
+- 所有动作只走统一 runtime
|
|
|
|
|
+
|
|
|
|
|
+### LocalMusic
|
|
|
|
|
+
|
|
|
|
|
+拆除职责:
|
|
|
|
|
+
|
|
|
|
|
+- 自己持有播放器
|
|
|
|
|
+- 自己写 AVSession
|
|
|
|
|
+- 自己维护另一套播放状态
|
|
|
|
|
+
|
|
|
|
|
+保留职责:
|
|
|
|
|
+
|
|
|
|
|
+- 页面展示
|
|
|
|
|
+- 页面交互
|
|
|
|
|
+- 歌单展示
|
|
|
|
|
+- 歌词展示
|
|
|
|
|
+- 各类 UI 面板和辅助功能
|
|
|
|
|
+
|
|
|
|
|
+## 数据流
|
|
|
|
|
+
|
|
|
|
|
+统一后的数据流如下:
|
|
|
|
|
+
|
|
|
|
|
+1. 用户通过卡片 / 页面 / AVSession 发起控制命令。
|
|
|
|
|
+2. 命令进入 `MusicPlaybackController`。
|
|
|
|
|
+3. `MusicPlaybackController` 转发给 `PlaybackCoordinator`。
|
|
|
|
|
+4. `PlaybackCoordinator` 将命令转发给 `BackgroundAudioPlaybackHost`。
|
|
|
|
|
+5. `BackgroundAudioPlaybackHost` 执行实际播放器操作。
|
|
|
|
|
+6. `BackgroundAudioPlaybackHost` 更新:
|
|
|
|
|
+ - 卡片
|
|
|
|
|
+ - AVSession
|
|
|
|
|
+ - 全局播放快照
|
|
|
|
|
+7. `LocalMusic` 订阅并渲染该快照。
|
|
|
|
|
+
|
|
|
|
|
+页面不再反向拥有播放状态,也不再反向驱动播放器。
|
|
|
|
|
+
|
|
|
|
|
+## 进入 app 的行为设计
|
|
|
|
|
+
|
|
|
|
|
+当后台宿主已经在播,用户从卡片进入 app 时:
|
|
|
|
|
+
|
|
|
|
|
+- 只能打开页面 UI
|
|
|
|
|
+- 页面必须直接读取后台宿主当前快照
|
|
|
|
|
+- 不允许再次 `doPlay()`
|
|
|
|
|
+- 不允许重建 `IjkMediaPlayer`
|
|
|
|
|
+- 不允许重新起播同一首歌
|
|
|
|
|
+
|
|
|
|
|
+也就是说,进入 app 是“显示接管”,不是“播放器迁移”。
|
|
|
|
|
+
|
|
|
|
|
+## AVSession / 播放条设计
|
|
|
|
|
+
|
|
|
|
|
+AVSession 的写入必须统一收口到后台宿主。
|
|
|
|
|
+
|
|
|
|
|
+这样可保证:
|
|
|
|
|
+
|
|
|
|
|
+- 卡片播放时已有 AVSession
|
|
|
|
|
+- 进入 app 后系统播放条不会断链
|
|
|
|
|
+- 页面显示与系统播放条一致
|
|
|
|
|
+
|
|
|
|
|
+`LocalMusic` 页面内部原有的 AVSession 初始化、状态更新、回调绑定逻辑,需要拆成两类:
|
|
|
|
|
+
|
|
|
|
|
+- 必须保留的控制入口:转发到统一宿主
|
|
|
|
|
+- 不再允许保留的状态写入:页面自己的 AVSession 更新
|
|
|
|
|
+
|
|
|
|
|
+## 双播消除原则
|
|
|
|
|
+
|
|
|
|
|
+双播的根因是存在两套底层播放器。
|
|
|
|
|
+
|
|
|
|
|
+要彻底消除双播,必须同时满足:
|
|
|
|
|
+
|
|
|
|
|
+1. 页面层不再自行持有播放器并起播。
|
|
|
|
|
+2. 所有播放命令只走后台宿主。
|
|
|
|
|
+3. 进入页面时只同步状态,不重新起播。
|
|
|
|
|
+
|
|
|
|
|
+只做其中任意一项都不够。
|
|
|
|
|
+
|
|
|
|
|
+## 分阶段迁移策略
|
|
|
|
|
+
|
|
|
|
|
+### 阶段 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:页面状态空白
|
|
|
|
|
+
|
|
|
|
|
+原因:
|
|
|
|
|
+
|
|
|
|
|
+- 页面原本依赖自己内部播放器状态
|
|
|
|
|
+
|
|
|
|
|
+控制:
|
|
|
|
|
+
|
|
|
|
|
+- 先建立宿主快照和订阅机制,再切 UI 读取路径
|
|
|
|
|
+
|
|
|
|
|
+### 风险 2:复杂播放分支回归
|
|
|
|
|
+
|
|
|
|
|
+原因:
|
|
|
|
|
+
|
|
|
|
|
+- `LocalMusic` 内部聚合了大量特殊播放逻辑
|
|
|
|
|
+
|
|
|
|
|
+控制:
|
|
|
|
|
+
|
|
|
|
|
+- 第一阶段只统一主链路
|
|
|
|
|
+- 复杂分支分批迁移
|
|
|
|
|
+
|
|
|
|
|
+### 风险 3:AVSession 回调回流冲突
|
|
|
|
|
+
|
|
|
|
|
+原因:
|
|
|
|
|
+
|
|
|
|
|
+- 页面和后台宿主同时写 AVSession 时容易互相打架
|
|
|
|
|
+
|
|
|
|
|
+控制:
|
|
|
|
|
+
|
|
|
|
|
+- AVSession 写入只允许后台宿主负责
|
|
|
|
|
+
|
|
|
|
|
+## 测试策略
|
|
|
|
|
+
|
|
|
|
|
+第一阶段至少验证以下场景:
|
|
|
|
|
+
|
|
|
|
|
+1. 强杀 app 后点击卡片播放,可直接播放,不拉前台。
|
|
|
|
|
+2. 播放中点击卡片进入 app,页面直接显示当前歌曲、进度、播放条。
|
|
|
|
|
+3. 进入 app 后点击播放/暂停、上一首、下一首,实际控制同一后台宿主。
|
|
|
|
|
+4. 页面播放过程中不会双播。
|
|
|
|
|
+5. 系统播放条状态与页面状态一致。
|
|
|
|
|
+6. 卡片状态、页面状态、AVSession 状态一致。
|
|
|
|
|
+
|
|
|
|
|
+## 成功标准
|
|
|
|
|
+
|
|
|
|
|
+当满足以下标准时,认为第一阶段完成:
|
|
|
|
|
+
|
|
|
|
|
+- 代码中只存在一套实际音乐播放器主链路
|
|
|
|
|
+- 卡片和页面控播落到同一宿主
|
|
|
|
|
+- 强杀后卡片播放可用
|
|
|
|
|
+- 进入 app 不重新起播
|
|
|
|
|
+- 不出现双播
|
|
|
|
|
+- 页面播放条与 AVSession 正常显示
|
|
|
|
|
+
|