# 播放宿主完全独立设计 ## 背景 当前项目虽然已经引入了 `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 基础上,补齐“独立宿主 + 强杀恢复 + 内容页彻底退化”这三个关键缺口,完成真正意义上的完全独立。