HarmonyOS 6媒体会话与后台播放实战:仿云音乐AVSession Kit源码教学

发布时间:2026/10/1 22:43:45
HarmonyOS 6媒体会话与后台播放实战:仿云音乐AVSession Kit源码教学 最近在做HarmonyOS 6上的音频应用适配升级到API20之后AVSession Kit的变化确实不小控制指令回调更规范了会话层级也更清晰配合后台播放管理基本能做到“一份会话状态全端同步”。这篇我就以仿某云音乐为例做一期源码教学带大家把媒体会话和后台播放这条链路完整捋一遍。适合正在做音乐、播客、有声书类应用的开发者也适合刚接触HarmonyOS 6媒体框架的朋友。你会看到从创建AVSession、上报元数据和播放状态到接收控制中心指令、申请长时任务、退后台不打断播放的完整代码。我尽量把关键参数和选型原因说清楚这些坑都是一次一次踩出来的照着做能少走不少弯路。1. 项目背景与整体设计思路1.1 为什么选择AVSession Kit而不是自己写通知栏很多同学第一次接媒体会话时都会有个疑问以前自己画通知栏、自己监听锁屏按键不是也能跑吗为什么要引入AVSession Kit这么一套东西答案很简单AVSession是系统级媒体会话的统一出口通知栏、控制中心、锁屏卡片、耳机按键、语音助手全部通过它和你的播放器通信。你只需要把状态上报给系统剩下所有系统UI都由HarmonyOS帮你去渲染。API20的AVSession Kit新特性里最直观的变化是“会话控制项”更精细了。以前很多时候是系统告诉你“用户点了播放”你再去处理现在你可以明确告诉系统当前会话支持哪些控制能力不支持哪些。比如纯音频播放器通常只支持播放、暂停、上一首、下一首拖拽进度条则要看播放器能力。这样控制中心就不会出现一个点了没反应的按钮体验会干净很多。还有一个很实际的原因多端一致。我的项目要适配手机和折叠屏之后可能还要上手表。如果自绘通知栏每端UI都要单独写工作量翻倍。用AVSession Kit锁屏封面、控制中心卡片、蓝牙耳机状态栏显示这些全是系统根据元数据和播放状态自动生成的我们只需要维护好数据源不需要关心UI层具体怎么画。这对中小团队来说省下的就是真金白银的开发工时。1.2 仿云音乐的功能拆解与技术链路我们要做的仿云音乐核心功能就四个前台播放、后台播放下不打断、控制中心能操作、锁屏能显示封面。听起来不复杂但每个环节都牵扯到不同模块。我习惯把工程拆成三个管理类PlayerManager负责实际播放封装AVPlayerSessionManager负责AVSession的创建、状态上报、指令监听BackgroundManager负责后台长时任务申请和释放。业务层只和三个Manager打交道不直接调用Kit API。这样拆的好处是换播放器实现时SessionManager不用动单独测试后台逻辑时也不需要真的播放音频。技术链路是这样的AVPlayer把音频解码输出声音和控制进度SessionManager把歌曲信息、播放状态同步给AVSession系统拿到AVSession后在通知栏和控制中心展示卡片并把用户的操作通过事件回调给SessionManager后台驻留则交给backgroundTaskManager申请音频播放类型的长时任务。整条链路的核心是会话和状态播放器反而是相对独立的一环。2. 核心概念与环境准备2.1 AVSession Kit的核心概念会话端与控制端AVSession Kit里有两个关键角色AVSession和AVSessionController。AVSession由播放应用创建可以理解成“播放器的对外代理”它保存了元数据和播放状态AVSessionController则是系统侧的视图控制中心、锁屏、甚至其他应用都可以拿到Controller来查询状态、发指令。我们自己就是通过AVSession接收Controller转发的指令。有个容易混淆的点AVSession本身不做播放它只是个“状态容器”和“指令通道”。真正的播放逻辑必须自己实现会话只是把状态暴露出去。你可以把它想象成前台接待用户按了耳机键系统把这个动作告诉前台前台再喊你一句“有人要暂停了”你才去执行暂停。如果你没开前台没有激活会话系统就只能干瞪眼。API20里元数据和播放状态的数据结构也更清晰了。AVMetadata主要描述歌曲本身比如标题、歌手、专辑、时长、封面图AVPlaybackState描述播放过程比如当前是播放还是暂停、进度位置、速度、缓冲进度。两者要分开设置并且每次更新都要保证数据完整否则系统UI会显示一个残缺的卡片很影响观感。2.2 API20工程配置与权限申请先说工程环境。我用的DevEco Studio是支持HarmonyOS 6 SDK的版本构建目标选API20。API20的包管理开始推荐Kit方式导入不再用老的ohos.multimedia.avSession那种散落方式。代码里建议这样写import { avSession } from kit.AVSessionKit; import { backgroundTaskManager } from kit.BackgroundTasksKit;如果IDE提示导入失败或者标红大概率是SDK版本没切对先检查build-profile.json5里的compileSdkVersion是不是20然后再同步工程。接下来是权限。后台播放属于长时任务必须在module.json5里申请ohos.permission.KEEP_BACKGROUND_RUNNING。直接在module节点下加requestPermissionsrequestPermissions: [ { name: ohos.permission.KEEP_BACKGROUND_RUNNING } ]注意这里的权限是系统弹窗外的“授权类型”一般不需要用户弹窗确认但真机上系统设置里“应用启动管理”会影响长时任务能否正常拉起。调试时如果一直失败先去系统设置里把应用的后台活动权限打开这个坑后面单独说。另外不同版本的SDK对Ability的后台模式声明要求不一样。有的版本需要在module.json5对应的ability节点里补充类似backgroundModes的声明表示这个Ability允许哪种后台任务。API20的具体字段建议以当前SDK的配置文件模板为准但原则是权限声明、后台模式、代码里的BackgroundMode三者要一致都指向音频播放缺一个就容易在真机上被系统拒绝。3. 媒体会话实现实战3.1 创建AVSession并激活三步操作搞定创建会话这部分网上很多教程写得比较零散其实核心就三步创建、设置数据、激活。第一步用createAVSession创建实例第二个参数是会话标记我习惯用业务名“CloudMusicSession”第三个参数是会话类型纯音乐就是AUDIO。import { avSession } from kit.AVSessionKit; import { common } from kit.AbilityKit; private session: avSession.AVSession | null null; async initSession(context: common.UIAbilityContext) { this.session await avSession.createAVSession( context, CloudMusicSession, avSession.SessionType.AUDIO ); await this.session.activate(); }很多新手会忘记activate()导致会话虽然在但没生效控制中心不显示卡片、指令回调也收不到。激活操作是告诉系统“这个会话已经开始服务了”必须在数据上报之后再激活还是可以先激活再上报我的经验是两种都行但建议先创建会话、设置好元数据和初始播放状态最后再activate这样控制中心第一次看到会话时数据就是完整的不会先呈现一个空白卡片再突然变出来。3.2 上报元数据与播放状态控制中心才能认识你创建完会话就要喂数据。AVMetadata里的字段很多但音乐场景最核心的是assetId、title、artist、album、duration、mediaImage。assetId一定要写业务上的唯一标识后续切歌、上报进度都靠它来区分。const metadata: avSession.AVMetadata { assetId: song_001, title: 晚风, artist: 示例歌手, album: 示例专辑, duration: 245000, // 单位毫秒 mediaImage: this.coverPixelMap // 最好压缩到600*600以内 }; await this.session.setAVMetadata(metadata);这里的mediaImage是PixelMap类型不是普通图片路径。如果你拿到的是网络图片需要先用image组件解码成PixelMap再塞进去。我踩过坑直接把文件路径传进去控制中心封面一直不出来日志报“invalid mediaImage”。另外图片别用原始大图我从图库挑一张5MB的照片测试过内存直接往上窜系统UI渲染也卡压缩到600x600左右再传观感最合适。播放状态AVPlaybackState的字段稍微复杂一点但关键是state、position、speed、bufferedTime、updateTime。第一次播放时必须把缓冲进度和当前进度都上报否则进度条是死的。const playbackState: avSession.AVPlaybackState { state: avSession.PlaybackState.PLAYING, position: 60000, // 当前播放到第60秒 speed: 1.0, bufferedTime: 180000, // 缓冲到第180秒 updateTime: Date.now() }; await this.session.setAVPlaybackState(playbackState);关于进度更新有个很重要的性能细节不要每秒钟都调setAVPlaybackState。系统会在通知栏和控制中心自己按时间轴推算进度你只需要在加载完成、播放暂停、seek、切歌这些节点上报一次状态即可。如果你真的一秒上报一次控制中心倒是不会崩但IPC通信压力会明显变大复杂场景下会出现卡顿掉帧。我一开始就是“强迫症”秒更后来改成事件驱动更新效果好很多。3.3 接收控制中心指令并回写暂停/播放会话创建好、数据也齐了接下来就是最关键的一环让控制中心和耳机的按键能真正控制播放器。AVSession的事件监听是分命令注册的播放、暂停、上一首、下一首、seek都是独立事件。private registerSessionCallbacks() { if (!this.session) return; this.session.on(play, () { this.playerManager.play(); this.updatePlaybackState(avSession.PlaybackState.PLAYING); }); this.session.on(pause, () { this.playerManager.pause(); this.updatePlaybackState(avSession.PlaybackState.PAUSED); }); this.session.on(seek, (time: number) { this.playerManager.seek(time); this.updatePlaybackState(avSession.PlaybackState.PLAYING, time); }); this.session.on(next, () { this.playNextSong(); }); this.session.on(previous, () { this.playPreviousSong(); }); }如果你希望通知栏卡片上显示进度条并且可以拖拽需要监听seek事件而且拖拽过程中系统会连续回调很多次建议在回调里做防抖比如100ms内只处理最后一次避免频繁seek导致播放器卡顿。另外所有回调都不保证在UI线程不要在回调里直接操作UIScheduler相关的东西正确做法是切到主线程或直接用异步接口操作播放器。关于释放页面销毁或播放器停止时要记得this.session.off(play)等解绑事件否则会话虽然release了但事件回调仍然可能被触发出现“幽灵暂停”之类的问题。我见过有同学在单例Manager里不释放事件切了三个页面后按一下耳机键播放器状态乱跳解绑之后就干净了。4. 后台播放管理实战4.1 用长时任务保活申请时机与代码模板只创建AVSession不申请长时任务应用一旦退到后台很快就会被系统挂起音乐自然就断了。AVSession本身不管应用存活它只负责“会话还存在”至于应用进程是否还活着需要后台长时任务来保证。音乐播放对应BackgroundMode.AUDIO_PLAYBACK启动长时任务需要传一个WantAgent其实就是系统点通知栏时拉回哪个Ability的意图描述。这里要注意WantAgent的构造逻辑不能省直接传空对象会抛错。代码大概长这样import { backgroundTaskManager } from kit.BackgroundTasksKit; // 实际项目中createBackToEntryAgent内部会用wantAgent.getWantAgent构造 const wantAgent await this.createBackToEntryAgent(); await backgroundTaskManager.startBackgroundRunning( this.context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgent );什么时候申请长时任务我的建议是“播放器真正开始出声之后”再申请。不要在创建AVSession的同时就申请也不要在onBackground里才急匆匆申请。前者容易被系统判定为滥用后台资源后者可能因为时序太晚被拒绝。正确节奏是用户点播放、AVPlayer触发onStateChange且状态为PLAYING时再去申请长时任务。暂停之后如果一段时间内没有继续播放就调用backgroundTaskManager.stopBackgroundRunning(this.context)释放长时任务让系统知道你不需要常驻了。4.2 处理Ability前后台切换暂停不搞神秘后台播放的基础是长时任务但Ability的生命周期同样要处理好。当应用退到后台onBackground回调触发时播放管理器要判断正在播放就保持现状不要释放资源没在播放就趁机释放播放器节省内存。// EntryAbility 里的示意逻辑 onBackground() { if (this.playManager.isPlaying()) { // 保持播放不停止、不释放AVPlayer return; } this.playManager.releaseResource(); }有一种场景容易被忽略应用被用户主动滑掉不是普通的onBackground这种时候系统可能直接销毁Ability。如果此时还在播放长时任务会保证进程不挂但Ability的UI可能已经不在。为了避免交互错乱我一般会在onDestroy里做一次兜底判断如果真的在播放就让播放管理器进入“纯后台模式”不再尝试刷新UI只维护AVSession状态。另外回到前台时要检查AVSession是否还活着。如果某些极端情况下会话被系统回收了需要在onForeground里重新拉起来。我封装了一个ensureSession()方法内部判断session实例是否为空、是否激活不满足就重建这样业务层只需要调一次不用到处关心会话生命周期。4.3 锁屏与控制中心卡片体验优化很多人以为锁屏媒体卡片是应用自己画的其实不是。系统通过AVSession的元数据和播放状态自动生成控制中心和锁屏卡片。你要做的就是让数据尽量完整、更新尽量及时剩下的交给系统。API20允许你通过设置控制项来控制卡片上展示哪些操作按钮。比如你的播放器暂时不支持后台播放下“下一首”就可以在会话里把next能力关掉卡片上对应的按钮就会置灰或者不显示。这里的“控制项”在不同SDK版本里方法名略有差异但语义是相同的告诉系统这张卡片支持哪些按钮、不支持哪些。实现云音乐那种“上一首、播放/暂停、下一首”三键布局就是把这三个控制项都打开别的关掉。另外封面图的显示有一点延迟。网络封面需要解码成PixelMap这个过程有可能比歌曲开始播放慢。我试过直接同步加载大图结果切歌时控制中心封面还是上一首很尴尬。解决办法是切歌时先用上一张封面占位等新封面解码完再更新元数据这样视觉上是无缝过渡的。也可以提前预解码下一首歌的封面在播放器加载的同时更新到会话里体验会更好。5. 常见问题与排查技巧实录5.1 长时任务启动失败的关键排查清单后台长时任务这part我在真机上遇到的问题最多。这里整理一个排查速查表可以照着顺序检查现象可能原因解决思路startBackgroundRunning报错权限没配检查module.json5里有没有KEEP_BACKGROUND_RUNNING申请时明明在播放却失败不是“真正播放状态”确认AVPlayer状态是PLAYING而不是加载中真机测试时任务被系统杀掉应用后台管理受限系统设置里允许应用后台活动关闭电池优化限制退到后台几秒后音乐停止WantAgent构造异常确认WantAgent非空且能启动到目标Ability锁屏卡片显示但按钮不可用没有提前声明控制项设置会话支持的控制能力还有一个小经验真机调试时不要用“开发者模式里的后台进程限制”来压测那个会把正常的长时任务也杀掉造成误判。最好先在正式的同款系统版本上侧载安装测试。5.2 控制指令回调不触发或重复触发的处理明明代码里注册了on(play)控制中心点了播放应用却没有任何反应。我排查过很多次大部分是三个原因会话没有activate、事件没有注册成功、或者是回调抛了异常被系统吞了。先说activate必须在createAVSession之后调用具体在哪个时机不会错我会在创建完、设置完元数据之后主动调一次并且用日志确认返回值。第二事件注册要在会话激活后、用户可操作前完成不要等到播放器prepare之后才注册。第三回调里如果出现异常通常不会直接崩溃但会用日志打印错误你看不到所以我建议在on回调里第一行就加hilog确认系统是否真的把指令发过来了。重复触发的问题多半是同一个事件在多个页面注册了。比如列表页注册一次详情页又注册一次每次都没off按一下耳机键两个回调都跑播放器被暂停两次。解决方式是把事件注册的逻辑收敛到SessionManager单例里不管页面怎么跳转只有一份注册。确实要跨页面复用也要保证on/off成对出现。5.3 音频焦点冲突来电、切到其他播放器音乐App最怕来电话。AVSession虽然负责媒体会话但音频焦点冲突需要单独处理。当其他应用抢占音频焦点时系统会有中断事件通知比如来电、语音助手启动、另一个播放器开始播放。在实际项目里我的处理逻辑是这样的收到暂停类中断就立即暂停并记录当前进度收到恢复类中断时如果用户之前正在播放则恢复播放但有些场景也要尊重用户主动暂停的状态。比如用户在我播放音乐时接了个电话通话结束通常希望音乐继续但如果是用户自己打开了另一个音乐App那我这边就不该自动恢复避免两个声音打架。AVSession Kit的事件里会有中断回调不同版本叫法略有不同但原则上都会给出中断的类型和提示。重点不是API名字而是业务上的“是否需要自动恢复”要谨慎。仿云音乐这类播放器的做法是来电打断直接暂停电话结束后回到音乐页时再提示用户而不是全自动播放这样比较克制用户不会觉得被突然惊吓。5.4 真机调试的独家避坑技巧开发AVSession千万不能用模拟器模拟器的锁屏界面和控制中心和真机差异很大很多指令回调在模拟器上压根不触发会浪费大量时间。我一开始图省事用模拟器调结果怎么点通知栏都没反应换真机一次通过从此老老实实真机调试。留意日志关键词想看AVSession是否生效过滤AVSession想看后台任务过滤BackgroundTask。系统服务和应用进程是分开的如果回调没触发去系统日志里搜索AVSessionManager相关错误通常能找到原因。另外一个容易被忽略的点调试断点打在AVSession回调里时经常不会命中。因为控制中心指令是从系统服务进程调度过来的不是我们App自己调用的断点有时候会断在UI线程之外IDE不一定能感知。我的做法是在回调里打hilog用日志确认触发内容和时间比断点靠谱得多。一点个人实战心得代码写了这么久最深的体会是AVSession和后台播放的时序问题会话激活、长时任务申请、播放器真实状态这三者必须对齐。我见过太多“控制中心显示播放中其实声音早就断了”的案例基本都是数据状态和实际播放器状态脱节。解决它没有捷径就是老老实实把所有播放器状态变化都同步给会话暂停就是暂停加载就是加载不要为了显示好看而做假状态。还有一个值得尝试的方向把SessionManager里“更新元数据”和“更新播放状态”这两个动作做成异步队列。因为切歌时会有连续多次状态更新如果并发执行后一个请求可能覆盖前一个导致控制中心显示混乱。我用了一个简单的链表串行执行问题就消失了。这个细节虽然不起眼但真正运行时体验差异很大。希望这些源码级经验能帮你少踩一些坑一次性把媒体会话做好。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询