浏览代码

docs(player): 补充播放宿主完全独立设计

onecold 4 月之前
父节点
当前提交
5bb1c15599
共有 1 个文件被更改,包括 416 次插入0 次删除
  1. 416 0
      docs/superpowers/specs/2026-04-03-playback-host-full-independence-design.md

+ 416 - 0
docs/superpowers/specs/2026-04-03-playback-host-full-independence-design.md

@@ -0,0 +1,416 @@
+# 播放宿主完全独立设计
+
+## 背景
+
+当前项目虽然已经引入了 `MusicPlaybackController` 与 `PlaybackCoordinator`,但真实播放运行时仍然绑定在 [LocalMusic.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/LocalMusic.ets):
+
+- `IjkMediaPlayer` 由 `LocalMusic` 持有。
+- `PlaybackRuntime` 与 controller actions 由 `LocalMusic` 在页面生命周期里注册。
+- 当前歌曲、播放队列、索引、进度、歌词、AVSession、卡片同步、播放页显隐等关键状态仍以 `LocalMusic` 为主状态源。
+- `EntryAbility` 打开播放页时,仍然依赖 `PlaybackCoordinator.hasRuntime()`,本质上还是在等 `LocalMusic` 先成为宿主。
+
+这意味着当前播放能力只是“入口抽象了一层”,并没有真正脱离 `LocalMusic`。只要 `LocalMusic` 还是运行时宿主,播放就不算完全独立。
+
+## 本次目标
+
+本轮目标是把“真实播放 runtime + 播放状态源 + 播放器宿主 UI”从 `LocalMusic` 中完整迁出,建立独立播放宿主。
+
+完成后必须满足:
+
+- `LocalMusic` 只负责内容展示与发起播放请求,不再持有真实播放器。
+- `NewIndex` 挂载独立 `PlaybackHost`,成为唯一播放器宿主。
+- `PlaybackCoordinator` 的 runtime 来源是 `PlaybackHost`,不再是 `LocalMusic`。
+- 迷你播放条与全屏播放页的数据来源是独立宿主,不反向依赖 `LocalMusic`。
+- 强杀应用后,音乐卡片点击播放会先拉起应用,再由独立宿主恢复“上次整条播放队列 + 当前索引 + 当前进度”并开始播放。
+
+## 用户确认的约束
+
+- 首轮优先实现“完全独立”,不主动重做现有播放行为和 UI 视觉。
+- 强杀应用后不要求后台自动继续播放。
+- 只有用户点击音乐卡片播放时,才触发恢复播放。
+- 恢复范围不是单曲,而是“上次整条播放队列 + 当前索引 + 当前进度”。
+- 首轮不重写底层 Ijk 播放内核。
+
+## 非目标
+
+- 不重做现有播放器视觉设计。
+- 不重构所有远程源的 URL 解析实现,只调整归属和调用链。
+- 不顺手重构 `LocalMusic` 的本地列表结构、搜索结构、歌单结构。
+- 不实现“应用被系统杀死后自动后台恢复播放”。
+- 不追求一轮消灭所有 `AppStorage` 兼容字段,允许保留必要镜像。
+
+## 推荐方案
+
+采用“独立宿主 + 快照恢复”的轻量宿主方案。
+
+核心思想:
+
+- 新增独立 `PlaybackHost`,挂在 [NewIndex.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/pages/NewIndex.ets)。
+- `PlaybackHost` 接管真实播放运行时、核心播放状态、恢复链路、播放器页面开关和卡片联动。
+- `MusicPlaybackController` 继续作为统一播放入口。
+- `PlaybackCoordinator` 继续作为运行时协调层,但 runtime 改由 `PlaybackHost` 注册。
+- `LocalMusic`、`WebDavMainPage`、`FindView`、`PlaylistDetailPage`、`ChartsCount` 等页面全部退化成内容页,只负责组装队列并发起请求。
+
+不采用以下方案:
+
+- 继续让 `LocalMusic` 作为隐藏宿主常驻。这样只是“藏起来的耦合”,不是完全独立。
+- 直接把所有播放逻辑堆回 `MusicPlaybackController`。这样会把 controller 演化成新的超大类,结构上只是把问题平移。
+- 首轮同时做 store、runtime service、全量 UI 重建。改动面过大,验证成本太高,不符合“先完全独立,再保持行为稳定”的要求。
+
+## 目标架构
+
+### 1. MusicPlaybackController
+
+职责保持为统一入口层:
+
+- 对外暴露统一播放 API。
+- 负责少量纯逻辑解析。
+- 把动作转发给 `PlaybackCoordinator`。
+- 对外提供“宿主激活恢复播放”的统一入口。
+
+它不再承担:
+
+- 真实播放器生命周期。
+- 播放页显隐状态源。
+- 强杀恢复状态的主存储。
+
+### 2. PlaybackCoordinator
+
+继续作为协调层存在:
+
+- 持有当前唯一 runtime 引用。
+- 负责把 controller 请求转发给 runtime。
+- 负责在起播队列失败时回滚基础桥接状态。
+
+它不再依赖 `LocalMusic` 生命周期,而是只认 `PlaybackHost` 注册的 runtime。
+
+### 3. PlaybackHost
+
+新增独立播放宿主,挂在 `NewIndex`。
+
+它是本轮的核心新增组件,负责:
+
+- 持有 `IjkMediaPlayer`。
+- 持有当前歌曲、当前队列、当前索引。
+- 处理播放、暂停、上一首、下一首、seek。
+- 处理起播、切歌、恢复播放、记忆进度。
+- 处理歌词解析与同步。
+- 处理 AVSession、卡片同步、播放状态桥接。
+- 处理播放页显隐与迷你播放条数据来源。
+- 注册 `PlaybackRuntime` 与 controller actions。
+
+拆完后,`PlaybackHost` 是唯一真实播放宿主。
+
+### 4. 内容页
+
+包括但不限于:
+
+- [LocalMusic.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/LocalMusic.ets)
+- [WebDavMainPage.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/pages/WebDavMainPage.ets)
+- [FindView.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/FindView.ets)
+- [PlaylistDetailPage.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/pages/PlaylistDetailPage.ets)
+- [ChartsCount.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/pages/ChartsCount.ets)
+
+这些页面迁移后只负责:
+
+- 展示内容。
+- 组织队列。
+- 调用 `MusicPlaybackController.playQueue/playSong`。
+
+这些页面迁移后不再负责:
+
+- 持有播放器实例。
+- 维护当前播放状态源。
+- 决定播放页是否可打开。
+- 执行真实播放控制动作。
+
+## 硬边界
+
+本轮完成后必须满足以下硬边界:
+
+- `LocalMusic` 不能再持有 `IjkMediaPlayer`。
+- `LocalMusic` 不能再注册 `PlaybackRuntime` 或 `MusicPlaybackControllerActions`。
+- `LocalMusic` 不能再作为 `showPlayerView`、播放页开关、切歌逻辑、进度推进、当前歌曲、歌词、AVSession、卡片同步的主状态源。
+- 打开播放页的逻辑不能再依赖 `LocalMusic` 是否已挂载。
+- 迷你播放条和全屏播放页都必须从 `PlaybackHost` 读取状态。
+- 强杀恢复不能再依赖 `LocalMusic` 生命周期。
+
+## 强杀恢复设计
+
+### 1. 恢复原则
+
+强杀后的恢复语义明确如下:
+
+- 应用被强杀后不自动播放。
+- 只有用户点击音乐卡片播放时,才会拉起应用并恢复播放。
+- 恢复的是“上次整条播放队列 + 当前索引 + 当前进度”,不是单曲。
+- 恢复成功后直接开始播放,不严格还原“上次是暂停还是播放”的状态。
+
+最后一点的原因是:在应用已死场景下,用户从卡片点击播放的意图更接近“恢复并开始播放”,而不是“恢复到暂停态”。
+
+### 2. PlaybackSnapshot
+
+新增独立快照模型 [PlaybackSnapshot.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/model/PlaybackSnapshot.ets)。
+
+快照至少包含:
+
+- `queue`: 当前播放队列的精简快照。
+- `currentIndex`: 当前播放索引。
+- `currentSongKey`: 当前歌曲稳定标识。
+- `positionMs`: 当前播放进度。
+- `playType`: 当前播放模式。
+- `playlistContext`: 队列来源上下文。
+- `updatedAt`: 最近更新时间。
+- `shouldResumeWhenActivated`: 是否存在待恢复播放意图。
+
+队列内每首歌的快照项只保留恢复所需字段,例如:
+
+- `filePath`
+- `id`
+- `type`
+- `name`
+- `artist`
+- `remote_rel_path`
+- `webdav_account_id`
+- 其他当前远程源重建播放地址所需的稳定字段
+
+明确不持久化:
+
+- 实时解析出来的播放 URL
+- `IjkMediaPlayer` 内部状态
+- UI 动画状态
+- 临时回调与运行时对象引用
+
+### 3. PlaybackSnapshotStore
+
+新增 [PlaybackSnapshotStore.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/PlaybackSnapshotStore.ets),职责如下:
+
+- 写入当前快照
+- 读取最近快照
+- 清理无效快照
+- 兼容损坏数据回退
+
+写入时机由 `PlaybackHost` 统一控制:
+
+- 歌曲切换
+- 队列切换
+- 播放进度推进到节流点
+- 播放模式变化
+- 应用即将进入不可见状态
+
+### 4. 强杀后的恢复流程
+
+统一恢复流程如下:
+
+1. `PlaybackHost` 正常播放过程中持续写入 `PlaybackSnapshot`。
+2. 应用被强杀后,音乐卡片触发一个“播放/恢复播放”激活动作。
+3. [EntryAbility.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/entryability/EntryAbility.ets) 不直接播放,只写入一个待执行的激活动作。
+4. 应用启动后,`NewIndex` 先挂载 `PlaybackHost`。
+5. `PlaybackHost` 初始化时读取 `PlaybackSnapshot`,恢复队列、当前索引、当前歌曲和进度到内存,但先不自动播放。
+6. `PlaybackHost` 检测到待执行激活动作后,再开始恢复播放:
+   - 恢复整条队列
+   - 修正当前索引
+   - 为当前歌曲重新解析播放 URL
+   - 从 `positionMs` 开始播放
+7. 若当前歌曲恢复失败,则优先尝试队列中的下一首可播歌曲。
+8. 若整条队列失效,则清空快照并提示恢复失败。
+
+## 文件边界
+
+### 新增文件
+
+- [PlaybackHost.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/PlaybackHost.ets)
+  独立播放宿主组件,挂在 `NewIndex`,负责 runtime 注册和宿主生命周期。
+- [PlaybackHostState.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/PlaybackHostState.ets)
+  收口宿主共享状态定义。
+- [PlaybackSnapshotStore.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/PlaybackSnapshotStore.ets)
+  读写恢复快照。
+- [PlaybackRestoreCoordinator.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/PlaybackRestoreCoordinator.ets)
+  负责快照恢复与激活动作编排。
+- [PlaybackSnapshot.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/playback/model/PlaybackSnapshot.ets)
+  定义恢复快照和精简队列项结构。
+
+### 主要修改文件
+
+- [NewIndex.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/pages/NewIndex.ets)
+  挂载 `PlaybackHost`,让迷你播放条和全屏播放页从宿主取状态。
+- [LocalMusic.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/LocalMusic.ets)
+  收缩为内容页,删除宿主职责。
+- [MusicPlaybackController.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/controller/MusicPlaybackController.ets)
+  保持统一入口定位,补激活动作入口。
+- [PlaybackCoordinator.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/controller/PlaybackCoordinator.ets)
+  保持协调层定位,runtime 改由宿主注册。
+- [EntryAbility.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/entryability/EntryAbility.ets)
+  音乐卡片点击后改为写入待执行宿主激活动作,而不是等待 `LocalMusic`。
+- [PlayerPage.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/player/PlayerPage.ets)
+  继续做 UI 壳层,但状态来源换成 `PlaybackHost`。
+- [PlayerControls.ets](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/entry/src/main/ets/view/player/PlayerControls.ets)
+  保持纯组件定位,不新增业务状态。
+
+## 分阶段迁移顺序
+
+### 阶段 1:建立独立宿主
+
+目标:
+
+- 新增 `PlaybackHost`
+- 迁出 `IjkMediaPlayer`
+- 迁出 runtime 注册和 controller actions 注册
+- 迁出基础播放状态源
+- 让 `NewIndex` 挂载独立宿主
+
+验收:
+
+- 即使 `LocalMusic` 不可见,播放器也能存活
+- `PlaybackCoordinator` 的 runtime 来源是 `PlaybackHost`
+
+### 阶段 2:迁出播放器页面和迷你条状态源
+
+目标:
+
+- 迷你播放条和全屏播放页改为从宿主读状态
+- 打开播放页不再依赖 `LocalMusic`
+- `showPlayerView/openPlayerViewFromMiniBar/dismissPlayerView` 等入口统一收敛到宿主
+
+验收:
+
+- 不打开 `LocalMusic` 页面,也能打开播放页并正常播控
+
+### 阶段 3:迁出恢复链路
+
+目标:
+
+- 新增 `PlaybackSnapshotStore`
+- 新增 `PlaybackRestoreCoordinator`
+- 打通“强杀后卡片点击播放 -> 应用拉起 -> 恢复整条队列并播放”
+
+验收:
+
+- 强杀后点击卡片可以恢复整条队列、当前索引和当前进度
+
+### 阶段 4:内容页彻底退化
+
+目标:
+
+- `LocalMusic` 只保留内容页逻辑
+- 其他内容页统一只发请求
+- 删除 `LocalMusic` 中剩余宿主级播放状态写入
+
+验收:
+
+- `LocalMusic` 不再是播放状态源
+- 理论上删除 `LocalMusic` 不应影响播放器主链路,只会影响本地内容页功能
+
+## 错误处理策略
+
+错误处理统一收口在 `PlaybackHost` 与 `PlaybackRestoreCoordinator`,不再分散在 `LocalMusic`。
+
+### 1. 快照读取失败
+
+- 视为无可恢复状态
+- 不阻塞应用启动
+- 记录日志并清理损坏快照
+
+### 2. 队列为空或全部无效
+
+- 清空恢复状态
+- 给出“暂无可恢复播放内容”或“恢复播放失败”的提示
+
+### 3. 当前索引越界
+
+- 自动修正到安全范围
+- 修正后若无有效歌曲,则清空快照
+
+### 4. 当前歌曲恢复失败
+
+- 优先尝试队列中的下一首可播放歌曲
+- 全部失败后再提示恢复失败
+
+### 5. 远程播放 URL 失效
+
+- 绝不复用旧 URL
+- 恢复时统一重新解析
+- 解析失败则本次播放失败,但保留队列状态
+
+### 6. 本地文件不存在
+
+- 跳过当前项尝试下一首
+- 必要时提示文件已失效
+
+### 7. 宿主未就绪时收到动作
+
+- 动作进入待执行队列
+- `PlaybackHost` ready 后统一消费
+- 避免卡片动作、迷你条动作、页面动作打空
+
+## 日志与可观测性
+
+本轮必须新增统一日志前缀,便于排查恢复链路:
+
+- `[playback-host]`
+- `[playback-snapshot]`
+- `[playback-restore]`
+- `[playback-activation]`
+
+至少覆盖以下事件:
+
+- 宿主初始化完成
+- runtime 注册与注销
+- 快照保存与读取
+- 卡片激活动作入队与消费
+- 队列恢复成功与失败
+- 当前歌曲 URL 重建成功与失败
+- 从哪个进度开始恢复播放
+
+## 自动化测试建议
+
+优先覆盖纯逻辑与恢复决策,不强求首轮就完整自动化播放器真机链路。
+
+建议新增 Hypium 或纯逻辑测试:
+
+- `PlaybackSnapshotStore`
+  - 写入后可正确读回
+  - 损坏数据能安全回退
+  - 空快照返回空结果
+- `PlaybackRestoreCoordinator`
+  - 当前索引越界时能修正
+  - 当前歌曲失效时能尝试下一首
+  - 空队列与全失效队列能正确失败
+- `MusicPlaybackController`
+  - 宿主未就绪时动作不会丢失
+  - 激活动作可以被宿主消费
+
+若播放器行为难以自动化,至少把“恢复决策”“快照解析”“索引修正”拆成纯函数做单测。
+
+## 手工验收清单
+
+必须手工验证以下场景:
+
+- 本地歌曲播放后强杀,点击音乐卡片播放,能恢复整条队列与当前进度
+- WebDAV 歌曲播放后强杀,点击音乐卡片播放,能重新解析地址并恢复
+- 当前歌曲失效时,能自动跳到下一首可播歌曲
+- 不进入 `LocalMusic` 页面,也能从排行榜、发现页、歌单详情发起播放
+- 迷你播放条和全屏播放页在 `LocalMusic` 不可见时仍正常
+- `LocalMusic` 打开后不会重复注册 runtime,也不会重新抢占宿主状态
+- 无有效快照时,点击卡片只提示无可恢复内容,不崩溃
+
+## 完成定义
+
+只有以下条件全部成立,才算完成“播放完全独立”:
+
+- 播放 runtime 不再属于 `LocalMusic`
+- `LocalMusic` 不再是播放状态源
+- `NewIndex + PlaybackHost` 成为唯一播放器宿主
+- 强杀后通过音乐卡片可以恢复“整条队列 + 当前索引 + 当前进度”的播放
+- 播放主链路不依赖先进入 `LocalMusic`
+
+## 与既有设计的关系
+
+本设计是对已有两份设计文档的收束与推进:
+
+- [2026-03-29-localmusic2-player-refactor-design.md](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/docs/superpowers/specs/2026-03-29-localmusic2-player-refactor-design.md)
+  已提出“播放器宿主应独立于内容页”的方向,但当时仍偏向新入口与大范围文件重组。
+- [2026-04-02-playback-controller-decouple-localmusic-design.md](/mnt/d/harmony/2025/qimeng/TTMusic_Card/TTMusic/docs/superpowers/specs/2026-04-02-playback-controller-decouple-localmusic-design.md)
+  已把 controller/coordinator 入口抽出来,但 runtime 仍未真正脱离 `LocalMusic`。
+
+本次设计的定位是:在现有 controller/coordinator 基础上,补齐“独立宿主 + 强杀恢复 + 内容页彻底退化”这三个关键缺口,完成真正意义上的完全独立。