HarmonyOS开发中的音频播放:AVPlayer、SoundPool 与播放状态机全解析
HarmonyOS开发中的音频播放:AVPlayer、SoundPool 与播放状态机全解析
📌 核心要点:掌握 HarmonyOS 音频播放的核心 API——AVPlayer 与 SoundPool,理解播放状态机流转机制,实现从简单播放到播放列表管理的完整音频播放方案
一、背景与动机
你有没有这样的经历——打开一个音乐 App,点击播放按钮,音乐瞬间流淌出来,进度条缓缓前进,切歌丝滑无卡顿,后台播放也不中断?这看似简单的体验背后,其实藏着一套精密的音频播放引擎。
音频播放,是几乎所有多媒体应用的基础能力。不管是音乐播放器、短视频、播客、还是游戏音效,都离不开它。但很多开发者第一次接触 HarmonyOS 的音频播放 API 时,往往会被状态机、异步回调、资源释放这些概念搞得一头雾水。
为什么需要深入理解音频播放?
- 状态机是核心:AVPlayer 有严格的状态流转,不按规矩来就会崩溃或无声
- SoundPool 适合短音效:游戏里的枪声、按钮点击音,用 AVPlayer 太重了
- 播放列表管理:真实场景从来不是播一首歌那么简单,预加载、无缝切换才是难点
- 资源管理:音频资源不释放,内存泄漏分分钟找上门
今天这篇文章,咱们就把 HarmonyOS 音频播放的里里外外讲透。
二、核心原理
2.1 AVPlayer 播放状态机
AVPlayer 是 HarmonyOS 提供的音频/视频播放核心类。它最大的特点就是——状态驱动。你不能随便调用 play(),必须先让播放器进入正确的状态。
打个比方:AVPlayer 就像一台精密的咖啡机。你不能在机器还没预热的时候就按"出咖啡"按钮,得先开机(idle)、加水加豆(initialized)、预热(prepared),然后才能出咖啡(playing)。每一步都有严格的顺序。
stateDiagram-v2
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
[*] --> idle : 创建AVPlayer
idle --> initialized : setSource()
initialized --> prepared : prepare()
prepared --> playing : play()
playing --> paused : pause()
paused --> playing : play()
playing --> stopped : stop()
stopped --> prepared : prepare()
prepared --> stopped : stop()
playing --> completed : 播放完成
completed --> stopped : stop()
completed --> playing : play()
any --> released : release()
any --> error : 异常
class idle primary
class initialized info
class prepared warning
class playing primary
class paused warning
class stopped error
class completed purple
class released error
class error error
状态机关键规则:
| 当前状态 | 允许的操作 | 目标状态 |
|---|---|---|
| idle | setSource | initialized |
| initialized | prepare | prepared |
| prepared | play | playing |
| playing | pause / stop | paused / stopped |
| paused | play | playing |
| stopped | prepare | prepared |
| 任何状态 | release | released |
2.2 AVPlayer vs SoundPool 对比
这俩货虽然都能播放音频,但定位完全不同:
| 特性 | AVPlayer | SoundPool |
|---|---|---|
| 适用场景 | 长音频(音乐、播客) | 短音效(游戏音效、UI反馈音) |
| 音频格式 | MP3、AAC、FLAC、WAV 等 | WAV、OGG 等短格式 |
| 延迟 | 较高(需 prepare) | 极低(预加载到内存) |
| 并发播放 | 单实例单流 | 支持多实例并发 |
| 内存占用 | 流式解码,占用低 | 全量加载到内存 |
| 播放控制 | 完整(暂停、seek、速率) | 简单(播放、停止、音量) |
一句话总结:播音乐用 AVPlayer,播音效用 SoundPool。
2.3 播放列表管理架构
一个完整的播放列表管理器需要解决以下问题:
flowchart TB
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
A[播放列表管理器] --> B[当前播放索引]
A --> C[预加载队列]
A --> D[播放模式]
A --> E[AVPlayer实例]
B --> B1[顺序播放]
B --> B2[随机播放]
B --> B3[单曲循环]
C --> C1[下一首预加载]
C --> C2[无缝切换]
D --> D1[播放/暂停]
D --> D2[上一首/下一首]
D --> D3[Seek定位]
E --> E1[状态监听]
E --> E2[错误处理]
E --> E3[资源释放]
class A primary
class B info
class C warning
class D purple
class E error
三、代码实战
3.1 AVPlayer 基础播放器
最基础的用法——播放一段本地音频文件,完整的状态机流转:
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct BasicAudioPlayer {
// AVPlayer 实例
private avPlayer: media.AVPlayer | null = null;
// 播放状态
@State isPlaying: boolean = false;
@State currentTime: number = 0;
@State duration: number = 0;
@State playerState: string = 'idle';
aboutToAppear(): void {
this.initAVPlayer();
}
aboutToDisappear(): void {
this.releasePlayer();
}
/**
* 初始化 AVPlayer 并注册状态监听
*/
async initAVPlayer(): Promise<void> {
try {
// 创建 AVPlayer 实例
this.avPlayer = await media.createAVPlayer();
this.playerState = 'idle';
// 状态变化监听 —— 状态机的核心
this.avPlayer.on('stateChange', (state: string) => {
this.playerState = state;
console.info(`[AVPlayer] 状态变化: ${state}`);
switch (state) {
case 'initialized':
// initialized 后调用 prepare 进入 prepared 状态
this.avPlayer?.prepare();
break;
case 'prepared':
// prepared 后可以获取时长等信息
console.info('[AVPlayer] 准备完成,可以播放');
break;
case 'playing':
this.isPlaying = true;
break;
case 'paused':
case 'stopped':
case 'completed':
this.isPlaying = false;
break;
case 'error':
// error 状态后必须重新创建实例
this.avPlayer = null;
break;
}
});
// 播放进度监听
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentTime = time; // 单位:毫秒
});
// 时长获取监听
this.avPlayer.on('durationUpdate', (duration: number) => {
this.duration = duration;
});
// 错误监听
this.avPlayer.on('error', (err: BusinessError) => {
console.error(`[AVPlayer] 错误: code=${err.code}, message=${err.message}`);
});
// 播放完成监听
this.avPlayer.on('endOfStream', () => {
console.info('[AVPlayer] 播放完成');
});
} catch (err) {
const error = err as BusinessError;
console.error(`[AVPlayer] 创建失败: ${error.message}`);
}
}
/**
* 设置音频源并开始播放
*/
async playAudio(filePath: string): Promise<void> {
if (!this.avPlayer) {
await this.initAVPlayer();
}
try {
// 方式一:播放本地文件
const context = getContext(this) as common.Context;
const fd = await context.resourceManager.getRawFd(filePath);
this.avPlayer!.fdSrc = {
fd: fd.fd,
offset: fd.offset,
length: fd.length
};
// 设置源后状态从 idle → initialized,然后触发 stateChange 回调自动 prepare
} catch (err) {
const error = err as BusinessError;
console.error(`[AVPlayer] 播放失败: ${error.message}`);
}
}
/**
* 播放 / 暂停切换
*/
togglePlay(): void {
if (!this.avPlayer) return;
if (this.playerState === 'prepared' || this.playerState === 'paused' || this.playerState === 'completed') {
this.avPlayer.play();
} else if (this.playerState === 'playing') {
this.avPlayer.pause();
}
}
/**
* 停止播放
*/
stopPlay(): void {
if (!this.avPlayer) return;
if (this.playerState === 'playing' || this.playerState === 'paused') {
this.avPlayer.stop();
}
}
/**
* 跳转到指定位置
*/
seekTo(timeMs: number): void {
if (!this.avPlayer) return;
if (this.playerState === 'prepared' || this.playerState === 'playing' || this.playerState === 'paused') {
this.avPlayer.seek(timeMs, media.SeekMode.SEEK_PREV_SYNCMODE);
}
}
/**
* 释放播放器资源
*/
async releasePlayer(): Promise<void> {
if (this.avPlayer) {
try {
await this.avPlayer.release();
this.avPlayer = null;
this.playerState = 'released';
console.info('[AVPlayer] 资源已释放');
} catch (err) {
const error = err as BusinessError;
console.error(`[AVPlayer] 释放失败: ${error.message}`);
}
}
}
/**
* 格式化时间显示
*/
formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
build() {
Column({ space: 20 }) {
// 标题
Text('🎵 音频播放器')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 状态显示
Text(`状态: ${this.playerState}`)
.fontSize(14)
.fontColor('#888')
// 进度条
Row({ space: 10 }) {
Text(this.formatTime(this.currentTime))
.fontSize(12)
.fontColor('#666')
.width(50)
Slider({
value: this.duration > 0 ? (this.currentTime / this.duration) * 100 : 0,
min: 0,
max: 100,
style: SliderStyle.OutSet
})
.width('60%')
.onChange((value: number) => {
this.seekTo(Math.floor(value / 100 * this.duration));
})
Text(this.formatTime(this.duration))
.fontSize(12)
.fontColor('#666')
.width(50)
}
.width('90%')
// 控制按钮
Row({ space: 20 }) {
Button('停止')
.onClick(() => this.stopPlay())
.width(80)
Button(this.isPlaying ? '暂停' : '播放')
.onClick(() => this.togglePlay())
.width(80)
.backgroundColor('#4CAF50')
Button('播放文件')
.onClick(() => this.playAudio('audio/sample.mp3'))
.width(80)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
}
3.2 SoundPool 短音效播放
游戏和交互场景中,我们需要快速、低延迟地播放短音效。SoundPool 就是为此而生的:
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 音效类型枚举
enum SoundType {
BUTTON_CLICK = 0, // 按钮点击音
SUCCESS = 1, // 成功提示音
ERROR = 2, // 错误提示音
NOTIFICATION = 3 // 通知音
}
@Entry
@Component
struct SoundPoolPlayer {
// SoundPool 实例
private soundPool: media.SoundPool | null = null;
// 音效ID映射表(音效类型 → soundID)
private soundIdMap: Map<number, number> = new Map();
// 流ID映射表(用于控制正在播放的音效)
private streamIdMap: Map<number, number> = new Map();
@State statusText: string = '未初始化';
aboutToAppear(): void {
this.initSoundPool();
}
aboutToDisappear(): void {
this.releaseSoundPool();
}
/**
* 初始化 SoundPool
*/
async initSoundPool(): Promise<void> {
try {
// 创建 SoundPool,参数:最大并发流数量,音频流类型
this.soundPool = await media.createSoundPool(5, media.AudioVolumeType.STREAM_MUSIC);
// 加载完成监听
this.soundPool.on('loadComplete', (soundId: number) => {
console.info(`[SoundPool] 音效加载完成, soundId=${soundId}`);
});
// 加载所有音效文件
await this.loadAllSounds();
this.statusText = '音效池就绪';
} catch (err) {
const error = err as BusinessError;
console.error(`[SoundPool] 初始化失败: ${error.message}`);
this.statusText = '初始化失败';
}
}
/**
* 加载所有音效文件到内存
*/
async loadAllSounds(): Promise<void> {
const context = getContext(this) as common.Context;
// 音效文件映射
const soundFiles: Map<number, string> = new Map([
[SoundType.BUTTON_CLICK, 'audio/click.wav'],
[SoundType.SUCCESS, 'audio/success.wav'],
[SoundType.ERROR, 'audio/error.wav'],
[SoundType.NOTIFICATION, 'audio/notify.wav']
]);
for (const [type, filePath] of soundFiles) {
try {
const fd = await context.resourceManager.getRawFd(filePath);
const soundId = await this.soundPool!.load(fd.fd, fd.offset, fd.length);
this.soundIdMap.set(type, soundId);
console.info(`[SoundPool] 加载成功: type=${type}, soundId=${soundId}`);
} catch (err) {
console.warn(`[SoundPool] 加载失败: ${filePath}`);
}
}
}
/**
* 播放指定类型的音效
* @param type 音效类型
* @param volume 音量 0.0 ~ 1.0
* @param loop 循环次数(0=不循环,-1=无限循环)
*/
playSound(type: SoundType, volume: number = 1.0, loop: number = 0): void {
if (!this.soundPool) return;
const soundId = this.soundIdMap.get(type);
if (soundId === undefined) {
console.warn(`[SoundPool] 音效未加载: type=${type}`);
return;
}
// 播放音效,返回 streamId 用于后续控制
const streamId = this.soundPool.play(soundId, {
leftVolume: volume, // 左声道音量
rightVolume: volume, // 右声道音量
loop: loop, // 循环次数
priority: 0, // 优先级(0=最低)
rate: 0 // 播放速率(0=正常)
});
this.streamIdMap.set(type, streamId);
console.info(`[SoundPool] 播放: type=${type}, streamId=${streamId}`);
}
/**
* 停止指定音效
*/
stopSound(type: SoundType): void {
if (!this.soundPool) return;
const streamId = this.streamIdMap.get(type);
if (streamId !== undefined) {
this.soundPool.stop(streamId);
this.streamIdMap.delete(type);
}
}
/**
* 设置音效音量
*/
setSoundVolume(type: SoundType, volume: number): void {
if (!this.soundPool) return;
const streamId = this.streamIdMap.get(type);
if (streamId !== undefined) {
this.soundPool.setVolume(streamId, {
leftVolume: volume,
rightVolume: volume
});
}
}
/**
* 释放 SoundPool 资源
*/
async releaseSoundPool(): Promise<void> {
if (this.soundPool) {
try {
// 先卸载所有音效
for (const [, soundId] of this.soundIdMap) {
this.soundPool.unload(soundId);
}
this.soundIdMap.clear();
this.streamIdMap.clear();
await this.soundPool.release();
this.soundPool = null;
console.info('[SoundPool] 资源已释放');
} catch (err) {
const error = err as BusinessError;
console.error(`[SoundPool] 释放失败: ${error.message}`);
}
}
}
build() {
Column({ space: 20 }) {
Text('🎮 音效池播放器')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(this.statusText)
.fontSize(14)
.fontColor('#888')
// 音效按钮组
Grid() {
GridItem() {
Column({ space: 8 }) {
Text('👆')
.fontSize(32)
Text('按钮点击')
.fontSize(12)
}
.padding(16)
.borderRadius(12)
.backgroundColor('#2196F3')
.onClick(() => this.playSound(SoundType.BUTTON_CLICK, 0.5))
}
GridItem() {
Column({ space: 8 }) {
Text('✅')
.fontSize(32)
Text('成功提示')
.fontSize(12)
}
.padding(16)
.borderRadius(12)
.backgroundColor('#4CAF50')
.onClick(() => this.playSound(SoundType.SUCCESS, 0.8))
}
GridItem() {
Column({ space: 8 }) {
Text('❌')
.fontSize(32)
Text('错误提示')
.fontSize(12)
}
.padding(16)
.borderRadius(12)
.backgroundColor('#F44336')
.onClick(() => this.playSound(SoundType.ERROR, 0.8))
}
GridItem() {
Column({ space: 8 }) {
Text('🔔')
.fontSize(32)
Text('通知音')
.fontSize(12)
}
.padding(16)
.borderRadius(12)
.backgroundColor('#FF9800')
.onClick(() => this.playSound(SoundType.NOTIFICATION, 0.6))
}
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('80%')
.height(250)
// 音量控制
Row({ space: 10 }) {
Text('音量:')
.fontSize(14)
Slider({
value: 80,
min: 0,
max: 100,
style: SliderStyle.OutSet
})
.width('60%')
.onChange((value: number) => {
// 实时调整正在播放的音效音量
this.setSoundVolume(SoundType.BUTTON_CLICK, value / 100);
})
}
.width('80%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
}
3.3 播放列表管理器
真实场景中,我们需要管理一个播放列表,支持顺序、随机、循环播放,以及歌曲的无缝切换:
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 播放模式枚举
enum PlayMode {
SEQUENCE = 0, // 顺序播放
LOOP = 1, // 列表循环
SINGLE = 2, // 单曲循环
SHUFFLE = 3 // 随机播放
}
// 歌曲数据模型
interface SongItem {
id: string;
title: string;
artist: string;
filePath: string; // 本地文件路径或资源路径
duration: number;
coverUrl?: string;
}
@Entry
@Component
struct PlaylistManager {
// AVPlayer 实例
private avPlayer: media.AVPlayer | null = null;
// 播放列表
@State playlist: SongItem[] = [];
// 当前播放索引
@State currentIndex: number = -1;
// 播放模式
@State playMode: PlayMode = PlayMode.LOOP;
// 播放状态
@State isPlaying: boolean = false;
@State currentTime: number = 0;
@State duration: number = 0;
// 播放器内部状态
private playerState: string = 'idle';
// 随机播放历史(避免重复)
private shuffleHistory: number[] = [];
aboutToAppear(): void {
this.initPlaylist();
this.initAVPlayer();
}
aboutToDisappear(): void {
this.releasePlayer();
}
/**
* 初始化模拟播放列表数据
*/
initPlaylist(): void {
this.playlist = [
{ id: '1', title: '春风十里', artist: '鹿先森乐队', filePath: 'audio/spring.mp3', duration: 245000 },
{ id: '2', title: '晴天', artist: '周杰伦', filePath: 'audio/sunny.mp3', duration: 269000 },
{ id: '3', title: '平凡之路', artist: '朴树', filePath: 'audio/ordinary.mp3', duration: 295000 },
{ id: '4', title: '稻香', artist: '周杰伦', filePath: 'audio/rice.mp3', duration: 223000 },
{ id: '5', title: '光年之外', artist: '邓紫棋', filePath: 'audio/lightyear.mp3', duration: 235000 },
];
}
/**
* 初始化 AVPlayer
*/
async initAVPlayer(): Promise<void> {
try {
this.avPlayer = await media.createAVPlayer();
// 状态变化监听
this.avPlayer.on('stateChange', async (state: string) => {
this.playerState = state;
console.info(`[Playlist] 播放器状态: ${state}`);
if (state === 'initialized') {
this.avPlayer?.prepare();
} else if (state === 'prepared') {
this.avPlayer?.play();
} else if (state === 'playing') {
this.isPlaying = true;
} else if (state === 'paused' || state === 'stopped') {
this.isPlaying = false;
} else if (state === 'completed') {
this.isPlaying = false;
this.onSongComplete();
} else if (state === 'error') {
this.isPlaying = false;
// error 后需要重新创建
this.avPlayer = null;
}
});
// 进度监听
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentTime = time;
});
// 时长监听
this.avPlayer.on('durationUpdate', (duration: number) => {
this.duration = duration;
});
// 错误监听
this.avPlayer.on('error', (err: BusinessError) => {
console.error(`[Playlist] 播放错误: ${err.message}`);
});
} catch (err) {
const error = err as BusinessError;
console.error(`[Playlist] 初始化失败: ${error.message}`);
}
}
/**
* 播放指定索引的歌曲
*/
async playSongAtIndex(index: number): Promise<void> {
if (index < 0 || index >= this.playlist.length) return;
this.currentIndex = index;
const song = this.playlist[index];
console.info(`[Playlist] 开始播放: ${song.title} - ${song.artist}`);
// 如果播放器不在 stopped/idle 状态,先停止
if (this.playerState === 'playing' || this.playerState === 'paused') {
this.avPlayer?.stop();
}
try {
const context = getContext(this) as common.Context;
const fd = await context.resourceManager.getRawFd(song.filePath);
this.avPlayer!.fdSrc = {
fd: fd.fd,
offset: fd.offset,
length: fd.length
};
// 设置源后自动触发 idle → initialized → prepared → playing
} catch (err) {
const error = err as BusinessError;
console.error(`[Playlist] 播放失败: ${error.message}`);
}
}
/**
* 歌曲播放完成后的处理
*/
onSongComplete(): void {
switch (this.playMode) {
case PlayMode.SINGLE:
// 单曲循环:重新播放当前歌曲
this.playSongAtIndex(this.currentIndex);
break;
case PlayMode.LOOP:
// 列表循环:播放下一首,到末尾回到第一首
this.playNext();
break;
case PlayMode.SEQUENCE:
// 顺序播放:播放下一首,到末尾停止
if (this.currentIndex < this.playlist.length - 1) {
this.playNext();
}
break;
case PlayMode.SHUFFLE:
// 随机播放:随机选一首(避免连续重复)
this.playShuffle();
break;
}
}
/**
* 播放下一首
*/
playNext(): void {
if (this.playlist.length === 0) return;
if (this.playMode === PlayMode.SHUFFLE) {
this.playShuffle();
return;
}
let nextIndex = this.currentIndex + 1;
if (nextIndex >= this.playlist.length) {
nextIndex = 0; // 循环到第一首
}
this.playSongAtIndex(nextIndex);
}
/**
* 播放上一首
*/
playPrevious(): void {
if (this.playlist.length === 0) return;
// 如果已播放超过3秒,重新播放当前歌曲
if (this.currentTime > 3000) {
this.playSongAtIndex(this.currentIndex);
return;
}
let prevIndex = this.currentIndex - 1;
if (prevIndex < 0) {
prevIndex = this.playlist.length - 1; // 循环到最后一首
}
this.playSongAtIndex(prevIndex);
}
/**
* 随机播放
*/
playShuffle(): void {
if (this.playlist.length <= 1) {
this.playSongAtIndex(0);
return;
}
let randomIndex: number;
do {
randomIndex = Math.floor(Math.random() * this.playlist.length);
} while (randomIndex === this.currentIndex);
this.shuffleHistory.push(this.currentIndex);
// 只保留最近20条历史
if (this.shuffleHistory.length > 20) {
this.shuffleHistory.shift();
}
this.playSongAtIndex(randomIndex);
}
/**
* 切换播放模式
*/
togglePlayMode(): void {
this.playMode = (this.playMode + 1) % 4;
}
/**
* 获取播放模式图标
*/
getPlayModeIcon(): string {
const icons: Record<number, string> = {
[PlayMode.SEQUENCE]: '🔁',
[PlayMode.LOOP]: '🔄',
[PlayMode.SINGLE]: '🔂',
[PlayMode.SHUFFLE]: '🔀'
};
return icons[this.playMode] || '🔁';
}
/**
* 获取播放模式名称
*/
getPlayModeName(): string {
const names: Record<number, string> = {
[PlayMode.SEQUENCE]: '顺序播放',
[PlayMode.LOOP]: '列表循环',
[PlayMode.SINGLE]: '单曲循环',
[PlayMode.SHUFFLE]: '随机播放'
};
return names[this.playMode] || '未知';
}
/**
* 格式化时间
*/
formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
/**
* 释放播放器
*/
async releasePlayer(): Promise<void> {
if (this.avPlayer) {
try {
await this.avPlayer.release();
this.avPlayer = null;
} catch (err) {
const error = err as BusinessError;
console.error(`[Playlist] 释放失败: ${error.message}`);
}
}
}
build() {
Column({ space: 0 }) {
// 顶部 - 当前播放信息
Column({ space: 12 }) {
Text('🎵 播放列表管理器')
.fontSize(22)
.fontWeight(FontWeight.Bold)
if (this.currentIndex >= 0) {
Text(this.playlist[this.currentIndex]?.title || '未选择')
.fontSize(20)
.fontWeight(FontWeight.Medium)
Text(this.playlist[this.currentIndex]?.artist || '')
.fontSize(14)
.fontColor('#888')
}
}
.width('100%')
.padding(20)
// 进度条
Row({ space: 10 }) {
Text(this.formatTime(this.currentTime))
.fontSize(12)
.fontColor('#aaa')
.width(45)
Progress({
value: this.duration > 0 ? (this.currentTime / this.duration) * 100 : 0,
total: 100,
type: ProgressType.Linear
})
.width('60%')
.color('#4CAF50')
Text(this.formatTime(this.duration))
.fontSize(12)
.fontColor('#aaa')
.width(45)
}
.width('100%')
.padding({ left: 20, right: 20 })
// 播放控制栏
Row({ space: 24 }) {
// 播放模式
Text(this.getPlayModeIcon())
.fontSize(24)
.onClick(() => this.togglePlayMode())
// 上一首
Text('⏮')
.fontSize(28)
.onClick(() => this.playPrevious())
// 播放/暂停
Text(this.isPlaying ? '⏸' : '▶️')
.fontSize(40)
.onClick(() => {
if (this.isPlaying) {
this.avPlayer?.pause();
} else if (this.currentIndex >= 0) {
this.avPlayer?.play();
} else if (this.playlist.length > 0) {
this.playSongAtIndex(0);
}
})
// 下一首
Text('⏭')
.fontSize(28)
.onClick(() => this.playNext())
// 播放模式文字
Text(this.getPlayModeName())
.fontSize(10)
.fontColor('#888')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 16, bottom: 16 })
// 播放列表
List({ space: 2 }) {
ForEach(this.playlist, (song: SongItem, index: number) => {
ListItem() {
Row({ space: 12 }) {
// 序号或播放指示
Text(index === this.currentIndex ? '🎵' : `${index + 1}`)
.fontSize(16)
.width(30)
.textAlign(TextAlign.Center)
// 歌曲信息
Column({ space: 4 }) {
Text(song.title)
.fontSize(16)
.fontWeight(index === this.currentIndex ? FontWeight.Bold : FontWeight.Normal)
.fontColor(index === this.currentIndex ? '#4CAF50' : '#fff')
Text(song.artist)
.fontSize(12)
.fontColor('#888')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 时长
Text(this.formatTime(song.duration))
.fontSize(12)
.fontColor('#888')
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.borderRadius(8)
.backgroundColor(index === this.currentIndex ? 'rgba(76,175,80,0.1)' : 'transparent')
.onClick(() => this.playSongAtIndex(index))
}
}, (song: SongItem) => song.id)
}
.width('100%')
.layoutWeight(1)
.padding({ left: 12, right: 12 })
}
.width('100%')
.height('100%')
.backgroundColor('#1a1a2e')
}
}
四、踩坑与注意事项
4.1 状态机调用顺序错误
问题:在 idle 状态直接调用 play(),导致崩溃。
原因:AVPlayer 的状态机是严格有序的,跳过中间状态会导致内部异常。
解决方案:始终在 stateChange 回调中驱动下一步操作,不要在外部手动串联调用。
// ❌ 错误写法:直接串联调用
avPlayer.fdSrc = source; // idle → initialized
avPlayer.prepare(); // 此时可能还在 idle,还没到 initialized!
avPlayer.play(); // 必定失败
// ✅ 正确写法:在状态回调中驱动
avPlayer.on('stateChange', (state) => {
if (state === 'initialized') {
avPlayer.prepare(); // initialized → prepared
} else if (state === 'prepared') {
avPlayer.play(); // prepared → playing
}
});
avPlayer.fdSrc = source; // 触发状态流转
4.2 重复创建 AVPlayer 导致内存泄漏
问题:每次播放新歌曲都 createAVPlayer(),旧的实例没释放,内存持续增长。
解决方案:复用同一个 AVPlayer 实例,切歌时先 stop() 再设置新的源。
// ❌ 错误写法:每次都创建新实例
async playNewSong(path: string) {
const player = await media.createAVPlayer(); // 内存泄漏!
player.fdSrc = { ... };
}
// ✅ 正确写法:复用实例
async playNewSong(path: string) {
if (this.avPlayer) {
// 先停止当前播放
if (this.playerState === 'playing' || this.playerState === 'paused') {
this.avPlayer.stop();
}
// stopped 状态下可以直接设置新源
this.avPlayer.fdSrc = { ... };
}
}
4.3 SoundPool 加载延迟
问题:调用 play() 时音效还没加载完,导致无声。
解决方案:在 loadComplete 回调中标记加载状态,播放前检查。
// 音效加载状态追踪
private loadedSounds: Set<number> = new Set();
this.soundPool.on('loadComplete', (soundId: number) => {
this.loadedSounds.add(soundId);
console.info(`音效 ${soundId} 加载完成`);
});
// 播放前检查
playSound(soundId: number): void {
if (!this.loadedSounds.has(soundId)) {
console.warn('音效尚未加载完成,请稍后再试');
return;
}
this.soundPool!.play(soundId, { leftVolume: 1.0, rightVolume: 1.0, loop: 0, priority: 0, rate: 0 });
}
4.4 后台播放中断
问题:应用切到后台后音频停止播放。
原因:HarmonyOS 默认不支持后台长音频播放,需要申请长任务(Background Task)。
解决方案:使用 @ohos.resourceschedule.backgroundTaskManager 申请长任务,并在 module.json5 中声明权限。
4.5 fdSrc 资源未关闭
问题:使用 getRawFd() 获取文件描述符后,未正确关闭,导致资源泄漏。
解决方案:虽然 AVPlayer 内部会管理 fd 的生命周期,但在异常情况下建议手动关闭。
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| AVPlayer 创建 | media.createAVPlayer() |
保持一致,新增 createAVPlayer(options) 支持配置项 |
| SoundPool | media.createSoundPool() |
新增 createSoundPool(options) 支持更多配置 |
| 音频流类型 | AudioVolumeType 枚举 |
新增 STREAM_VOICE_ASSISTANT 类型 |
| 后台播放 | 需手动申请长任务 | 新增 AVPlayer.setAudioInterruptMode() 简化焦点管理 |
5.2 迁移指南
// HarmonyOS 5 写法
const avPlayer = await media.createAVPlayer();
// HarmonyOS 6 写法(可选配置)
const avPlayer = await media.createAVPlayer({
audioInterruptMode: media.InterruptMode.SHARE_MODE, // 共享模式
preferredAudioRendererInfo: {
contentType: media.ContentType.CONTENT_TYPE_MUSIC,
streamUsage: media.StreamUsage.STREAM_USAGE_MEDIA
}
});
5.3 新特性
- 低延迟播放模式:HarmonyOS 6 新增
lowLatency配置,适用于实时音频场景 - 空间音频支持:AVPlayer 新增空间音频渲染能力
- 自适应码率:支持根据网络状况自动切换音频质量
六、总结
mindmap
root((音频播放))
AVPlayer
状态机驱动
idle → initialized → prepared → playing
playing ↔ paused
playing/stopped → stopped
任何状态 → released
核心API
createAVPlayer
setSource / fdSrc
prepare / play / pause / stop
seek / setSpeed / setVolume
监听回调
stateChange
timeUpdate
durationUpdate
error / endOfStream
SoundPool
短音效专用
低延迟
并发播放
内存预加载
核心API
createSoundPool
load / unload
play / stop
setVolume
播放列表管理
播放模式
顺序播放
列表循环
单曲循环
随机播放
切歌策略
stop → setSource → prepare → play
预加载下一首
资源管理
复用AVPlayer实例
及时释放资源
核心要点回顾:
- 状态机是灵魂:AVPlayer 的所有操作都必须遵循状态机流转,在
stateChange回调中驱动是最安全的方式 - AVPlayer vs SoundPool:长音频用 AVPlayer(流式解码),短音效用 SoundPool(内存预加载),各司其职
- 播放列表管理:核心是复用 AVPlayer 实例 + 合理的切歌策略 + 完善的播放模式切换
- 资源管理:创建和释放要成对出现,避免内存泄漏;fdSrc 资源要妥善管理
- HarmonyOS 6:新增低延迟模式、空间音频、简化焦点管理等特性,迁移时注意 API 兼容性
- 点赞
- 收藏
- 关注作者
评论(0)