目录
一、鸿蒙音频框架架构剖析
1.1 整体架构层次
鸿蒙音频框架采用分层解耦设计,从上到下共五层:
┌──────────────────────────────────────────────┐
│ 应用层 (ArkTS / C++) │
│ AVPlayer AudioRenderer AudioCapturer │
├──────────────────────────────────────────────┤
│ 音频框架层 (Audio Framework) │
│ AudioManager AudioSession AudioRouter │
├──────────────────────────────────────────────┤
│ 多媒体引擎层 (Media Engine) │
│ 解码器(FFmpeg/硬解) 混音器 重采样器 │
├──────────────────────────────────────────────┤
│ 音频 HAL 层 (Audio HAL) │
│ AudioRenderer HAL AudioCapturer HAL │
├──────────────────────────────────────────────┤
│ 内核驱动层 (Kernel / Driver) │
│ ALSA / AudioFlinger / HDF 音频驱动 │
└──────────────────────────────────────────────┘
1.2 核心 API 选型对比
| API | 适用场景 | 底层机制 | 控制粒度 |
|-----|---------|---------|---------|
| AVPlayer | 媒体文件/网络流播放 | 封装解码+渲染全链路 | 中等(推荐) |
| AudioRenderer | PCM 原始数据输出 | 直接写入 HAL 渲染管道 | 极高(需自行解码) |
| SoundPool | 短音效、低延迟触发 | 预加载到内存池 | 低(适合音效) |
| TonePlayer | DTMF/系统提示音 | 软件合成 | 极低 |
结论:封装通用音频播放器首选 AVPlayer,它在内部通过 IPC 调用 media_service 进程完成解码,天然隔离了崩溃风险。
1.3 AVPlayer 进程间通信机制
App Process media_service Process
───────────────── ──────────────────────────────
AVPlayer (ArkTS) AVPlayerService
│ │
│── IPC (Binder/HDF) ─────────▶│
│ ┌───▼──────────────────┐
│ │ Demuxer (解封装) │
│ │ Decoder (硬/软解码) │
│ │ AudioRenderer (渲染) │
│ └───────────────────────┘
│◀── 状态回调 (onStateChange) ──│
│◀── 错误回调 (onError) ─────│
跨进程调用意味着:
-
AVPlayer 的状态变更是异步的,需通过回调监听,不可同步阻塞等待
-
进程崩溃时 App 侧会收到
error回调,需重建实例
二、AVPlayer 核心状态机原理
2.1 完整状态转移图
┌─────────┐
new() │ IDLE │
└────┬────┘
│ url/fdSrc 赋值
┌────▼────┐
│ INIT │
└────┬────┘
│ prepare()
┌────▼────────┐
│ PREPARED │◀────────────────────┐
└────┬────────┘ │
│ play() │
┌────▼────┐ pause() ┌──────────┴──┐
│ PLAYING │────────────▶│ PAUSED │
└────┬────┘◀────────────└─────────────┘
│ play()
│ (播放完毕)
┌────▼──────┐
│ COMPLETED │
└────┬──────┘
│ reset() 或 seek(0)+play()
┌────▼────┐
│ STOPPED │
└────┬────┘
│ release()
┌────▼────────┐
│ RELEASED │ (终态,不可恢复)
└─────────────┘
任意状态 ──── error ────▶ ERROR (需 reset() 恢复到 IDLE)
2.2 状态转移的关键约束
// 错误示范:在 IDLE 状态调用 play() → 触发 error
avPlayer.play(); // ❌ 未 prepare 直接播放
// 正确姿势:严格遵循状态机
avPlayer.url = 'http://...'; // IDLE → INIT
avPlayer.prepare(); // INIT → PREPARED (异步)
// 在 onStateChange('prepared') 回调中再调用
avPlayer.play(); // PREPARED → PLAYING
2.3 底层缓冲机制
AVPlayer 内部维护三级缓冲:
网络/文件 ──▶ [IO Buffer] ──▶ [Demux Buffer] ──▶ [Decode Buffer] ──▶ 渲染
~128KB ~1MB ~4帧PCM
bufferingUpdate 回调返回两个关键值:
-
BufferingInfoType.CACHED_DURATION:已缓冲的可播放时长(ms) -
BufferingInfoType.BUFFERING_PERCENT:缓冲百分比
三、音频焦点与会话管理
3.1 音频焦点原理
鸿蒙音频焦点(Audio Focus)通过 AudioSession 仲裁多个 App 的音频使用权:
App A (音乐) ──▶ 申请 STREAM_MUSIC 焦点
│
┌───────▼────────┐
│ AudioSession │ 焦点仲裁器
│ (系统级唯一) │
└───────┬────────┘
│ 通知 App B 失去焦点
App B (播客) ◀──────────┘ → onAudioFocusChange(LOSS)
3.2 焦点类型与响应策略
| 焦点事件 | 含义 | 推荐响应 |
|---------|------|---------|
| GAIN | 获得焦点 | 恢复播放 |
| LOSS | 永久失去焦点(另一个 App 独占)| 暂停并释放 |
| LOSS_TRANSIENT | 短暂失去(来电等)| 暂停,保留状态 |
| LOSS_TRANSIENT_CAN_DUCK | 可降音(导航播报)| 音量降至 20% |
3.3 音频流类型选择
// 不同内容使用不同 StreamUsage,影响路由、焦点优先级、音量曲线
import audio from '@ohos.multimedia.audio';
// 音乐播放
audio.StreamUsage.STREAM_USAGE_MUSIC
// 播客/有声书
audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION
// 游戏音效(不受焦点打断)
audio.StreamUsage.STREAM_USAGE_GAME
// 告警/提示(强制出声)
audio.StreamUsage.STREAM_USAGE_ALARM
四、播放器封装架构设计
4.1 整体设计思路
采用 状态机 + 事件总线 + 单例 模式:
┌─────────────────────────────────────────────────┐
│ AudioPlayerManager │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ PlayerCore │ │ PlaylistManager │ │
│ │ (AVPlayer) │ │ (队列/循环/随机) │ │
│ └──────┬───────┘ └────────────┬───────────┘ │
│ │ │ │
│ ┌──────▼─────────────────────────▼───────────┐ │
│ │ PlayerStateMachine │ │
│ │ IDLE/PREPARED/PLAYING/PAUSED/COMPLETED │ │
│ └──────────────────────┬──────────────────── ┘ │
│ │ │
│ ┌───────────────────────▼──────────────────┐ │
│ │ EventEmitter (事件总线) │ │
│ │ onStateChange / onProgress / onError │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
│ 注册监听 │ 注册监听
┌────────▼──────┐ ┌────────▼──────────┐
│ UI 组件 │ │ 通知栏控制器 │
│ (ArkUI Page) │ │ (Background Task) │
└───────────────┘ └───────────────────┘
4.2 接口设计
interface IAudioPlayer {
// 播放控制
play(source: AudioSource): Promise<void>;
pause(): void;
resume(): void;
stop(): void;
seek(position: number): Promise<void>;
// 状态查询
getState(): PlayerState;
getCurrentTime(): number;
getDuration(): number;
// 属性设置
setVolume(volume: number): void;
setSpeed(speed: PlaybackSpeed): void;
setLoop(loop: boolean): void;
// 事件监听
on(event: PlayerEvent, callback: Function): void;
off(event: PlayerEvent, callback: Function): void;
// 资源管理
destroy(): Promise<void>;
}
// 音频源支持多种输入
type AudioSource =
| { type: 'url'; url: string }
| { type: 'fd'; fd: number; offset?: number; length?: number }
| { type: 'asset'; path: string }; // rawfile 资源
type PlayerState =
| 'idle' | 'loading' | 'prepared'
| 'playing' | 'paused' | 'stopped'
| 'completed' | 'error';
type PlayerEvent =
| 'stateChange' | 'progress' | 'durationChange'
| 'bufferingUpdate' | 'error' | 'complete';
enum PlaybackSpeed {
SPEED_0_75X = 0.75,
SPEED_1_00X = 1.0,
SPEED_1_25X = 1.25,
SPEED_1_50X = 1.5,
SPEED_2_00X = 2.0,
}
五、完整代码实现
5.1 核心播放器类
// AudioPlayerCore.ts
import media from '@ohos.multimedia.media';
import audio from '@ohos.multimedia.audio';
import { BusinessError } from '@ohos.base';
export type PlayerState =
| 'idle' | 'loading' | 'prepared'
| 'playing' | 'paused' | 'stopped'
| 'completed' | 'error';
export type PlayerEvent =
| 'stateChange' | 'progress' | 'durationChange'
| 'bufferingUpdate' | 'error' | 'complete' | 'seekDone';
export interface AudioSource {
type: 'url' | 'fd' | 'asset';
url?: string;
fd?: number;
offset?: number;
length?: number;
path?: string;
}
type EventCallback = (...args: unknown[]) => void;
export class AudioPlayerCore {
private avPlayer: media.AVPlayer | null = null;
private _state: PlayerState = 'idle';
private _duration: number = 0;
private _currentTime: number = 0;
private progressTimer: number = -1;
private eventListeners: Map<PlayerEvent, Set<EventCallback>> = new Map();
// ────────────────────────────────────────────
// 初始化
// ────────────────────────────────────────────
/**
* 创建 AVPlayer 实例并注册所有底层回调
* 注意:必须在 UI 线程或 Worker 线程中调用,不可在 TaskPool 中使用
*/
async init(): Promise<void> {
if (this.avPlayer) {
await this.destroyPlayer();
}
this.avPlayer = await media.createAVPlayer();
this.registerPlayerCallbacks();
}
/**
* 注册 AVPlayer 的所有底层回调
* 这是状态机驱动的核心,任何操作结果都通过回调异步通知
*/
private registerPlayerCallbacks(): void {
if (!this.avPlayer) return;
// 状态变更回调 - AVPlayer 最核心的回调
this.avPlayer.on('stateChange', (state: media.AVPlayerState, reason: media.StateChangeReason) => {
console.info(`[AudioPlayer] stateChange: ${state}, reason: ${reason}`);
this.handleStateChange(state, reason);
});
// 错误回调 - 任何错误都在此处理
this.avPlayer.on('error', (error: BusinessError) => {
console.error(`[AudioPlayer] error: code=${error.code}, msg=${error.message}`);
this.setState('error');
this.emit('error', error);
// 错误后 AVPlayer 进入 ERROR 状态,需调用 reset() 才能复用
this.avPlayer?.reset();
});
// 时长更新 - prepare 完成后触发
this.avPlayer.on('durationUpdate', (duration: number) => {
this._duration = duration;
this.emit('durationChange', duration);
});
// 缓冲状态回调
this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
this.emit('bufferingUpdate', { infoType, value });
});
// seek 完成回调
this.avPlayer.on('seekDone', (seekDoneTime: number) => {
this._currentTime = seekDoneTime;
this.emit('seekDone', seekDoneTime);
});
// 视频尺寸(音频场景忽略,但注册防止告警)
this.avPlayer.on('videoSizeChange', () => {});
}
/**
* 状态机处理 - 将 AVPlayer 底层状态映射到业务状态
*/
private handleStateChange(state: media.AVPlayerState, reason: media.StateChangeReason): void {
switch (state) {
case 'idle':
this.setState('idle');
this.stopProgressTimer();
break;
case 'initialized':
// 赋值 url/fd 后进入此状态,立即触发 prepare
this.setState('loading');
this.avPlayer?.prepare();
break;
case 'prepared':
this.setState('prepared');
this.emit('durationChange', this.avPlayer?.duration ?? 0);
break;
case 'playing':
this.setState('playing');
this.startProgressTimer();
break;
case 'paused':
this.setState('paused');
this.stopProgressTimer();
break;
case 'stopped':
this.setState('stopped');
this.stopProgressTimer();
break;
case 'completed':
this.setState('completed');
this.stopProgressTimer();
this._currentTime = this._duration;
this.emit('complete');
break;
case 'released':
this.setState('idle');
this.avPlayer = null;
break;
case 'error':
// error 回调会先触发,此处仅做状态同步
this.setState('error');
break;
}
}
// ────────────────────────────────────────────
// 播放控制
// ────────────────────────────────────────────
/**
* 加载并播放音频源
* 支持 URL(网络/本地)、文件描述符、rawfile 资源
*/
async load(source: AudioSource): Promise<void> {
if (!this.avPlayer) {
await this.init();
}
// 非 idle 状态需先 reset
if (this._state !== 'idle') {
await this.resetPlayer();
}
return new Promise((resolve, reject) => {
const player = this.avPlayer!;
// 监听 prepared 状态,resolve Promise
const onPrepared = (state: media.AVPlayerState) => {
if (state === 'prepared') {
player.off('stateChange', onPrepared);
resolve();
} else if (state === 'error') {
player.off('stateChange', onPrepared);
reject(new Error('AVPlayer prepare failed'));
}
};
player.on('stateChange', onPrepared);
// 根据源类型赋值
try {
if (source.type === 'url' && source.url) {
player.url = source.url;
} else if (source.type === 'fd' && source.fd !== undefined) {
player.fdSrc = {
fd: source.fd,
offset: source.offset ?? 0,
size: source.length ?? -1,
};
} else {
reject(new Error('Invalid audio source'));
}
// 赋值后 AVPlayer 自动进入 initialized 状态,触发 stateChange 回调
// 回调中会自动调用 prepare()
} catch (e) {
reject(e);
}
});
}
play(): void {
if (!this.avPlayer) return;
const validStates: PlayerState[] = ['prepared', 'paused', 'completed'];
if (!validStates.includes(this._state)) {
console.warn(`[AudioPlayer] play() called in invalid state: ${this._state}`);
return;
}
if (this._state === 'completed') {
// completed 状态需先 seek 到 0
this.avPlayer.seek(0);
// seek 完成后在 seekDone 回调中 play(通过监听一次性事件)
this.once('seekDone', () => this.avPlayer?.play());
} else {
this.avPlayer.play();
}
}
pause(): void {
if (this._state !== 'playing') return;
this.avPlayer?.pause();
}
resume(): void {
if (this._state !== 'paused') return;
this.avPlayer?.play();
}
stop(): void {
if (this._state === 'idle' || this._state === 'stopped') return;
this.avPlayer?.stop();
}
/**
* seek 到指定位置(毫秒)
* mode 参数控制 seek 精度:
* SEEK_PREV_SYNC - 关键帧对齐(快,但不精确)
* SEEK_CLOSEST - 精确 seek(慢,音频推荐)
*/
async seek(positionMs: number): Promise<void> {
if (!this.avPlayer) return;
if (this._state !== 'playing' && this._state !== 'paused') return;
return new Promise((resolve) => {
this.once('seekDone', () => resolve());
this.avPlayer!.seek(positionMs, media.SeekMode.SEEK_CLOSEST);
});
}
setVolume(volume: number): void {
// volume 范围 0.0 ~ 1.0
const clampedVolume = Math.max(0, Math.min(1, volume));
this.avPlayer?.setVolume(clampedVolume);
}
setSpeed(speed: number): void {
// 将倍速枚举映射到 AVPlayer 的 SpeedType
const speedMap: Record<number, media.PlaybackSpeed> = {
0.75: media.PlaybackSpeed.SPEED_FORWARD_0_75_X,
1.0: media.PlaybackSpeed.SPEED_FORWARD_1_00_X,
1.25: media.PlaybackSpeed.SPEED_FORWARD_1_25_X,
1.5: media.PlaybackSpeed.SPEED_FORWARD_1_50_X,
2.0: media.PlaybackSpeed.SPEED_FORWARD_2_00_X,
};
const speedType = speedMap[speed] ?? media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
this.avPlayer?.setSpeed(speedType);
}
setLoop(loop: boolean): void {
if (this.avPlayer) {
this.avPlayer.loop = loop;
}
}
// ────────────────────────────────────────────
// 进度定时器
// ────────────────────────────────────────────
private startProgressTimer(): void {
this.stopProgressTimer();
// 每 500ms 轮询一次当前时间(AVPlayer 无实时进度推送)
this.progressTimer = setInterval(() => {
if (this.avPlayer && this._state === 'playing') {
this._currentTime = this.avPlayer.currentTime;
this.emit('progress', this._currentTime, this._duration);
}
}, 500);
}
private stopProgressTimer(): void {
if (this.progressTimer !== -1) {
clearInterval(this.progressTimer);
this.progressTimer = -1;
}
}
// ────────────────────────────────────────────
// 状态管理
// ────────────────────────────────────────────
private setState(state: PlayerState): void {
if (this._state === state) return;
const prev = this._state;
this._state = state;
this.emit('stateChange', state, prev);
}
get state(): PlayerState {
return this._state;
}
get currentTime(): number {
return this._currentTime;
}
get duration(): number {
return this._duration;
}
// ────────────────────────────────────────────
// 事件系统(简易 EventEmitter)
// ────────────────────────────────────────────
on(event: PlayerEvent, callback: EventCallback): this {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set());
}
this.eventListeners.get(event)!.add(callback);
return this;
}
off(event: PlayerEvent, callback: EventCallback): this {
this.eventListeners.get(event)?.delete(callback);
return this;
}
once(event: PlayerEvent, callback: EventCallback): this {
const wrapper: EventCallback = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
private emit(event: PlayerEvent, ...args: unknown[]): void {
this.eventListeners.get(event)?.forEach(cb => {
try {
cb(...args);
} catch (e) {
console.error(`[AudioPlayer] emit ${event} error:`, e);
}
});
}
// ────────────────────────────────────────────
// 资源释放
// ────────────────────────────────────────────
private async resetPlayer(): Promise<void> {
return new Promise((resolve) => {
if (!this.avPlayer) {
resolve();
return;
}
this.once('stateChange', (state: unknown) => {
if (state === 'idle') resolve();
});
this.avPlayer.reset();
});
}
private async destroyPlayer(): Promise<void> {
this.stopProgressTimer();
if (this.avPlayer) {
try {
await this.avPlayer.release();
} catch (e) {
console.warn('[AudioPlayer] release error:', e);
}
this.avPlayer = null;
}
}
async destroy(): Promise<void> {
this.stopProgressTimer();
this.eventListeners.clear();
await this.destroyPlayer();
this._state = 'idle';
}
}
5.2 音频焦点管理器
// AudioFocusManager.ts
import audio from '@ohos.multimedia.audio';
type FocusCallback = (focused: boolean, canDuck: boolean) => void;
export class AudioFocusManager {
private audioManager: audio.AudioManager;
private audioSession: audio.AudioSessionManager;
private focusCallback: FocusCallback | null = null;
private hasFocus: boolean = false;
constructor() {
this.audioManager = audio.getAudioManager();
this.audioSession = this.audioManager.getSessionManager();
}
/**
* 请求音频焦点
* 必须在 play() 前调用,否则可能与其他 App 音频冲突
*/
async requestFocus(callback: FocusCallback): Promise<boolean> {
this.focusCallback = callback;
try {
// 注册焦点变化监听
this.audioSession.on('audioSessionDeactivated', (deactivatedEvent) => {
console.info(`[FocusManager] session deactivated: ${JSON.stringify(deactivatedEvent)}`);
this.hasFocus = false;
// 会话被强制失活(如来电),通知播放器暂停
this.focusCallback?.(false, false);
});
// 激活 AudioSession
await this.audioSession.activateAudioSession({
// CONCURRENCY_MIX_WITH_OTHERS: 与其他音频混合(适合提示音)
// CONCURRENCY_DEFAULT: 独占焦点(适合音乐/播客)
concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_DEFAULT
});
this.hasFocus = true;
return true;
} catch (e) {
console.error('[FocusManager] requestFocus failed:', e);
return false;
}
}
/**
* 释放音频焦点
* 在 pause() 或 stop() 时调用,让出焦点给其他 App
*/
async releaseFocus(): Promise<void> {
if (!this.hasFocus) return;
try {
await this.audioSession.deactivateAudioSession();
this.audioSession.off('audioSessionDeactivated');
this.hasFocus = false;
} catch (e) {
console.error('[FocusManager] releaseFocus failed:', e);
}
}
isFocused(): boolean {
return this.hasFocus;
}
}
5.3 高层封装:AudioPlayerManager(单例)
// AudioPlayerManager.ts
import { AudioPlayerCore, AudioSource, PlayerState, PlayerEvent } from './AudioPlayerCore';
import { AudioFocusManager } from './AudioFocusManager';
import { PlaylistManager, AudioTrack } from './PlaylistManager';
/**
* 对外暴露的高层播放器单例
* 统一管理 Core、焦点、播放列表
*/
export class AudioPlayerManager {
private static instance: AudioPlayerManager;
private core: AudioPlayerCore;
private focusManager: AudioFocusManager;
private playlist: PlaylistManager;
private _volume: number = 1.0;
private _speed: number = 1.0;
private constructor() {
this.core = new AudioPlayerCore();
this.focusManager = new AudioFocusManager();
this.playlist = new PlaylistManager();
this.setupAutoNext();
}
static getInstance(): AudioPlayerManager {
if (!AudioPlayerManager.instance) {
AudioPlayerManager.instance = new AudioPlayerManager();
}
return AudioPlayerManager.instance;
}
// ────────────────────────────────────────────
// 播放控制(带焦点管理)
// ────────────────────────────────────────────
async playTrack(track: AudioTrack): Promise<void> {
console.info(`[Manager] playTrack: ${track.title}`);
// 1. 请求音频焦点
const focused = await this.focusManager.requestFocus((hasFocus, canDuck) => {
if (!hasFocus) {
// 焦点被抢占(来电/其他 App)
canDuck ? this.core.setVolume(0.2) : this.core.pause();
} else {
// 重新获得焦点
this.core.setVolume(this._volume);
this.core.resume();
}
});
if (!focused) {
console.warn('[Manager] failed to get audio focus, abort play');
return;
}
// 2. 构建音频源
const source: AudioSource = {
type: 'url',
url: track.url,
};
// 3. 加载并播放
try {
await this.core.load(source);
this.core.setVolume(this._volume);
this.core.setSpeed(this._speed);
this.core.play();
} catch (e) {
console.error('[Manager] playTrack failed:', e);
await this.focusManager.releaseFocus();
throw e;
}
}
async pause(): Promise<void> {
this.core.pause();
await this.focusManager.releaseFocus();
}
async resume(): Promise<void> {
const focused = await this.focusManager.requestFocus((hasFocus) => {
if (!hasFocus) this.core.pause();
});
if (focused) this.core.resume();
}
async stop(): Promise<void> {
this.core.stop();
await this.focusManager.releaseFocus();
}
async seek(positionMs: number): Promise<void> {
await this.core.seek(positionMs);
}
setVolume(volume: number): void {
this._volume = Math.max(0, Math.min(1, volume));
this.core.setVolume(this._volume);
}
setSpeed(speed: number): void {
this._speed = speed;
this.core.setSpeed(speed);
}
// ────────────────────────────────────────────
// 播放列表操作
// ────────────────────────────────────────────
setPlaylist(tracks: AudioTrack[], startIndex: number = 0): void {
this.playlist.setTracks(tracks, startIndex);
}
async playNext(): Promise<void> {
const next = this.playlist.next();
if (next) await this.playTrack(next);
}
async playPrev(): Promise<void> {
const prev = this.playlist.prev();
if (prev) await this.playTrack(prev);
}
setLoopMode(mode: 'none' | 'single' | 'list'): void {
this.playlist.setLoopMode(mode);
this.core.setLoop(mode === 'single');
}
setShuffle(enable: boolean): void {
this.playlist.setShuffle(enable);
}
// ────────────────────────────────────────────
// 自动播放下一首
// ────────────────────────────────────────────
private setupAutoNext(): void {
this.core.on('complete', async () => {
const loopMode = this.playlist.getLoopMode();
if (loopMode === 'single') {
// 单曲循环:seek 到 0 重播
await this.core.seek(0);
this.core.play();
} else {
// 列表循环/顺序播放:播放下一首
await this.playNext();
}
});
}
// ────────────────────────────────────────────
// 事件代理(透传给 UI 层)
// ────────────────────────────────────────────
on(event: PlayerEvent, callback: (...args: unknown[]) => void): this {
this.core.on(event, callback);
return this;
}
off(event: PlayerEvent, callback: (...args: unknown[]) => void): this {
this.core.off(event, callback);
return this;
}
// ────────────────────────────────────────────
// 状态查询
// ────────────────────────────────────────────
get state(): PlayerState {
return this.core.state;
}
get currentTime(): number {
return this.core.currentTime;
}
get duration(): number {
return this.core.duration;
}
get currentTrack(): AudioTrack | null {
return this.playlist.current();
}
// ────────────────────────────────────────────
// 销毁
// ────────────────────────────────────────────
async destroy(): Promise<void> {
await this.stop();
await this.core.destroy();
AudioPlayerManager.instance = null!;
}
}
六、播放列表与队列管理
// PlaylistManager.ts
export interface AudioTrack {
id: string;
title: string;
artist?: string;
album?: string;
url: string;
coverUrl?: string;
duration?: number;
}
type LoopMode = 'none' | 'single' | 'list';
export class PlaylistManager {
private tracks: AudioTrack[] = [];
private originalTracks: AudioTrack[] = [];
private currentIndex: number = -1;
private loopMode: LoopMode = 'list';
private shuffleEnabled: boolean = false;
setTracks(tracks: AudioTrack[], startIndex: number = 0): void {
this.originalTracks = [...tracks];
this.tracks = [...tracks];
this.currentIndex = startIndex;
}
current(): AudioTrack | null {
if (this.currentIndex < 0 || this.currentIndex >= this.tracks.length) {
return null;
}
return this.tracks[this.currentIndex];
}
next(): AudioTrack | null {
if (this.tracks.length === 0) return null;
if (this.shuffleEnabled) {
// 随机模式:随机跳到非当前的任意一首
let nextIndex: number;
do {
nextIndex = Math.floor(Math.random() * this.tracks.length);
} while (nextIndex === this.currentIndex && this.tracks.length > 1);
this.currentIndex = nextIndex;
return this.tracks[this.currentIndex];
}
const nextIndex = this.currentIndex + 1;
if (nextIndex >= this.tracks.length) {
if (this.loopMode === 'list') {
// 列表循环:回到第一首
this.currentIndex = 0;
return this.tracks[0];
}
// 顺序播放:已到末尾
return null;
}
this.currentIndex = nextIndex;
return this.tracks[this.currentIndex];
}
prev(): AudioTrack | null {
if (this.tracks.length === 0) return null;
const prevIndex = this.currentIndex - 1;
if (prevIndex < 0) {
if (this.loopMode === 'list') {
this.currentIndex = this.tracks.length - 1;
return this.tracks[this.currentIndex];
}
return null;
}
this.currentIndex = prevIndex;
return this.tracks[this.currentIndex];
}
setShuffle(enable: boolean): void {
this.shuffleEnabled = enable;
if (enable) {
// Fisher-Yates shuffle,保留当前曲目在第 0 位
const current = this.tracks[this.currentIndex];
const rest = this.tracks.filter((_, i) => i !== this.currentIndex);
for (let i = rest.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[rest[i], rest[j]] = [rest[j], rest[i]];
}
this.tracks = [current, ...rest];
this.currentIndex = 0;
} else {
// 恢复原始顺序
const currentId = this.current()?.id;
this.tracks = [...this.originalTracks];
this.currentIndex = this.tracks.findIndex(t => t.id === currentId);
}
}
setLoopMode(mode: LoopMode): void {
this.loopMode = mode;
}
getLoopMode(): LoopMode {
return this.loopMode;
}
getAll(): AudioTrack[] {
return this.tracks;
}
get length(): number {
return this.tracks.length;
}
}
七、后台播放与通知栏控件
7.1 配置后台任务
在 module.json5 中声明后台任务权限:
// module.json5
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" }
],
"abilities": [{
"backgroundModes": ["audioPlayback"]
}]
}
}
7.2 后台任务管理器
// BackgroundTaskManager.ts
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import wantAgent from '@ohos.app.ability.wantAgent';
import notificationManager from '@ohos.notificationManager';
export class AudioBackgroundTask {
private continuousTaskId: number = -1;
/**
* 启动持续后台任务(音频播放专属)
* 必须在实际播放开始时调用,否则系统会在 App 切后台后强制暂停
*/
async startContinuousTask(context: Context): Promise<void> {
try {
// 构建点击通知跳转的 WantAgent
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [{
bundleName: context.applicationInfo.name,
abilityName: 'MainAbility',
}],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
};
const agent = await wantAgent.getWantAgent(wantAgentInfo);
await backgroundTaskManager.startBackgroundRunning(
context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
agent
);
console.info('[BackgroundTask] started');
} catch (e) {
console.error('[BackgroundTask] start failed:', e);
}
}
/**
* 停止后台任务(暂停或停止时调用,节省系统资源)
*/
async stopContinuousTask(context: Context): Promise<void> {
try {
await backgroundTaskManager.stopBackgroundRunning(context);
console.info('[BackgroundTask] stopped');
} catch (e) {
console.error('[BackgroundTask] stop failed:', e);
}
}
/**
* 发布/更新通知栏播放控件
*/
async updateNotification(
track: { title: string; artist?: string },
isPlaying: boolean
): Promise<void> {
const request: notificationManager.NotificationRequest = {
id: 1001,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: track.title,
text: track.artist ?? '未知艺人',
additionalText: isPlaying ? '正在播放' : '已暂停',
},
},
actionButtons: [
{
title: '上一首',
wantAgent: await this.buildWantAgent('PREV'),
},
{
title: isPlaying ? '暂停' : '播放',
wantAgent: await this.buildWantAgent(isPlaying ? 'PAUSE' : 'PLAY'),
},
{
title: '下一首',
wantAgent: await this.buildWantAgent('NEXT'),
},
],
};
await notificationManager.publish(request);
}
private async buildWantAgent(action: string): Promise<object> {
return wantAgent.getWantAgent({
wants: [{
action: `com.example.audio.${action}`,
}],
operationType: wantAgent.OperationType.SEND_COMMON_EVENT,
requestCode: action.charCodeAt(0),
});
}
}
八、异常处理与容错机制
8.1 错误码速查
| 错误码 | 含义 | 处理策略 |
|--------|------|---------|
| 5400101 | 无内存 | 释放其他资源后重试 |
| 5400102 | 操作不允许(状态错误)| 检查当前状态再调用 |
| 5400103 | IO 错误(文件不存在)| 提示用户,切换曲目 |
| 5400104 | 超时 | 重试 1 次,失败则跳过 |
| 5400105 | 服务异常(media_service 崩溃)| 重建 AVPlayer 实例 |
| 5400106 | 不支持的格式 | 提示格式不支持 |
8.2 自动重试机制
// RetryHelper.ts
export class RetryHelper {
static async withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 2,
delayMs: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (e) {
lastError = e as Error;
console.warn(`[Retry] attempt ${attempt + 1} failed:`, e);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delayMs * (attempt + 1)));
}
}
}
throw lastError!;
}
}
// 使用示例
await RetryHelper.withRetry(
() => playerManager.playTrack(track),
2, // 最多重试 2 次
500 // 首次重试等待 500ms,第二次等待 1000ms
);
8.3 网络中断恢复
// NetworkRecoveryHandler.ts
import connection from '@ohos.net.connection';
export class NetworkRecoveryHandler {
private playerManager: AudioPlayerManager;
private lastPosition: number = 0;
private wasPlaying: boolean = false;
constructor(manager: AudioPlayerManager) {
this.playerManager = manager;
this.setupNetworkListener();
}
private setupNetworkListener(): void {
// 记录网络断开时的播放状态
connection.getDefaultNet().then(netHandle => {
netHandle.on('netUnavailable', () => {
this.wasPlaying = this.playerManager.state === 'playing';
this.lastPosition = this.playerManager.currentTime;
console.info(`[NetworkRecovery] network lost at ${this.lastPosition}ms`);
});
// 网络恢复后自动续播
netHandle.on('netAvailable', async () => {
if (this.wasPlaying && this.playerManager.currentTrack) {
console.info(`[NetworkRecovery] network restored, resuming at ${this.lastPosition}ms`);
await this.playerManager.playTrack(this.playerManager.currentTrack);
await this.playerManager.seek(this.lastPosition);
}
});
});
}
}
九、性能优化要点
9.1 预加载策略
// 当前曲目播放到 80% 时,预解析下一首 URL
core.on('progress', (current: number, duration: number) => {
if (duration > 0 && current / duration > 0.8) {
const nextTrack = playlist.next();
if (nextTrack && !nextTrack._preloaded) {
nextTrack._preloaded = true;
// DNS 预解析(仅网络流有效)
dns.getAddrByName(new URL(nextTrack.url).hostname);
}
}
});
9.2 内存管理注意事项
// ❌ 错误:在 Page 组件中直接持有 AVPlayer 引用,页面销毁时可能内存泄漏
@Component
struct PlayerPage {
private player = new AudioPlayerCore(); // 危险!
aboutToDisappear() {
// 如果忘记调用 destroy(),AVPlayer 不会被释放
}
}
// ✅ 正确:使用单例管理器,Page 仅注册/注销事件监听
@Component
struct PlayerPage {
private manager = AudioPlayerManager.getInstance();
private onProgress = (time: number, duration: number) => { /* update UI */ };
aboutToAppear() {
this.manager.on('progress', this.onProgress);
}
aboutToDisappear() {
// 只移除监听,不销毁播放器(后台继续播放)
this.manager.off('progress', this.onProgress);
}
}
9.3 Worker 线程处理音频元数据
// 耗时操作(解析 ID3 Tag、读取专辑封面)放入 Worker,不阻塞 UI 线程
import worker from '@ohos.worker';
const metadataWorker = new worker.ThreadWorker('entry/ets/workers/MetadataWorker.ts');
metadataWorker.postMessage({ type: 'parse', url: track.url });
metadataWorker.onmessage = (event) => {
const { title, artist, coverData } = event.data;
// 回到主线程更新 UI
};
十、UI 层集成示例
// PlayerPage.ets
import { AudioPlayerManager } from '../player/AudioPlayerManager';
import { AudioTrack } from '../player/PlaylistManager';
@Entry
@Component
struct PlayerPage {
@State isPlaying: boolean = false;
@State currentTime: number = 0;
@State duration: number = 0;
@State trackTitle: string = '未知曲目';
@State trackArtist: string = '未知艺人';
@State volume: number = 1.0;
private manager = AudioPlayerManager.getInstance();
// 使用函数属性保存回调引用,确保 off() 能正确移除
private onStateChange = (state: string) => {
this.isPlaying = state === 'playing';
};
private onProgress = (current: number, total: number) => {
this.currentTime = current;
this.duration = total;
};
aboutToAppear(): void {
this.manager.on('stateChange', this.onStateChange);
this.manager.on('progress', this.onProgress);
// 初始化播放列表
const tracks: AudioTrack[] = [
{ id: '1', title: '曲目一', artist: '歌手A', url: 'https://example.com/1.mp3' },
{ id: '2', title: '曲目二', artist: '歌手B', url: 'https://example.com/2.mp3' },
];
this.manager.setPlaylist(tracks, 0);
this.manager.setLoopMode('list');
}
aboutToDisappear(): void {
this.manager.off('stateChange', this.onStateChange);
this.manager.off('progress', this.onProgress);
}
// 格式化时间 mm:ss
private formatTime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
const s = (seconds % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
build() {
Column({ space: 24 }) {
// 曲目信息
Column({ space: 8 }) {
Text(this.trackTitle)
.fontSize(22)
.fontWeight(FontWeight.Bold)
Text(this.trackArtist)
.fontSize(16)
.fontColor('#888888')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
// 进度条
Column({ space: 4 }) {
Slider({
value: this.duration > 0 ? (this.currentTime / this.duration) * 100 : 0,
min: 0,
max: 100,
step: 0.1,
style: SliderStyle.OutSet,
})
.width('100%')
.onChange((value: number) => {
const targetMs = (value / 100) * this.duration;
this.manager.seek(targetMs);
})
Row() {
Text(this.formatTime(this.currentTime)).fontSize(12).fontColor('#888')
Blank()
Text(this.formatTime(this.duration)).fontSize(12).fontColor('#888')
}
.width('100%')
}
// 控制按钮
Row({ space: 40 }) {
// 上一首
Button() {
Image($r('app.media.ic_prev')).width(28).height(28)
}
.backgroundColor(Color.Transparent)
.onClick(() => this.manager.playPrev())
// 播放/暂停
Button() {
Image(this.isPlaying
? $r('app.media.ic_pause')
: $r('app.media.ic_play')
).width(36).height(36)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#1890FF')
.onClick(() => {
if (this.isPlaying) {
this.manager.pause();
} else {
const track = this.manager.currentTrack;
if (track) {
this.manager.resume();
}
}
})
// 下一首
Button() {
Image($r('app.media.ic_next')).width(28).height(28)
}
.backgroundColor(Color.Transparent)
.onClick(() => this.manager.playNext())
}
.justifyContent(FlexAlign.Center)
// 音量控制
Row({ space: 12 }) {
Image($r('app.media.ic_volume')).width(20).height(20)
Slider({
value: this.volume * 100,
min: 0,
max: 100,
})
.width(200)
.onChange((value: number) => {
this.volume = value / 100;
this.manager.setVolume(this.volume);
})
}
}
.width('100%')
.height('100%')
.padding(32)
.justifyContent(FlexAlign.Center)
}
}
附录:常见问题
Q1:为什么 AVPlayer 的操作必须等回调,不能直接返回?
AVPlayer 的实际执行在 media_service 子进程中,通过 IPC 通信,天然是异步的。强行同步等待会造成主线程阻塞,引发 ANR(Application Not Responding)。
Q2:切换曲目时出现短暂噪音怎么处理?
在调用 reset() 前先将音量渐降到 0,reset 完成并 prepare 后再恢复。这是业界通用的"淡出-切换-淡入"方案。
Q3:为什么有时候 seek 之后 currentTime 不准确?
SEEK_PREV_SYNC 会 seek 到最近的关键帧,音频流帧间隔可能达到 0.5s。使用 SEEK_CLOSEST 模式可获得精确位置,但性能略低。
Q4:后台播放被系统杀掉怎么办?
必须同时满足三点:
-
声明
backgroundModes: ["audioPlayback"] -
调用
backgroundTaskManager.startBackgroundRunning() -
持有
ohos.permission.KEEP_BACKGROUND_RUNNING权限
缺少任意一点都会导致后台被回收。
完