这个系列写到现在,越写越觉得鸿蒙多媒体这块有味道。前面几篇咱们把音频渲染、焦点管理这些底层能力都过了一遍,这篇终于轮到全局播控了。搞 HarmonyOS 多媒体开发的,早晚都会碰到这个东西:AVSession。
先别嫌名字拗口,它干的事其实每个用手机的人都见过。你打开音乐App放一首歌,锁屏界面上出现专辑封面、进度条、暂停切歌按钮;通知栏下拉,媒体卡片展示当前播放的歌曲;按一下蓝牙耳机上的切歌键,手机上的播放器切到下一首。这些都属于全局播控的范畴,而 HarmonyOS 这边实现这套交互的核心就是 AVSession。简单理解,它就是给系统媒体中心、锁屏界面、蓝牙耳机这些控制入口提供统一数据与指令通道的“中间人”。
这篇适合正在做音乐播放器、播客FM、有声书、甚至视频类应用的开发者。如果你只是用框架放个声音、不做任何媒体中心联动,那这篇可以跳过;但只要你希望应用退到后台之后还能被用户正常控制,AVSession 这一关早晚要过。我会从原理讲到代码,再到后面的坑和调试手段,尽量一次讲透。
1. 先搞清楚 AVSession 到底管什么
1.1 没有 AVSession 的时候,媒体中心是“瞎”的
我在实际开发中见过一个很典型的场景:刚开始做播放器,觉得能出声就行,没有接 AVSession。结果用户把 App 切到后台,锁屏界面上看不到任何播放信息,用户想暂停只能把应用拉回来。更麻烦的是,用蓝牙耳机按一下播放键,系统根本不知道该把指令发给谁。
这就是缺了“中间人”的后果。系统侧的媒体中心、控制中心、锁屏组件,它们并不知道你那个 App 内部播放器当前是什么状态,也不知道歌曲叫什么名字。它们能感知的只是“当前有没有 App 在出声”,以及“全局音量是多少”。要做精细的、按应用维度的媒体控制,就得让每个播放器把自己的状态主动同步出来。
AVSession 解决的就是这个信息不对称问题。你作为应用开发者,创建一个会话,把歌曲标题、播放状态、进度、封面这些数据塞进去,再接收系统派发过来的控制指令,双向打通。系统只需要面向会话操作,不需要知道你的播放器内部实现是软解还是硬解,是 AVPlayer 还是自研解码器。
1.2 角色拆解:Host 与 Controller
AVSession 模型里有两个核心角色,理解清楚后面看文档就不懵。
第一个是会话的创建方,叫 Host。你的播放器应用就是 Host。Host 负责维护一份“当前播放清单”,包括播放状态(播放中、暂停中、已停止)、媒体信息(标题、歌手、专辑、时长、封面)。同时 Host 还要响应来自系统的控制请求,例如播放、暂停、上一首、下一首、拖动进度。
第二个是会话的控制方,叫 Controller。系统媒体中心、锁屏、控制中心、甚至第三方设备都通过 Controller 来获取会话信息、发送控制指令。Controller 可以是一次性的临时对象,系统会为每个活跃的 AVSession 动态创建一个控制器,你的代码里一般拿不到它,不需要你管。
还有几个配套概念要一起说清楚。元数据(AVMetadata)描述媒体是什么;播放状态(PlaybackState)描述播放进行到哪了;媒体命令(AVSessionCommand)描述系统想让播放器做什么。这三类数据就是 Host 与 Controller 之间来回传递的“语言”。
1.3 会话类型别选错
创建会话的时候需要指定一个类型,可真别小看这个参数。audio 类型会进入系统音频媒体中心,适用于音乐播放器、播客、有声书;video 类型除了媒体卡片,还容易被视频应用做投屏时关联;call 类型则对应通话场景,和音乐 App 几乎没关系。
选错类型的典型症状是:你的音乐播放器创建了 audio 类型,想在媒体卡片上显示,结果卡片上没有你的应用,或者只有声音没有封面。更常见的错误是把所有场景都当 audio 用,导致系统在处理突发事件(来电、SOS 等)的时候,不知道你的应用到底属于哪一类,优先级就会乱套。
2. 动手之前,先把这几件事想清楚
2.1 模块引入与权限声明
在 HarmonyOS 6 系列的 SDK 里,AVSession 相关能力集中在@kit.AVSessionKit里,和音频播放的@kit.AudioKit是两套模块,别引混了。同时后台任务要用@kit.BackgroundTasksKit。开发环境建议直接用 DevEco Studio 创建 Stage 模型工程,代码里通过import { avSession } from '@kit.AVSessionKit'引用。
权限方面,AVSession 本身不需要申请特殊权限,但这里有个容易踩的坑:系统媒体卡片要正常展示通知栏媒体信息,需要通知权限。如果应用没有通知权限,媒体卡片可能不出现,或者只出现个半残状态。另外如果要申请后台长任务让应用在后台持续播放,还需要在module.json5里声明对应权限。这个我后面讲后台控制时再细说。
提示:项目开发中,如果遇到媒体卡片不显示,先排查通知权限,不要一上来就去翻 AVSession 的代码。我见过有人查了两个小时,最后发现是系统设置里把 App 通知关掉了。
2.2 先定义清楚播放器的状态机
这个建议听起来简单,但很多应用翻车都翻在这。AVSession 上报的是你业务的播放状态,如果你的播放器内部没有一个清晰的状态机,状态上报就会乱。
什么叫清晰?至少你要能区分这些状态:初始(IDLE)、缓冲中(BUFFERING)、播放中(PLAYING)、暂停(PAUSED)、停止(STOPPED)、播放完成(COMPLETED)。每个状态变化要么是你自己触发的(比如用户点了暂停),要么是系统指令触发的(比如来了个电话系统让你暂停),要么是播放器底层回调的。你要做的就是把这些变化统一收敛到一个入口,再去刷新 AVSession。
我见过一个很典型的 bug:播放器底层是别的同学封装的,暂停和停止用的是同一个回调,结果 AVSession 上报的全是 STOPPED,系统媒体卡片上的暂停按钮就永远不亮了。这就是状态机不清晰导致的。
2.3 设计好合理的更新频度
还有一个容易被忽略的设计点:状态的更新频度。播放进度 position 如果每 100ms 就上报一次,系统的媒体卡片会不断刷新,这不仅浪费资源,还可能让系统侧出现刷新闪烁。而如果完全不更新 position,用户从控制面板拖动进度条时,参考基准线就是旧的,体验很差。
我的习惯是:进度类信息由播放器的定时器驱动,每秒或每两秒上报一次;状态类信息(比如从播放变为暂停)立即上报;元数据在切换曲目时上报,歌曲播放过程中不去反复刷。这个策略后面还会展开。
3. 核心流程:从创建会话到响应控制
3.1 创建并激活会话
代码写起来并不复杂,核心是三步:createAVSession、activate、setAVSessionCallback。先用一个 UIAbilityContext 创建会话对象,然后立即激活,再注册控制指令回调。
import { avSession } from '@kit.AVSessionKit'; import { common } from '@kit.AbilityKit'; let session: avSession.AVSession | null = null; async function initAVSession(context: common.UIAbilityContext) { try { session = await avSession.createAVSession(context, '我的音乐播放器', 'audio'); await session.activate(); session.setAVSessionCallback({ onPlay: () => { player.play(); }, onPause: () => { player.pause(); }, onNext: () => { playNextTrack(); }, onPrevious: () => { playPreviousTrack(); }, onSeek: (time: number) => { player.seek(time); } }); console.info('AVSession created and callback registered'); } catch (error) { console.error('createAVSession failed: ' + JSON.stringify(error)); } }有几个细节值得注意。createAVSession 的第二个参数“我的音乐播放器”是会话名称,它会展示在系统媒体中心里,建议起一个用户能看懂的名字,不要用什么 “Music” 这种通用词。第三个参数是类型,上面说过了,audio。
activate 这一步容易被漏。不激活的会话,系统媒体中心看不到。有不少同学在真机上调试发现卡片出不来,排查半天,发现就是忘了调 activate。
setAVSessionCallback 是否设置同样关键。即使你把元数据和状态都上报了,如果不处理回调,用户点击系统侧按钮后指令没人响应,体验就是“按钮能点但不干活”。这个回调不仅包括播放暂停切歌,还应该把 seek 和类似“停止”这样的命令都处理掉。
3.2 上报元数据:歌曲信息从哪来
会话创建好了,接下来要把“现在放的是什么”告诉系统。这个过程叫设置元数据。代码里常见这些字段:
session.setAVMetadata({ assetId: currentMedia.id, // 内部标识 title: currentMedia.title, // 歌曲名 artist: currentMedia.artist, // 歌手 album: currentMedia.album, // 专辑 albumCoverUri: currentMedia.cover, // 封面图,uri 或 resource duration: currentMedia.duration // 总时长,单位毫秒 });assetId 很多人不重视,但它对系统排序、索引以及你自己后续做“再次播放继续”功能都有用,建议用你播放器内部的媒体 ID,且保证切换歌曲时变化。duration 一定用毫秒。我在代码评审里见过有人填秒,结果媒体卡片上的进度条显示时长只有几十毫秒,整个轨道高度就崩了。
封面图这里多说一句。AVSession 支持多种封面格式,包括本地资源、网络地址 uri、以及 mediaId 等。网络封面在系统侧加载时,要考虑网络失败的问题,所以我建议应用尽早把封面下载到本地,用本地文件 uri 传给系统。这样媒体卡片展示速度更快,也不容易出现“标题有了,封面灰一块”的状态。
3.3 播放状态更新:明确区分 state 与 position
元数据描述“是什么”,状态描述“正在干什么”。这一块是最容易出问题的地方。播放状态的设置如下:
session.setPlaybackState({ state: avSession.PlaybackState.PLAYING, position: currentPosition, speed: 1.0 });state 的取值包括 PREPARE、PLAYING、PAUSED、STOPPED、COMPLETED 等,注意不同 SDK 版本命名可能有差异,以你工程里的声明文件为准。position 是当前播放位置,毫秒单位;speed 是倍速,一般播放器都支持,0.5、1.0、2.0 按实际情况填。
我建议的刷新策略是:
- 播放器内部状态变化,比如点击暂停、播放结束,这时候立即调用 setPlaybackState,连 position 一起带上;
- 播放中进度的持续刷新,放在你自己的播放器进度回调里,每秒最多一次;
- seek 操作完成之后,一定要立刻上报新的 position,否则系统媒体卡片上的进度条会和实际播放位置对不上。
还有一个实操细节很多人会忽略:当应用进入后台时,如果播放中断,你要及时把状态改成 PAUSED 或 STOPPED,不要让它一直停留在 PLAYING。否则系统媒体中心上的卡片是一直“播放中”的假象,用户点暂停连反馈都没有,非常影响口碑。
3.4 响应命令:别漏掉“列表控制”
系统媒体中心发送给会话的指令,并不只有播放暂停。新版系统里,用户长按媒体卡片还能看到“下一首”操作,桌面小部件、耳机按键也会派发不同命令。setAVSessionCallback 里要处理的常见命令包括:
- onPlay:开始播放
- onPause:暂停
- onNext:下一首
- onPrevious:上一首
- onSeek:跳转进度
- onStop:停止播放
后两个容易被漏。onSeek 在有声书、播客场景里尤其重要,因为用户经常在长音频里拖进度;onStop 通常和系统级的“停止会话”挂钩,如果你漏了这个回调,可能出现通知栏上永远霸着一张卡片,点关闭也没反应。
命令处理完成后,记得更新状态。比如 onPause 触发后,在播放器暂停成功回调里再 setPlaybackState 为 PAUSED。别在 onPause 里直接改状态,因为如果底层播放器刚好处于异常状态,暂停失败,你却上报了 PAUSED,系统侧会以为暂停成功了,用户再按播放,状态又回到 PLAYING,整个 UI 会感觉卡卡的。
3.5 系统媒体中心联动的运行效果
走到这一步,你已经可以在下拉通知栏看到媒体卡片了。卡片上会有封面、标题、歌手、进度条、播放/暂停按钮、上一首/下一首按钮。锁屏状态下,锁屏界面也能展示同样信息。这些展示基于你上报的元数据与状态,系统自动搭好,不需要你再开发 UI。
需要注意的是,媒体卡片 UI 展示有一定的“生效延迟”。你会发现,第一次激活会话后,媒体卡片可能隔 1 到 3 秒才出现,这是正常的。如果你在应用启动瞬间立刻调用 activate,再立刻上报元数据,系统可能还没准备好展示,偶尔会丢。稳妥的姿势是:在首次播放开始的时刻创建会话并上报,而不是在应用启动时。
4. 后台播放与后台控制
4.1 没有后台任务,应用后台播放就是“裸奔”
AVSession 把状态同步给了系统,但同步不代表系统会保你不死。默认情况下,应用切到后台后,进程可能被系统回收,一旦回收,AVSession 也随之销毁,媒体卡片立刻消失。要保证后台继续播放,必须申请后台长任务。
在 HarmonyOS 里,常用的做法是通过@kit.BackgroundTasksKit的接口申请。播放音乐这种场景,对应的是音频播放类长任务,申请时需要一个描述字段,这个描述系统可能会展示给用户,建议写清楚用途:
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; import { common } from '@kit.AbilityKit'; async function startBackgroundTask(context: common.UIAbilityContext) { try { await backgroundTaskManager.startBackgroundRunning(context, { taskType: backgroundTaskManager.TaskType.AUDIO_PLAYBACK, description: '正在播放音乐,需要保持后台运行' }); console.info('background task started'); } catch (error) { console.error('startBackgroundRunning failed: ' + JSON.stringify(error)); } }taskType 要根据你的业务选择。播放音乐选 AUDIO_PLAYBACK,而如果做的是类似语音聊天、录音这类,可能要去匹配对应的类型。类型选错,系统可能会拒绝申请或者按错误策略调度。
申请后台任务的最佳时机是“即将退到后台且在播放中”。不要在应用启动时申请,也不要在暂停时申请。我看到过有人把申请逻辑写在 UIAbility 的 onBackground 里,判断当前状态是 PLAYING 就申请,这个思路是对的。应用回到前端后,可以再判断是否停止播放,如果已暂停则释放长任务。
4.2 保持会话与后台任务的节奏一致
这里有一个容易忽略的联动:AVSession 的会话状态建议与后台任务申请/释放保持同步。具体来说,当播放器进入 PLAYING 时,尽力确保后台任务已经申请;当状态变成 PAUSED 且准备退后台时,考虑释放后台任务。
为什么这么建议?因为如果播放暂停了还占着后台长任务,系统会认为你滥用资源,可能降低你应用的调度优先级,甚至限制一次申请的时长。而反过来,如果还在播放但后台任务提前释放,进程被杀概率会急剧上升。
有种更稳的写法:每次从 PAUSED 恢复 PLAYING 的时候,重新申请一次后台任务。这样即使之前的任务因为某种原因被系统收回,也还有机会续上。
4.3 音频焦点与后台冲突:谁来控制谁
后台控制的高度,往往取决于你前面音频焦点处理得好不好。这里只强调几个和 AVSession 强相关的点。
你的播放器在播放前,通常要抢占音频焦点。焦点抢到了,应该先更新 AVSession 状态为 PLAYING,再开始放声音。如果焦点抢占失败,不建议把会话状态改成 PLAYING,因为用户看到的卡片是播放中,实际却没声。
另一个容易踩的坑:来电、闹钟等场景,系统会打断你的播放。这种情况下,你的播放器会收到中断回调,你应该在回调里暂停播放并更新 AVSession 状态为 PAUSED。中断结束后系统可能允许你恢复,也可能不允许,取决于你的焦点恢复机制。最忌讳的是中断发生时不处理,AVSession 还保持在 PLAYING,用户看着卡片一直转,耳朵里却没音乐。
我又要吐槽一个真实经历:之前某项目接第三方音频 SDK,它的播放器内部自动处理了来电暂停,但并没有回调给上层业务。业务侧不知道播放已经暂停,AVSession 一直处于 PLAYING 状态。结果就是电话结束后,用户看着媒体卡片以为是播放中,点暂停再点播放,状态就乱了,最后只能重启应用解决。后来我们在 SDK 回调区域加了一层状态监听,才算根治。所以,AVSession 状态永远要以播放器真实状态为准,千万别提前上报。
4.4 前后台切换时的状态校准
应用从后台回到前台,AVSession 一般不会被销毁,但系统侧可能会对它做一些调整。稳妥起见,在 UIAbility.onForeground 里做一次主动校准:调用一次 setPlaybackState,把当前真实状态刷一遍;如果此时播放已经停止,可以考虑把会话销毁或重置,避免占着媒体中心的卡片。
还有种特殊场景:应用在后台停留很久,系统可能自动清理掉会话对象。此时访问原 session 的接口大概率会报错,需要重新创建。我在做某个长时间播放的播客 App 时,遇到过后台挂机一晚上之后,再回来操作,发现媒体卡片还在,但点任何按钮都没反应。最后定位到根因是应用进程还活着,但旧的 AVSession 已经被系统回收了。以后的代码里,凡是系统侧回调找不到会话时,我都要求返回一个“会话失效”标志给 UI,然后走重建流程。
5. 避坑锦囊与调试心得
5.1 常见问题速查表
下面这张表是我自己排查 AVSession 问题时最常用的一张对照表,写出来给你参考:
| 症状 | 可能的根因 | 处理方法 |
|---|---|---|
| 媒体卡片完全不出现 | 会话未 activate;通知权限关闭;创建了 session 但从未上报元数据或状态 | 检查 activate;检查系统设置里的通知权限;确认上报了初始状态 |
| 卡片出现,但没有封面 | 封面 uri 失效,网络加载失败,或传了不支持的格式 | 用本地文件 uri;封面下载失败时给默认图兜底 |
| 能显示歌曲名,却不能切歌 | setAVSessionCallback 没注册,或回调里只处理了 play/pause | 补全 onNext/onPrevious 等命令处理 |
| 进度条不推进 | position 上报频率太低,或 seek 后没更新 | 播放中用定时器每秒刷一次;seek 完成后立即上报 |
| 锁屏界面控制失效 | 锁屏前的会话状态异常;应用被系统回收 | 确认退后台前状态是 PLAYING,并且后台任务在运行 |
| 切后台就断声 | 没有申请后台长任务,或任务类型错误 | 申请对应音频类型的后台任务;在 playback 开始后申请 |
| 来电话后状态错乱 | 中断回调未同步到 AVSession | 在焦点中断回调里同步执行 setPlaybackState |
| 多个音乐 App 混播 | 没有互斥处理,没有正确响应系统焦点请求 | 完善焦点抢占与释放逻辑,让后启动的应用暂停前一个 |
表格只能帮你定位方向,实际开发中大概率是多个因素叠加。遇到问题别慌,按层次排查:先确认播放器自身状态对不对,再看 AVSession 上报数据对不对,最后看系统展示端。
5.2 调试手段:日志与命令行工具
AVSession 排障有一点很关键:状态是“上报”给系统的,系统展示端是否刷新,你从应用侧看不到全貌。所以调试时要两头看。
应用侧日志建议增加专门的 Tag,比如 AVSessionDemo,把每个上报、命令回调的关键节点都打一行日志。下面的日志格式可以做个参考:
[AVSessionDemo] create session, type = audio [AVSessionDemo] activate success [AVSessionDemo] set metadata, title = XXX [AVSessionDemo] set playback state, state = PLAYING, pos = 12345 [AVSessionDemo] received command: onPause日志粒度不用太细,关键转折点有就行。否则一秒钟刷几十条进度日志,问题都被淹没了。
系统侧可以通过 DevEco Studio 的日志工具过滤关键字,比如AVSession、MediaSessionController、SessionManager,用系统日志确认系统到底有没有收到你的上报。真机上可以用 hdc 命令连接设备后做更细致的检查,比如查看当前活跃的 AVSession 数量。这里不展开具体命令了,因为不同开发环境命令不太一样,但思路是通用的:应用侧确认了“我已上报”,系统侧确认“我已收到”,两个日志一对应,问题基本能定位在哪一半。
5.3 自己反复踩过之后的几点经验
最后说几条不写进文档、但项目里很有用的经验。
第一,能创建会话不等于能用。有个项目把 createAVSession 写在 Application 初始化里,应用一启动就拿到了 session 并 activate。看着没问题,可一旦系统在应用还开着的时候杀过一次进程,重启后 session 拿到的是旧的失效对象,各种调用都返回异常。后来统一改成“每次播放开始时都重新获取会话,带 try/catch,失败就重新创建”才解决。
第二,状态上报的时序,比状态本身重要。比如点击播放按钮的瞬间,底层播放器还在缓冲,你直接上报 PLAYING,卡片立刻变成播放中,但实际声音要等两秒才出来。用户看到卡片转了,耳朵里没声,体验极差。这种情况建议先上报 BUFFERING,等播放器真的开始产出数据了,再更新 PLAYING。
第三,命令回调里尽量避免做耗时操作。系统侧派发指令给你,如果你在主线程里做网络加载、解析、甚至取封面,系统会认为你的会话“不响应”,可能降低指令派发频率。我的做法是:在回调里立刻切换一个轻量状态,把真正的耗时操作丢到执行器里去跑。比如 onNext 回调,先把状态改成 LOADING,再在后台加载下一首,加载完再更新元数据和状态。
第四,AVSession 和音量的关系也值得注意。系统媒体中心有音量控制,你要确保播放器的音量输出流类型是对的。如果是音频播放,输出流类型应该是音乐(media)。如果流类型配错,你上报了 AVSession,但媒体中心的音量调节控制不到你的播放器,看起来就像“系统音量键失效”,实际上指向了别的流。
写在最后
AVSession 这块,我第一次接入的时候也觉得是个很小的点,不就是通知栏一张卡片吗?真正做进去才发现,它牵扯到播放器状态管理、后台任务、音频焦点、通知展示机制,任何一个环节掉链子,用户看到的就是一个失灵的控制入口。控制中心这东西,平时不坏没人夸,坏一次用户感受特别明显。
我个人在实际项目里养成的习惯是:先做一个最小 Demo,不接业务逻辑,用系统自带的音频文件都能跑起来,把“创建会话—上报元数据—更新状态—接收命令”这条链路彻底跑通,再往真实业务里搬。你可以试试从一段最朴素的播放代码开始,把媒体中心上的暂停、切歌、进度条都点到可用,再往下优化细节。这套流程走顺之后,后面开发什么播客、有声书、在线音乐,基本就是复用同一套骨架。
下一篇如果继续聊多媒体,我可能会写投屏相关的内容,或者是系统媒体中心自定义卡片能力。有想法的可以先自己摸索一下,咱们在评论区碰碰思路。