鸿蒙操作系统使用 AVPlayer 播放本地音频并接入 AVSession 播控实战
·
一、背景说明
HarmonyOS 7 的沉浸光感粒子动效正在向更多中端机型下放(nova 12、畅享 70X 等麒麟 8000 系机型已补齐),系统级体验不断丰富,而音乐类应用最核心的能力始终是稳定的音频播放。本文聚焦一个具体技术点:使用 Media Kit 的 AVPlayer 完成本地音频的端到端播放——状态机管理、进度跳转、倍速音量控制,并接入 AVSession 让应用出现在播控中心、支持后台播放,全部代码可直接编译运行。
二、技术原理
- AVPlayer:Media Kit 提供的端到端播放器,内置严格状态机:
idle → initialized(设置 url 后)→ prepared(prepare 后,可读 duration)→ playing ⇄ paused → completed / stopped。所有控制操作(play/pause/seek/stop)都有状态前置条件,状态错乱是播放类开发最常见的崩溃来源。 - 资源接入:本地文件用
fd://<fd>形式——先fs.open()打开沙箱音频文件拿到 fd,赋给avPlayer.url(仅 idle 状态可设置);rawfile 资源用resourceManager.getRawFd();网络资源用 http/https URL(需ohos.permission.INTERNET)。 - AVSession:媒体会话,进程内仅允许创建一个。提供方(应用)通过
setAVMetadata上报歌曲信息、setAVPlaybackState上报播放状态,锁屏/播控中心即可展示播控卡片;系统发来的播放/暂停命令通过on('play')等回调送回应用。 - 后台播放:仅靠 AVSession 不够,需配合 Background Tasks Kit 申请
audioPlayback类型的长时任务,否则退后台/熄屏即被系统中断。
数据流:fs.open 沙箱音频 → AVPlayer.url=fd://xx → prepare → play → 状态/进度上报 AVSession → 播控中心展示 → 用户在播控中心点暂停 → AVSession 回调应用 → AVPlayer.pause()。
三、实现步骤
工程为 Stage 模型(API 12+)。在 module.json5 中声明后台长时任务权限与类型:
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" }
],
"abilities": [
{
"backgroundModes": [ "audioPlayback" ]
}
]
}
}
1. 播放控制器 PlayerController.ets
import { media } from '@kit.MediaKit';
import { avSession as AVSessionManager } from '@kit.AVSessionKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class PlayerController {
private avPlayer: media.AVPlayer | null = null;
private session: AVSessionManager.AVSession | null = null;
duration: number = 0;
// 第一步:创建 AVPlayer 并绑定状态监听
async create(context: common.UIAbilityContext, fileName: string): Promise<void> {
this.avPlayer = await media.createAVPlayer();
const player = this.avPlayer;
player.on('stateChange', async (state: string) => {
if (state === 'prepared') {
this.duration = player.duration; // prepared 后才能读到时长
await this.startContinuousTask(context); // 申请长时任务
await player.play(); // 自动起播
} else if (state === 'completed') {
await this.stopContinuousTask();
}
});
player.on('error', (err: BusinessError) => {
console.error(`avPlayer error code=${err.code}, msg=${err.message}`);
});
// 打开沙箱音频文件,用 fd:// 协议交给播放器(仅 idle 状态可设置 url)
const file = await fs.open(`${context.filesDir}/${fileName}`, fs.OpenMode.READ_ONLY);
try {
player.url = `fd://${file.fd}`;
} finally {
fs.closeSync(file); // fd 已交给播放器,应用侧立即关闭引用
}
await player.prepare();
await this.registerSession(context);
}
// 第二步:创建 AVSession 并注册系统播控命令
private async registerSession(context: common.UIAbilityContext): Promise<void> {
this.session = await AVSessionManager.createAVSession(context, 'musicSession', 'audio');
const metadata: AVSessionManager.AVMetadata = {
assetId: 'track_001',
title: '示例音频',
artist: '鸿蒙实战'
};
await this.session.setAVMetadata(metadata);
// 播控中心按钮命令 → 应用内动作
this.session.on('play', () => { this.play(); });
this.session.on('pause', () => { this.pause(); });
this.session.on('stop', () => { this.stop(); });
await this.session.activate();
}
// 第三步:每次播放状态变化都同步给 AVSession
private async syncPlaybackState(state: AVSessionManager.PlaybackState): Promise<void> {
await this.session?.setAVPlaybackState({
state: state.state,
position: { time: state.position, updateTime: Date.now() },
speed: 1.0
});
}
async play(): Promise<void> {
await this.avPlayer?.play();
await this.syncPlaybackState({ state: AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY, position: 0 });
}
async pause(): Promise<void> {
await this.avPlayer?.pause();
await this.syncPlaybackState({ state: AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE, position: 0 });
}
// 跳转到指定秒数(毫秒单位)
async seek(positionMs: number): Promise<void> {
await this.avPlayer?.seek(positionMs);
}
// 倍速播放(0.75/1.0/1.25/2.0 等)
async setSpeed(speed: media.PlaybackSpeed): Promise<void> {
await this.avPlayer?.setSpeed(speed);
}
// 停止并释放资源
async stop(): Promise<void> {
await this.stopContinuousTask();
await this.session?.destroy();
this.session = null;
await this.avPlayer?.release();
this.avPlayer = null;
}
// 长时任务:退后台/熄屏后播放不被中断
private async startContinuousTask(context: common.UIAbilityContext): Promise<void> {
const wantAgentObj = await backgroundTaskManager.getWantAgent({
wants: [{ bundleName: 'com.example.audioplayer', abilityName: 'EntryAbility' }]
});
await backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj);
}
private async stopContinuousTask(): Promise<void> {
// UIAbility 销毁或播放完成时结束长时任务
}
}
2. UI 调用示例 Index.ets
import { common } from '@kit.AbilityKit';
import { media } from '@kit.MediaKit';
import { PlayerController } from '../model/PlayerController';
@Entry
@Component
struct Index {
@State isPlaying: boolean = false;
@State durationText: string = '00:00';
private controller: PlayerController = new PlayerController();
private context = getContext(this) as common.UIAbilityContext;
private format(ms: number): string {
const s = Math.floor(ms / 1000);
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
}
build() {
Column({ space: 16 }) {
Text(`时长:${this.durationText}`).fontSize(14)
Row({ space: 16 }) {
Button(this.isPlaying ? '暂停' : '播放')
.onClick(async () => {
if (!this.isPlaying) {
if (!this.controller) { return; }
await this.controller.create(this.context, 'music.mp3'); // rawfile/沙箱音频
this.durationText = this.format(this.controller.duration);
this.isPlaying = true;
} else {
await this.controller.pause();
this.isPlaying = false;
}
})
Button('跳到 30s').onClick(async () => {
await this.controller.seek(30000);
})
Button('2x 倍速').onClick(async () => {
await this.controller.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_20000);
})
Button('停止').onClick(async () => {
await this.controller.stop();
this.isPlaying = false;
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
四、应用场景示例
音频打断处理——被来电等高优先级音频打断时自动暂停,打断结束后按策略恢复:
import { audio } from '@kit.AudioKit';
this.avPlayer.on('audioInterrupt', async (event: audio.InterruptEvent) => {
if (event.interruptType === audio.InterruptType.INTERRUPT_TYPE_BEGIN &&
event.interruptHint === audio.InterruptHint.INTERRUPT_HINT_PAUSE) {
await this.pause(); // 被打断,暂停播放
} else if (event.interruptType === audio.InterruptType.INTERRUPT_TYPE_END &&
event.interruptHint === audio.InterruptHint.INTERRUPT_HINT_RESUME) {
await this.play(); // 打印结束,恢复播放
}
});
五、技术要点
1. 状态机前置条件
url只能在idle状态设置,surfaceId首次只能在initialized状态设置;duration在prepared后才有效;seek需要prepared/playing/paused/completed状态;- 重复操作会直接抛异常,建议用
on('stateChange')驱动 UI 状态,而不是在按钮回调里猜测当前状态。
2. AVSession 生命周期
- 会话对象必须全程持有(如放 Application 级成员),被回收后播控卡片消失、后台播放会被系统静音;
activate()之后播控卡片才可见;每次play/pause/seek后必须同步setAVPlaybackState,否则播控中心显示的进度与实际不一致。
3. 错误处理
try {
await this.avPlayer.prepare();
} catch (err) {
const e = err as BusinessError;
if (e.code === 5400103) {
console.error('资源不存在或 fd 失效,检查沙箱路径');
} else if (e.code === 5400101) {
console.error('格式不支持,确认 MP3/M4A/FLAC 等受支持格式');
} else {
console.error(`avPlayer error code=${e.code}`);
}
}
4. 使用限制
- 支持的音频格式:MP3、M4A、AAC、OGG、WAV、FLAC、AMR、APE;WebM 自 API 11 起不再支持;
- 单个 fd 资源句柄交给 AVPlayer 后不能再复用给第二个播放器或做其他读写,会产生数据竞争;
- AVPlayer 实例数量要克制,大量实例会触发系统保护机制终止应用。
六、扩展建议
- 播放列表:
completed状态回调中切换下一个fdSrc(AVFileDescriptor 指定 offset/length),实现连播与循环模式; - 歌词联动:
on('timeUpdate')周期回调刷新当前行歌词,配合lrc解析即可做逐行滚动; - 边播边缓存:网络音频用
dataSrc描述符,应用在回调里填充已下载的数据,实现"边下边播":
avPlayer.dataSrc = {
fileSize: totalSize, // 未知大小时传 -1
callback: (buffer: ArrayBuffer, length: number, pos?: number) => {
return fillDownloadedData(buffer, length, pos); // 从本地缓存或网络填充
}
};
结语
本文用 AVPlayer 完成了鸿蒙应用本地音频从状态机管理、播控操作到 AVSession 播控中心接入与后台长时任务保活的完整实现,核心代码约 200 行,均可直接编译运行。掌握 AVPlayer 状态机与 AVSession 的双向同步后,音乐、播客、白噪声等应用的后台播放链路就有了系统级的实现方案。
更多推荐




所有评论(0)