鸿蒙音频播放器封装:从底层原理到工程实践

6 阅读12分钟

目录

  1. 鸿蒙音频框架架构剖析

  2. AVPlayer 核心状态机原理

  3. 音频焦点与会话管理

  4. 播放器封装架构设计

  5. 完整代码实现

  6. 播放列表与队列管理

  7. 后台播放与通知栏控件

  8. 异常处理与容错机制

  9. 性能优化要点

  10. UI 层集成示例


一、鸿蒙音频框架架构剖析

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:后台播放被系统杀掉怎么办?

必须同时满足三点:

  1. 声明 backgroundModes: ["audioPlayback"]

  2. 调用 backgroundTaskManager.startBackgroundRunning()

  3. 持有 ohos.permission.KEEP_BACKGROUND_RUNNING 权限

缺少任意一点都会导致后台被回收。