2026-04-03-playback-host-full-independence-design.md 16 KB

播放宿主完全独立设计

背景

当前项目虽然已经引入了 MusicPlaybackControllerPlaybackCoordinator,但真实播放运行时仍然绑定在 LocalMusic.ets

  • IjkMediaPlayerLocalMusic 持有。
  • 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
  • PlaybackHost 接管真实播放运行时、核心播放状态、恢复链路、播放器页面开关和卡片联动。
  • MusicPlaybackController 继续作为统一播放入口。
  • PlaybackCoordinator 继续作为运行时协调层,但 runtime 改由 PlaybackHost 注册。
  • LocalMusicWebDavMainPageFindViewPlaylistDetailPageChartsCount 等页面全部退化成内容页,只负责组装队列并发起请求。

不采用以下方案:

  • 继续让 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. 内容页

包括但不限于:

这些页面迁移后只负责:

  • 展示内容。
  • 组织队列。
  • 调用 MusicPlaybackController.playQueue/playSong

这些页面迁移后不再负责:

  • 持有播放器实例。
  • 维护当前播放状态源。
  • 决定播放页是否可打开。
  • 执行真实播放控制动作。

硬边界

本轮完成后必须满足以下硬边界:

  • LocalMusic 不能再持有 IjkMediaPlayer
  • LocalMusic 不能再注册 PlaybackRuntimeMusicPlaybackControllerActions
  • LocalMusic 不能再作为 showPlayerView、播放页开关、切歌逻辑、进度推进、当前歌曲、歌词、AVSession、卡片同步的主状态源。
  • 打开播放页的逻辑不能再依赖 LocalMusic 是否已挂载。
  • 迷你播放条和全屏播放页都必须从 PlaybackHost 读取状态。
  • 强杀恢复不能再依赖 LocalMusic 生命周期。

强杀恢复设计

1. 恢复原则

强杀后的恢复语义明确如下:

  • 应用被强杀后不自动播放。
  • 只有用户点击音乐卡片播放时,才会拉起应用并恢复播放。
  • 恢复的是“上次整条播放队列 + 当前索引 + 当前进度”,不是单曲。
  • 恢复成功后直接开始播放,不严格还原“上次是暂停还是播放”的状态。

最后一点的原因是:在应用已死场景下,用户从卡片点击播放的意图更接近“恢复并开始播放”,而不是“恢复到暂停态”。

2. PlaybackSnapshot

新增独立快照模型 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,职责如下:

  • 写入当前快照
  • 读取最近快照
  • 清理无效快照
  • 兼容损坏数据回退

写入时机由 PlaybackHost 统一控制:

  • 歌曲切换
  • 队列切换
  • 播放进度推进到节流点
  • 播放模式变化
  • 应用即将进入不可见状态

4. 强杀后的恢复流程

统一恢复流程如下:

  1. PlaybackHost 正常播放过程中持续写入 PlaybackSnapshot
  2. 应用被强杀后,音乐卡片触发一个“播放/恢复播放”激活动作。
  3. EntryAbility.ets 不直接播放,只写入一个待执行的激活动作。
  4. 应用启动后,NewIndex 先挂载 PlaybackHost
  5. PlaybackHost 初始化时读取 PlaybackSnapshot,恢复队列、当前索引、当前歌曲和进度到内存,但先不自动播放。
  6. PlaybackHost 检测到待执行激活动作后,再开始恢复播放:
    • 恢复整条队列
    • 修正当前索引
    • 为当前歌曲重新解析播放 URL
    • positionMs 开始播放
  7. 若当前歌曲恢复失败,则优先尝试队列中的下一首可播歌曲。
  8. 若整条队列失效,则清空快照并提示恢复失败。

文件边界

新增文件

主要修改文件

分阶段迁移顺序

阶段 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 不应影响播放器主链路,只会影响本地内容页功能

错误处理策略

错误处理统一收口在 PlaybackHostPlaybackRestoreCoordinator,不再分散在 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

与既有设计的关系

本设计是对已有两份设计文档的收束与推进:

本次设计的定位是:在现有 controller/coordinator 基础上,补齐“独立宿主 + 强杀恢复 + 内容页彻底退化”这三个关键缺口,完成真正意义上的完全独立。