2026-04-04-music-card-background-call-design.md 8.0 KB

音乐卡片后台控播改造设计

背景

当前 TTMusic 的音乐卡片播放控制没有完全独立:

  • 卡片 play_pause/prev/next/seek_to 当前优先走 callEntryAbility
  • 当应用进程已被强杀时,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。

具体做法:

  • 新增一个专门承接音乐卡片后台 callUIAbility
  • UIAbility 不负责页面展示,只负责注册 callee.on(...)
  • 卡片上的 播放/暂停/上一首/下一首/进度跳转 全部发送到这个后台控播 UIAbility
  • 卡片上的“打开播放器”仍然走 routerEntryAbility
  • 新增一个真正独立的“卡片后台播放宿主”,它不依赖 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 视觉调整
  • 非音乐卡片场景的后台唤起能力复用