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