HarmonyOS 7 音频与媒体控制实战 04:接入 AVSession 实现系统级播放控制

做了个音乐播放器,用户反馈说锁屏的时候看不到歌曲信息,也不能在控制中心切歌,每次都要解锁打开 App 才能操作。这个体验确实差,系统播放器都支持锁屏控制,我们的播放器也得支持。
HarmonyOS 7 的 AVSession 就是干这个的——把应用的播放状态和媒体元数据暴露给系统,锁屏界面、控制中心、蓝牙耳机都能控制播放。这篇就讲讲怎么接入 AVSession,让播放器变成系统级的媒体应用。
一、真实开发中遇到的问题
最开始以为就是个通知,把歌曲信息放上去就行。真做了才发现不是那么回事:
第一,锁屏界面显示什么?歌名、歌手、专辑封面,这些元数据怎么传给系统?什么时候更新?
第二,控制中心的播放暂停按钮怎么联动?用户在控制中心点了暂停,我们的应用怎么收到这个事件?点了上一首下一首呢?
第三,蓝牙耳机按键怎么响应?用户按耳机上的播放键,应用怎么收到?
第四,播放状态同步。应用里点了暂停,锁屏上的按钮状态也要跟着变。状态不一致怎么办?
二、这个能力怎么接入
AVSession 的接入思路:
1. 创建 AVSession:调用 AVSessionManager.createAVSession() 创建一个媒体会话,指定会话类型(音频/视频)。
2. 设置媒体元数据:歌名、歌手、专辑名、封面图,通过 setAVMetadata() 传给系统。
3. 设置播放状态:当前播放状态(播放中/暂停/停止)、播放进度,通过 setAVPlaybackState() 更新。
4. 注册事件监听:监听系统发来的控制事件——播放、暂停、上一首、下一首、快进快退。
5. 销毁会话:应用退出或者播放器销毁的时候,release() 释放会话。
整个过程就是:应用告诉系统"我在放这首歌",系统把信息显示在锁屏和控制中心;用户在系统侧操作,系统把事件发给应用,应用执行对应的操作。

