HarmonyOS 7 + AVSession Kit + Audio Kit 技术干货:媒体会话、后台播放与系统播控联动机制【鸿蒙心迹】
音频能播放,只能说明播放器本身能工作;真正做成系统级媒体体验,还要让系统媒体中心、耳机、车载、手表等控制入口理解“现在播的是什么、播放到哪里、点击暂停以后该通知谁”。这篇文章从播放器内部状态出发,把 Audio Kit 与 AVSession Kit 的分工和联动链路完整拆开。

一、为什么播放器已经能播了,还需要 AVSession
第一次做音频播放时,很容易觉得播放器逻辑已经完整:创建播放实例、加载资源、播放、暂停、上一首、下一首,再做一个进度条,功能就差不多了。
这种实现放在应用内部确实能用。
但只要用户把应用退到后台,或者希望在系统控制中心、锁屏界面、蓝牙耳机、车载设备上控制播放,问题就出现了。
系统并不知道你的页面上哪一个按钮代表“播放”,也不知道当前歌曲叫什么、播放到哪一秒、下一首是谁。你的播放器状态只存在于自己的业务代码里,系统世界并没有“看见”它。
AVSession Kit 就是把这层信息从应用内部带到系统媒体会话层。
它的核心价值可以理解成两部分:
- 应用把媒体元数据和播放状态同步给系统;
- 系统或外设产生的播放控制指令,再通过会话回传给应用。
所以 Audio Kit / AVPlayer 解决“声音怎么真正播出来”,AVSession 解决“系统怎样理解和控制这次播放”。
两者不是替代关系,而是上下两层。
二、先把播放器状态和会话状态分开
这类功能最容易犯的错误,就是直接把播放器对象当成系统会话。
播放器内部通常有自己的状态,比如:
- 当前资源是否加载;
- 是否正在播放;
- 当前 position;
- 音量;
- 播放列表;
- 输出设备。
而 AVSession 维护的是系统播控需要看到的一组信息:
- 当前媒体元数据;
- 播放 / 暂停 / 停止状态;
- 播放位置;
- 可响应的控制事件;
- 当前输出设备和会话是否激活。
如果把这两层混在一起,后面最容易出现状态不同步:页面显示播放中,系统控制中心显示暂停;耳机按了暂停,但应用 UI 没变;切下一首以后声音变了,系统封面还停留在上一首。
所以我更推荐把播放层和会话层拆成两个 Service,再用统一的状态更新方法连接。
三、创建 AVSession 时,第一件事不是 setMetadata,而是把生命周期想清楚
AVSession 的典型入口是创建会话并激活。
官方能力里,应用可以创建 AVSession,设置媒体元数据和播放状态,并响应播放、暂停、上一首、下一首等控制事件。工程上真正要注意的是:会话应该和媒体播放生命周期一起管理,而不是页面一进来就永远创建一个。
我会把会话初始化单独封装:
import { avSession } from '@kit.AVSessionKit'
export class MediaSessionService {
private session: avSession.AVSession | null = null
async create(context: Context) {
if (this.session) return
this.session = await avSession.createAVSession(
context,
'music_session',
'audio'
)
await this.session.activate()
this.registerControllerEvents()
}
async destroy() {
if (!this.session) return
await this.session.deactivate()
await this.session.destroy()
this.session = null
}
}
这段代码最关键的是 activate / deactivate / destroy 这条生命周期链。
很多联动异常,并不是 setMetadata 写错了,而是会话已经失效、重复创建,或者播放结束以后没有释放。尤其当页面多次进入退出时,如果会话生命周期没有收口,很容易出现控制事件重复响应。
四、系统媒体中心到底靠什么知道“现在播的是哪首歌”
答案就是媒体元数据。
只让系统知道“现在正在播放”是不够的,还要同步标题、作者、专辑、封面等内容。否则控制中心最多只能出现一个没有上下文的播放状态。
切歌时,我通常会把“播放器切换资源”和“会话元数据更新”放到同一个业务动作里:
async syncMetadata(track: Track) {
if (!this.session) return
await this.session.setAVMetadata({
assetId: track.id,
title: track.title,
artist: track.artist,
album: track.album,
mediaImage: track.coverUri
})
}
async syncPlaybackState(isPlaying: boolean, position: number) {
if (!this.session) return
await this.session.setAVPlaybackState({
state: isPlaying
? avSession.PlaybackState.PLAYBACK_STATE_PLAY
: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
position: {
elapsedTime: position,
updateTime: Date.now()
}
})
}
不同 API 版本的字段和枚举要以当前 SDK 为准,但工程原则很固定:播放器状态发生变化时,会话状态要跟着变。
页面上的 UI 不应该是第三套独立状态,而应该和播放器、AVSession 共用同一份业务状态源。
五、系统控制事件不是“通知”,而是业务指令
AVSession 真正有价值的地方,在于它不是单向上报。
系统控制中心、耳机、手表、车载等控制端发出“播放、暂停、下一首”时,应用会收到会话事件。此时不能只记一条日志,而要真正驱动播放器。
我会在会话初始化时统一注册这些事件:
private registerControllerEvents() {
if (!this.session) return
this.session.on('play', async () => {
await playerService.play()
await this.syncPlaybackState(true, playerService.position)
})
this.session.on('pause', async () => {
await playerService.pause()
await this.syncPlaybackState(false, playerService.position)
})
this.session.on('playNext', async () => {
const track = await playerService.next()
await this.syncMetadata(track)
await this.syncPlaybackState(true, 0)
})
this.session.on('playPrevious', async () => {
const track = await playerService.previous()
await this.syncMetadata(track)
await this.syncPlaybackState(true, 0)
})
}
这就是系统播控真正的闭环:
外部控制事件 → AVSession → 播放器业务 → 更新播放结果 → 再同步 AVSession 状态。
中间任何一步断掉,用户都会感觉“系统按钮按了没反应”或者“按了以后 UI 不对”。
六、开发时我重点看的是“页面、会话、日志能不能对上”
媒体类问题很多时候很难只靠界面判断。
比如页面暂停按钮已经切成播放图标,但系统媒体中心还显示正在播放;这时视觉上很难确定究竟是哪一层没同步。
所以开发阶段,我更习惯让日志明确打出:
- AVSession 创建成功;
- activate 完成;
- metadata 已更新;
- playbackState 已更新;
- 收到 play / pause / next 等外部指令;
- 播放器真正执行成功。

