|
@@ -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 真正变成任何页面都能直接调用的播放入口,同时把风险控制在可验证范围内。
|