三、关键代码怎么写
下面是封装的 AVSession 管理器,文件位置在 entry/src/main/ets/utils/AVSessionManager.ets:
import { avSession } from '@kit.AVSessionKit';
export class AVSessionManager {
private session: avSession.AVSession | null = null;
private controller: avSession.AVSessionController | null = null;
// 初始化媒体会话
async initSession(): Promise<void> {
// 创建会话
this.session = await avSession.createAVSession(
getContext(this).resourceManager,
'MusicPlayer_' + Date.now().toString(),
'audio'
);
// 获取控制器
this.controller = this.session.getController();
// 注册控制事件监听
this.session.on('play', () => {
console.info('System command: play');
// 调用播放接口
this.onPlayCallback?.();
});
this.session.on('pause', () => {
console.info('System command: pause');
this.onPauseCallback?.();
});
this.session.on('playNext', () => {
console.info('System command: next');
this.onNextCallback?.();
});
this.session.on('playPrevious', () => {
console.info('System command: previous');
this.onPrevCallback?.();
});
// 激活会话
await this.session.activate();
}
// 更新媒体元数据(切歌时调用)
async updateMetadata(track: TrackInfo): Promise<void> {
if (!this.session) return;
const metadata: avSession.AVMetadata = {
title: track.title,
artist: track.artist,
album: track.album,
duration: track.duration,
mediaImage: track.coverUrl // 封面图 URI
};
await this.session.setAVMetadata(metadata);
}
// 更新播放状态(播放/暂停/进度变化时调用)
async updatePlaybackState(state: PlaybackState): Promise<void> {
if (!this.session) return;
const playbackState: avSession.AVPlaybackState = {
state: state.isPlaying
? avSession.PlaybackState.PLAYBACK_STATE_PLAY
: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
time: {
elapsedTime: state.currentPosition,
updateTime: Date.now()
},
speed: 1.0
};
await this.session.setAVPlaybackState(playbackState);
}
// 注册回调
onPlayCallback: (() => void) | null = null;
onPauseCallback: (() => void) | null = null;
onNextCallback: (() => void) | null = null;
onPrevCallback: (() => void) | null = null;
// 销毁会话
async release(): Promise<void> {
if (this.session) {
await this.session.deactivate();
await this.session.destroy();
this.session = null;
}
}
}
// 歌曲信息
interface TrackInfo {
title: string;
artist: string;
album: string;
duration: number;
coverUrl: string;
}
// 播放状态
interface PlaybackState {
isPlaying: boolean;
currentPosition: number;
}
这段代码的核心:createAVSession 创建会话,注册 play/pause/next/previous 事件监听。updateMetadata 在切歌的时候调用,把新歌的信息传给系统。updatePlaybackState 在播放状态变化的时候调用,同步给系统。
播放器页面怎么集成?文件位置 pages/PlayerPage.ets:
@Entry
@Component
struct PlayerPage {
@State currentTrack: TrackInfo = {} as TrackInfo;
@State isPlaying: boolean = false;
private avSession: AVSessionManager = new AVSessionManager();
async aboutToAppear() {
await this.avSession.initSession();
// 绑定回调:系统发来的控制指令
this.avSession.onPlayCallback = () => this.play();
this.avSession.onPauseCallback = () => this.pause();
this.avSession.onNextCallback = () => this.nextTrack();
this.avSession.onPrevCallback = () => this.prevTrack();
}
// 播放
async play() {
// 实际播放逻辑...
this.isPlaying = true;
await this.avSession.updatePlaybackState({
isPlaying: true,
currentPosition: 0
});
}
// 暂停
async pause() {
this.isPlaying = false;
await this.avSession.updatePlaybackState({
isPlaying: false,
currentPosition: 30000 // 当前进度
});
}
// 切歌
async onTrackChanged(track: TrackInfo) {
this.currentTrack = track;
await this.avSession.updateMetadata(track);
}
async aboutToDisappear() {
await this.avSession.release();
}
build() {
Column() {
// 专辑封面
Image(this.currentTrack.coverUrl)
.width(240)
.height(240)
// 歌名歌手
Text(this.currentTrack.title)
.fontSize(20)
Text(this.currentTrack.artist)
.fontSize(14)
// 控制按钮
Row({ space: 48 }) {
Button('上一首').onClick(() => this.prevTrack())
Button(this.isPlaying ? '暂停' : '播放')
.onClick(() => {
if (this.isPlaying) this.pause();
else this.play();
})
Button('下一首').onClick(() => this.nextTrack())
}
.margin({ top: 32 })
}
.padding(24)
}
}

四、运行过程中怎么处理异常
AVSession 的异常场景:
1. 会话创建失败:设备不支持 AVSession,或者系统资源不足。catch 住异常,降级成普通通知,不影响播放功能本身。
2. 激活失败:activate() 失败了,说明系统不认可这个会话。锁屏控制就用不了,但应用内播放正常。
3. 状态不同步:应用内暂停了,但锁屏上的状态没更新。要确保每次状态变化都调用 updatePlaybackState(),不要漏。
4. 多个会话冲突:如果有两个播放器同时激活 AVSession,系统会怎么处理?一般是后激活的顶掉前面的。要确保只有一个活跃会话。
五、实际开发中容易忽略的问题
1. 封面图要传 URI:mediaImage 是图片的 URI,不是 base64。本地封面图要用 file:// 协议或者资源 URI。
2. 进度更新不要太频繁:每播一秒就 updatePlaybackState 一次,锁屏上的进度条会刷新。太频繁了没必要,每秒一次够了。
3. 会话要及时销毁:播放器退出的时候一定要 release(),不然系统媒体列表里一直挂着你的应用,用户以为还在播放。
4. 蓝牙按键也要支持:AVSession 注册之后,蓝牙耳机的播放/暂停键自动就支持了,不用额外写代码。但要测试不同品牌的耳机兼容性。
5. 后台播放:AVSession 激活之后,应用切到后台还能继续播放。但要声明后台任务权限,不然系统会杀。
AVSession 是播放器应用的标配。接入之后,锁屏、控制中心、蓝牙耳机都能控制播放,体验一下子就专业了。代码量不大,但细节不少——元数据更新时机、状态同步、资源释放,每一步都要做到位。
更多推荐



所有评论(0)