HarmonyOS开发中的音频播放:AVPlayer、SoundPool 与播放状态机全解析

举报
Jack20 发表于 2026/06/20 20:33:40 2026/06/20
【摘要】 HarmonyOS开发中的音频播放:AVPlayer、SoundPool 与播放状态机全解析📌 核心要点:掌握 HarmonyOS 音频播放的核心 API——AVPlayer 与 SoundPool,理解播放状态机流转机制,实现从简单播放到播放列表管理的完整音频播放方案 一、背景与动机你有没有这样的经历——打开一个音乐 App,点击播放按钮,音乐瞬间流淌出来,进度条缓缓前进,切歌丝滑无卡...

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实例
        及时释放资源

核心要点回顾

  1. 状态机是灵魂:AVPlayer 的所有操作都必须遵循状态机流转,在 stateChange 回调中驱动是最安全的方式
  2. AVPlayer vs SoundPool:长音频用 AVPlayer(流式解码),短音效用 SoundPool(内存预加载),各司其职
  3. 播放列表管理:核心是复用 AVPlayer 实例 + 合理的切歌策略 + 完善的播放模式切换
  4. 资源管理:创建和释放要成对出现,避免内存泄漏;fdSrc 资源要妥善管理
  5. HarmonyOS 6:新增低延迟模式、空间音频、简化焦点管理等特性,迁移时注意 API 兼容性
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。