# 音乐卡片后台控播改造设计 ## 背景 当前 `TTMusic` 的音乐卡片播放控制没有完全独立: - 卡片 `play_pause/prev/next/seek_to` 当前优先走 `call` 到 `EntryAbility` - 当应用进程已被强杀时,`call` 的接收方不存在,按钮无响应 - `FormExtensionAbility` 当前只能把事件转发给 `eventHub`,或兜底拉起 `EntryAbility` - 一旦走 `EntryAbility` 冷启动兜底,就会把应用前台界面拉起,不符合目标 用户目标已经明确: - 强杀后,点击卡片 `播放/暂停/上一首/下一首` 仍然可用 - 这些控制动作不能把应用前台界面拉起 - 只有明确点击“打开播放器”的动作,才允许进入前台页面 ## 平台依据 根据用户提供的鸿蒙官方文档《卡片拉起应用UIAbility到后台(call事件)》: - 卡片 `postCardAction(... action: 'call')` 可以将指定 `UIAbility` 拉到后台 - `call` 事件支持指定方法名和参数 - 该能力依赖 `ohos.permission.KEEP_BACKGROUND_RUNNING` - 该路径不要求 `ohos.permission.START_INVISIBLE_ABILITY` 这意味着本项目不必走 `AppServiceExtensionAbility` 或 invisible ability 路线,可以直接采用: `卡片 -> call -> 后台 UIAbility -> 独立播放控制入口` ## 现状问题 ### 1. 控制入口仍然绑在 `EntryAbility` `TTMusic` 当前卡片控制动作的接收方是 `EntryAbility.callee`。这在应用存活时可用,但在进程被强杀后没有稳定的后台控制入口。 ### 2. `FormAbility` 不能直接控播 `MusicCardFormAbility` 当前只负责: - 更新卡片数据 - 将消息转发到 `eventHub` - 兜底拉起 `EntryAbility` 它不是实际的冷启动控播执行者。 ### 3. 播放控制仍依赖页面运行态 当前播放能力虽然已经在做拆分,但仍然与页面侧运行态、页面事件和 `LocalMusic` 生命周期耦合较深。结果是: - 热态控播能工作 - 冷态后台控播缺少一个真正独立的宿主 ## 设计目标 本次改造只解决音乐卡片后台控播,不做无关重构。 成功标准: 1. 应用未强杀时,卡片 `播放/暂停/上一首/下一首/进度跳转` 可正常工作 2. 应用被强杀后,卡片 `播放/暂停/上一首/下一首` 可直接生效 3. 上述控制动作不会拉起前台页面 4. 卡片“打开播放器”动作仍然可以进入前台播放器页面 5. 卡片状态在控播后能继续刷新 ## 方案对比 ### 方案 A:卡片控制统一走后台 `UIAbility` 路径: `卡片 -> call -> 后台控播 UIAbility -> 独立播放控制器` 优点: - 与官方文档能力完全一致 - 不需要 `START_INVISIBLE_ABILITY` - 能天然满足“强杀后可控播但不拉前台” - 冷启动路径和热启动路径可以统一到同一个控制接口 缺点: - 需要把现有控播逻辑进一步从页面侧拆出来 ### 方案 B:卡片走 `FormAbility.message` 路径: `卡片 -> message -> FormAbility -> 独立播放控制器` 优点: - 保持卡片提供方逻辑集中在 `FormAbility` 缺点: - 与官方推荐的“后台拉起 `UIAbility` 执行 call”路径不一致 - 当前项目里 `FormAbility` 已经承担卡片数据职责,再承担后台播放器宿主会混杂两类生命周期 ### 方案 C:继续复用 `EntryAbility` 路径: `卡片 -> call/router -> EntryAbility -> 播放控制器` 优点: - 表面改动最少 缺点: - 很难稳定保证“不拉前台” - 会继续把卡片后台控播和主界面生命周期绑在一起 ## 推荐方案 采用方案 A。 具体做法: - 新增一个专门承接音乐卡片后台 `call` 的 `UIAbility` - 该 `UIAbility` 不负责页面展示,只负责注册 `callee.on(...)` - 卡片上的 `播放/暂停/上一首/下一首/进度跳转` 全部发送到这个后台控播 `UIAbility` - 卡片上的“打开播放器”仍然走 `router` 到 `EntryAbility` - 新增一个真正独立的“卡片后台播放宿主”,它不依赖 `LocalMusic` 页面实例 ## 目标架构 ### 卡片事件分流 - `play_pause` -> `call` -> `MusicCardControlAbility` - `prev_song` -> `call` -> `MusicCardControlAbility` - `next_song` -> `call` -> `MusicCardControlAbility` - `seek_to` -> `call` -> `MusicCardControlAbility` - `open_player` -> `router` -> `EntryAbility` ### 后台控播宿主职责 新增后台控播宿主后,它需要负责: - 接收卡片 `call` 方法和参数 - 解析动作、表单 ID、跳转位置 - 恢复已持久化的播放快照和播放队列 - 在没有页面实例时独立执行 `play/pause/prev/next/seek` - 执行后刷新卡片快照和卡片 UI ### 页面侧职责 `LocalMusic` 和页面运行态继续负责: - 前台播放器 UI - 正常播放中的实时状态上报 - 将最新播放状态、队列和当前歌曲写入持久化存储 页面侧不再承担“卡片强杀后冷启动时的唯一执行入口”。 ## 关键设计细节 ### 1. 新增后台控播 `UIAbility` 建议新增单独能力,例如: - `MusicCardControlAbility` 职责: - 在 `onCreate` 中注册 `callee.on(MusicCardActionConstants.CALL_METHOD_HANDLE_ACTION, ...)` - 接收卡片参数后分发给后台控播宿主 - 不做页面路由,不创建播放器页面 - 生命周期结束时执行 `callee.off(...)` 该能力在 `module.json5` 中新增 `abilities` 配置。 ### 2. 卡片控制动作不再发往 `EntryAbility` 所有卡片页面中的控制按钮都改成: - `abilityName: 'MusicCardControlAbility'` - `action: 'call'` 只有“打开播放器”保留: - `abilityName: 'EntryAbility'` - `action: 'router'` ### 3. 新增后台独立播放控制入口 建议新增一个面向后台卡片控播的控制器,例如: - `MusicCardBackgroundPlaybackController` 职责: - 从现有持久化快照中恢复当前歌曲 - 从现有持久化队列中恢复播放队列和当前下标 - 执行 `toggle/previous/next/seekTo` - 将执行结果写回快照存储 - 触发 `MusicCardManager.updateAllForms(...)` 该控制器的目标不是替代现有前台控制器,而是提供“无页面实例时也能运行”的最小控播闭环。 ### 4. 播放恢复源 后台控播所需的数据优先来自现有持久化层: - 当前播放快照 - 当前播放队列 - 当前下标 - 当前文件路径 - 当前播放进度 如果快照存在但队列缺失: - `play_pause` 至少应支持恢复当前歌曲 - `prev/next` 在没有有效队列时退化为保持当前歌曲不切换,并记录日志 ### 5. 热态与冷态统一接口 最终控制接口应统一为: - 热态:页面/前台宿主调用同一组控制方法 - 冷态:后台控播 `UIAbility` 也调用同一组控制方法 这样可以避免出现两套播放切换逻辑不一致的问题。 ## 错误处理 - 没有任何可恢复的快照时,`play_pause` 不做前台拉起,只记录日志 - 队列不可恢复时,`prev/next` 不做前台拉起,只记录日志 - `seek_to` 参数非法时忽略 - 后台控播失败时,必须确保不会误触发页面路由 ## 测试策略 ### 自动化 新增或补充以下测试: - 卡片动作参数解析测试 - 后台控播宿主在只有快照时的恢复测试 - 后台控播宿主在有队列时的上一首/下一首切换测试 - 控制动作不会落到 `EntryAbility` 的路由测试 ### 手工验证 1. 应用前台运行时,卡片 `播放/暂停/上一首/下一首` 正常 2. 应用切后台但未强杀时,卡片控制正常且不拉前台 3. 强杀应用后,点击卡片播放可直接播放,不拉前台 4. 强杀应用后,点击卡片上一首/下一首可切歌,不拉前台 5. 点击卡片封面或“打开播放器”时,前台页面正常打开 ## 实施顺序 1. 新增后台控播 `UIAbility` 配置与空实现 2. 将卡片控制按钮的 `call` 目标切换到新能力 3. 提炼后台独立播放控制入口 4. 接入快照/队列恢复逻辑 5. 接入卡片刷新 6. 补测试并验证强杀场景 ## 范围外 本次不处理: - 全量重构现有前台播放器架构 - 远程歌曲缓存策略重做 - 卡片 UI 视觉调整 - 非音乐卡片场景的后台唤起能力复用