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个月内不可修改。