Преглед на файлове

docs(player): 补充播放控制脱离 LocalMusic 设计

Codex преди 4 месеца
родител
ревизия
f295f66858
променени са 1 файла, в които са добавени 281 реда и са изтрити 0 реда
  1. 281 0
      docs/superpowers/specs/2026-04-02-playback-controller-decouple-localmusic-design.md

+ 281 - 0
docs/superpowers/specs/2026-04-02-playback-controller-decouple-localmusic-design.md

@@ -0,0 +1,281 @@
+# 播放控制脱离 LocalMusic 宿主设计
+
+## 背景
+
+当前项目里的统一播放控制入口是 `MusicPlaybackController`,但它本质上仍然只是一个动作分发器或事件桥接层:
+
+- `playOrPause`、`playNext`、`playPrevious`、`seekTo` 等动作依赖 `LocalMusic` 在页面生命周期里注册 actions。
+- 起播一首歌或一组歌时,页面侧通常还是通过 `EVENT_PLAYLIST_PLAY` 发送请求,再由 `LocalMusic` 监听并执行真实播放。
+- 播放器实例、播放队列、当前歌曲、当前索引、AVSession、投播联动等核心逻辑都沉在 `LocalMusic` 内部。
+
+这导致一个结构性问题:任何页面想直接调用 controller 播放,实际上仍然依赖 `LocalMusic` 充当“播放器宿主页面”。
+
+## 本阶段目标
+
+第一阶段只解决“播放核心依赖 LocalMusic 宿主”的问题,不做整站 UI 重构。
+
+完成标准:
+
+- 任意页面可以直接通过 `MusicPlaybackController` 起播单曲或队列。
+- 任意页面可以直接通过 `MusicPlaybackController` 调用 `play/pause/next/previous/seek`。
+- `LocalMusic` 不再负责接收 `EVENT_PLAYLIST_PLAY` 作为主播放入口。
+- 迷你播放条、全屏播放器、播放页显隐、列表高亮等 UI 显示状态,暂时仍保留在 `NewIndex/LocalMusic` 层。
+- 允许 `LocalMusic` 继续常驻隐藏挂载,但它不再是播放指令的宿主。
+
+非目标:
+
+- 本阶段不把迷你播放条状态完全抽成独立 store。
+- 本阶段不强行拆完 `LocalMusic` 的所有播放相关字段。
+- 本阶段不重写投播、歌词、AVSession 的全部实现方式,只调整宿主归属和调用链。
+
+## 推荐方案
+
+采用“抽离 `PlaybackCoordinator`,保留现有 UI 宿主”的方案。
+
+核心思想:
+
+- `MusicPlaybackController` 升级成真正的统一控制入口。
+- 新增 `PlaybackCoordinator` 承接真实播放核心逻辑。
+- `LocalMusic` 从“播放逻辑宿主”降级为“播放 UI 容器”。
+- 页面层不再依赖 `EVENT_PLAYLIST_PLAY -> LocalMusic` 这条链路起播。
+
+不采用以下方案:
+
+- 仅继续做事件桥接。这样 controller 仍然不是实际播放入口,后续还要返工。
+- 一次性把 UI 状态和核心状态全部独立。首轮改动面过大,容易同时打坏迷你播放条、投播、AVSession 与歌词联动。
+
+## 目标架构
+
+### 1. MusicPlaybackController
+
+职责:
+
+- 提供全局统一 API。
+- 对外暴露直接控制接口,而不是依赖页面注册 actions。
+- 将页面请求转发给 `PlaybackCoordinator`。
+
+第一阶段期望接口:
+
+- `playSong(song: VideoItem, options?)`
+- `playQueue(songs: VideoItem[], startIndex: number, options?)`
+- `playOrPause()`
+- `playNext()`
+- `playPrevious()`
+- `seekTo(value: string, source?: string)`
+
+兼容策略:
+
+- 旧的 `setActions/clearActions` 暂时保留,但标记为过渡能力。
+- controller 内部优先走 `PlaybackCoordinator`;仅对尚未迁出的能力允许短期回退。
+
+### 2. PlaybackCoordinator
+
+新增协调层,负责播放核心链路。
+
+职责:
+
+- 持有当前播放队列、当前索引、当前歌曲、基础播放状态。
+- 承接起播单曲、起播队列、切歌、暂停/恢复、seek。
+- 协调底层 player、AVSession、投播模块与播放元数据同步。
+- 向 UI 层暴露可订阅的最小播放状态。
+
+第一阶段必须迁出的逻辑:
+
+- `doPlay` 主链路及其前后置准备逻辑。
+- 当前歌曲切换。
+- 播放队列切换。
+- `play/pause/next/previous/seek` 的控制主路径。
+
+第一阶段允许仍留在 UI 层的逻辑:
+
+- 播放页打开关闭。
+- 迷你播放条显示/隐藏状态。
+- 纯视觉动画与页面交互状态。
+
+### 3. LocalMusic
+
+迁移后职责:
+
+- 读取 `PlaybackCoordinator` 暴露的播放状态并渲染 UI。
+- 负责全屏播放器、播放列表界面、迷你播放条相关视觉层逻辑。
+- 保留页面内交互,但交互结果改为调用 controller,而不是直接执行播放核心流程。
+
+迁移后不再承担的职责:
+
+- 监听 `EVENT_PLAYLIST_PLAY` 作为主播放入口。
+- 持有独占的起播核心流程。
+- 作为 controller actions 的唯一宿主。
+
+### 4. NewIndex
+
+保持当前职责:
+
+- 承载页面级 UI 容器。
+- 继续持有迷你播放条显示层状态。
+- 通过共享播放状态决定何时展示播放 UI。
+
+## 数据流
+
+迁移前:
+
+1. 页面点击歌曲。
+2. 页面发事件或调用桥接逻辑。
+3. `LocalMusic` 监听事件。
+4. `LocalMusic` 执行 `doPlay` 与后续播放流程。
+
+迁移后:
+
+1. 页面点击歌曲。
+2. 页面直接调用 `MusicPlaybackController.playSong(...)` 或 `playQueue(...)`。
+3. controller 直接调用 `PlaybackCoordinator`。
+4. `PlaybackCoordinator` 更新播放队列、当前歌曲、底层播放器与共享播放状态。
+5. `LocalMusic/NewIndex` 作为 UI 消费者响应状态变化。
+
+关键变化:
+
+- 页面到播放核心的入口从“事件 + LocalMusic”改为“controller + coordinator”。
+- `LocalMusic` 从命令执行者变成状态消费者。
+
+## 状态拆分原则
+
+为降低风险,第一阶段只抽“核心播放状态”,不抽“全部 UI 状态”。
+
+核心播放状态建议包含:
+
+- 当前歌曲 `currentSong`
+- 播放队列 `songList`
+- 当前索引 `curIndex`
+- 是否正在播放 `isPlaying`
+- 当前进度/时长
+- 当前播放模式
+
+继续留在 UI 层的状态:
+
+- 播放页是否展开
+- 迷你播放条显隐
+- 页面动画状态
+- 手势、滚动、弹窗、浮层相关状态
+
+判断标准:
+
+- 影响实际播放行为的状态,属于 coordinator。
+- 只影响视觉表现的状态,留在 UI。
+
+## 分阶段迁移计划
+
+### 阶段 1:抽出起播入口
+
+目标:
+
+- 从 `LocalMusic` 中抽离 `doPlay` 主链路到 `PlaybackCoordinator`。
+- 支持 `playSong(song)` 和 `playQueue(songs, startIndex)`。
+- 首批页面切到新入口:`ChartsCount`、歌单页、发现页等最直接的列表页。
+
+验收:
+
+- 不经 `EVENT_PLAYLIST_PLAY` 也能直接起播。
+- 首次起播后迷你播放条和播放器界面仍能正常响应。
+
+### 阶段 2:抽出基础控制动作
+
+目标:
+
+- 将 `playOrPause`、`playNext`、`playPrevious`、`seekTo` 从 `LocalMusic` 挪到 `PlaybackCoordinator`。
+- `MusicPlaybackController.setActions/clearActions` 进入兼容态,不再作为主路径。
+
+验收:
+
+- 页面级 controller 调用不再依赖 `LocalMusic` 注册 actions。
+
+### 阶段 3:UI 状态改为消费共享状态
+
+目标:
+
+- `LocalMusic` 改为从共享状态读取当前歌曲、索引、播放状态、进度等。
+- 清理 UI 层对核心播放状态的直接写入。
+
+验收:
+
+- `LocalMusic` 只做 UI 响应,不再承担核心状态源头角色。
+
+### 阶段 4:清理旧事件桥接
+
+目标:
+
+- 清理 `EVENT_PLAYLIST_PLAY` 作为本地主播放主链的职责。
+- 清理为桥接页面播放而存在的过渡数据仓库和 pending request 逻辑。
+
+验收:
+
+- 本地播放主链全部走 controller -> coordinator。
+
+## 兼容与回退策略
+
+为了避免一次改坏所有场景,第一阶段允许短期双轨:
+
+- 新页面入口优先调用 `controller -> coordinator`。
+- 未迁移完的少数路径可保留旧逻辑,直到对应页面切换完成。
+
+但有两条硬约束:
+
+- 不允许新增新的 `EVENT_PLAYLIST_PLAY -> LocalMusic` 依赖。
+- controller 对外文档和新代码一律以 direct API 为准。
+
+## 风险与约束
+
+### 1. 强耦合字段风险
+
+`LocalMusic` 当前很可能把播放器状态、UI 状态、动画状态、投播状态混在同一个大组件中。
+
+应对:
+
+- 第一阶段先只迁移真正决定播放行为的字段。
+- 对一时拆不开的字段,先通过只读映射或桥接读取,避免一轮就全部改写。
+
+### 2. AVSession / 投播联动风险
+
+如果 AVSession 与投播流程深度耦合 `LocalMusic` 内部字段,直接搬迁会有回归风险。
+
+应对:
+
+- 优先保证 coordinator 成为“命令主入口”。
+- AVSession 与投播在第一阶段可通过适配层接入 coordinator,不要求同一轮完全重构。
+
+### 3. UI 不同步风险
+
+若 coordinator 成为状态源,但 `LocalMusic` 仍在局部直接写旧字段,容易出现 UI 与实际播放状态分裂。
+
+应对:
+
+- 每迁一类动作,就同步把对应 UI 读取切到共享状态。
+- 禁止同一字段同时存在“coordinator 写”和“UI 自己写”两套主路径。
+
+## 测试与验证
+
+第一阶段验证重点不是全量 UI,而是播放主链切换是否成功。
+
+必须验证:
+
+- controller 直接调用可起播单曲。
+- controller 直接调用可起播队列。
+- `play/pause/next/previous/seek` 不依赖 `LocalMusic` actions。
+- `LocalMusic` 不再作为 `EVENT_PLAYLIST_PLAY` 的主执行入口。
+- 迷你播放条和全屏播放器仍能跟随共享状态更新。
+
+建议测试层次:
+
+- 控制器与 coordinator 的纯逻辑测试。
+- 队列切换与索引切换的回归测试。
+- AVSession / 投播的冒烟验证。
+- 真机手工验证:统计页、歌单页、发现页直接起播。
+
+## 决策结论
+
+本阶段采用以下明确决策:
+
+- 可以接受 `LocalMusic` 继续常驻隐藏挂载。
+- 不再接受 `LocalMusic` 作为播放指令宿主。
+- 先保留 `NewIndex/LocalMusic` 的 UI 显示状态。
+- 先抽播放核心,再逐步抽状态与显示层。
+
+这保证第一阶段的目标聚焦为:让 controller 真正变成任何页面都能直接调用的播放入口,同时把风险控制在可验证范围内。