这样调试时,右侧模拟器负责看播放器结果,中间代码负责看状态收口,底部日志负责判断系统会话链路有没有走完整。
对于系统联动能力,这种“证据链”比只看按钮状态靠谱得多。
七、后台播放不是“把页面关掉声音还在”这么简单
用户对后台播放的理解很直接:退出页面甚至切到别的应用,音频继续播放,系统仍然能控制。
但工程实现不能只是“播放器对象没被销毁”。
真正完整的后台播放需要同时考虑:
- 播放能力是否符合系统后台运行规范;
- 媒体会话是否仍处于有效状态;
- 当前媒体元数据是否继续可见;
- 播放位置是否持续更新;
- 系统播控事件是否还能回到应用;
- 应用恢复前台以后 UI 是否能重新对齐当前真实播放状态。
所以应用进入后台时,我不会主动把 AVSession 清掉;只要播放还在继续,会话也应该继续保持有效。只有播放真正结束、用户明确停止或业务不再需要时,才进入 deactivate / destroy。
这和普通页面生命周期不一样。
八、播放页最好把 AVSession 的“系统状态”露出来一点
为了调试,我会在播放器页面加一张会话状态卡片:会话是否激活、当前 playbackState、输出设备、媒体信息是否已同步。

这类信息正式产品可以隐藏,但开发版本非常有用。
因为一旦系统媒体中心没有出现,你马上就能判断:
- 会话没创建;
- 会话没 activate;
- metadata 没设置;
- playbackState 没同步;
- 还是系统控制事件没有注册。
相比“重新运行一下看看”,定位效率高很多。
九、耳机、车载、手表的控制,本质上都应该回到同一套状态机
不同控制来源可能给人一种错觉,好像要分别写很多套处理逻辑。
实际上业务上更稳的设计,是不管事件来自系统媒体中心、蓝牙耳机、车载还是手表,都最终归一成同一组播放命令:
- play;
- pause;
- stop;
- next;
- previous;
- seek。
外部设备只是“事件入口”不同,真正执行的播放器逻辑应该一致。
我在调试页里会专门把“控制来源 + 收到的事件 + 处理结果”记录出来。

这样当用户反馈“耳机能暂停但车机下一首没反应”时,就能马上判断事件到底有没有传进来,而不是盲目怀疑播放器。
十、几个最常见的状态不同步问题
这类媒体会话项目,最后最容易卡住的是状态边界。
第一种是播放已经开始,但 setAVPlaybackState() 没跟上,系统仍然显示暂停。第二种是切歌后只换了播放器资源,没有更新 metadata,导致系统封面和标题滞后。第三种是系统控制事件触发了播放器,但业务状态没有更新,页面重新回到前台以后 UI 显示错误。
还有一种更隐蔽:重复注册会话事件。页面多次进入后,同一个 pause 事件被触发两三次,表面上看像播放器状态异常,实际是监听没有跟会话生命周期一起清理。
所以我会坚持三个原则:
- 播放状态只有一份业务真源;
- 所有外部控制统一进入播放器状态机;
- 会话创建、监听注册和销毁严格成对。
十一、AudioRenderer 还是 AVPlayer,要看你真正处理的音频是什么
Audio Kit 里不同播放能力适合不同场景。
如果是普通媒体文件播放,使用更高层的媒体播放器通常更省事。如果你需要直接处理 PCM、做实时预处理、效果链或更底层的音频渲染,AudioRenderer 会更灵活。
但不管底层播放最终用哪种能力,只要你希望系统控制中心和外部控制设备理解当前媒体状态,AVSession 这一层的思路基本一致。
这也是为什么我更愿意把 AVSession 看成“播控协议层”,而不是“另一个播放器”。
十二、本文小记
这次把 Audio Kit 和 AVSession Kit 串起来以后,我对 HarmonyOS 媒体能力最大的认识变化,是不再把播放器理解成一个独立页面。
真正完整的媒体体验其实有两层:底层播放负责声音,媒体会话负责系统级状态和控制。
完整链路应该是:
音频资源 → 播放器 → AVSession 元数据 / 播放状态 → 系统媒体中心与外设 → 控制事件回传 → 播放器状态更新。
这条链跑通以后,后台播放、系统控制中心、蓝牙耳机、车载、手表等体验才真正被串成一个整体。
如果后面继续往下做,我会把播放列表队列、seek 同步、输出设备切换和投播能力再接进来。做到那一步以后,媒体应用才不只是“能播放”,而是开始真正融入系统级播控体系。
更多推荐





所有评论(0)