# 统一后台播放宿主设计 ## 背景 当前项目同时存在两条独立的音乐播放链路: - `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 正常显示