Quellcode durchsuchen

docs(player): 设计音乐卡片后台控播链路

Codex vor 4 Monaten
Ursprung
Commit
76a2303514
1 geänderte Dateien mit 269 neuen und 0 gelöschten Zeilen
  1. 269 0
      docs/superpowers/specs/2026-04-04-music-card-background-call-design.md

+ 269 - 0
docs/superpowers/specs/2026-04-04-music-card-background-call-design.md

@@ -0,0 +1,269 @@
+# 音乐卡片后台控播改造设计
+
+## 背景
+
+当前 `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 视觉调整
+- 非音乐卡片场景的后台唤起能力复用
+