目录

使用AVPlayer播放音频(ArkTS)

使用AVPlayer可以实现端到端播放原始媒体资源,本开发指导将以完整播放一首音乐作为示例,向开发者讲解AVPlayer音频播放相关功能。如需播放PCM音频数据,请使用AudioRenderer。

播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。

在应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态,或使用on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或产生其他未定义的行为。

图1 播放状态变化示意图

状态的详细说明请参考AVPlayerState。当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统大量的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。

开发建议

当前指导仅介绍如何实现媒体资源播放,在应用开发过程中,涉及后台播放、播放冲突等情况时,请根据实际需要参考以下说明。

开发步骤及注意事项

详细的API说明请参考AVPlayer。

  1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。

    import { media } from '@kit.MediaKit';
    // ...
        // 创建avPlayer实例对象
        this.avPlayer = await media.createAVPlayer();
  2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:

    事件类型 说明
    stateChange

    必要事件,监听播放器的state属性改变。

    需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的stateChange事件。

    error

    必要事件,监听播放器的错误信息。

    需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的error事件。

    durationUpdate 用于进度条,监听进度条长度,刷新资源时长。
    timeUpdate 用于进度条,监听进度条当前位置,刷新当前时间。
    seekDone

    响应API调用,监听seek()请求完成情况。

    当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。

    speedDone

    响应API调用,监听setSpeed()请求完成情况。

    当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。

    volumeChange

    响应API调用,监听setVolume()请求完成情况。

    当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。

    bufferingUpdate 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。
    audioInterrupt

    监听音频焦点切换信息,搭配属性audioInterruptMode使用。

    如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。

    if (this.avPlayer == null) {
      console.error(`${this.tag}: avPlayer has not init!`);
      return;
    }
    this.avPlayer.on('seekDone', (seekDoneTime) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    });
    this.avPlayer.on('speedDone', (speed) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
    });
    this.avPlayer.on('durationUpdate', (duration: number) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer durationUpdate, duration is ${duration}`);
    });
    this.avPlayer.on('volumeChange', (vol: number) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer volumeChange, vol is ${vol}`);
    });
    this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer bufferingUpdate, vol is ${infoType}, value is ${value}`);
    });
    this.avPlayer.on('audioInterrupt', (info: audio.InterruptEvent) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer audioInterrupt, info is ${info}`);
    });
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程。
    this.avPlayer.on('error', (err) => {
      console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);
      if (this.avPlayer == null) {
        console.error(`${this.tag}: avPlayer has not init on error`);
        return;
      }
      this.avPlayer.reset();
    });
    // 状态机变化回调函数。
    this.avPlayer.on('stateChange', async (state, reason) => {
      if (this.avPlayer == null) {
        console.info(`${this.tag}: avPlayer has not init on state change`);
        return;
      }
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报。
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报。
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);
          if (this.surfaceId) {
            this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置。
            console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
            this.avPlayer.prepare();
          }
          break;
        case 'prepared': // prepare调用成功后上报该状态机。
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);
          this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
            console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);
          })
          this.durationTime = this.avPlayer.duration;
          this.currentTime = this.avPlayer.currentTime;
          this.avPlayer.play(); // 调用播放接口开始播放。
          console.info(`${this.tag}:
            setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);
          if (this.speedSelect != -1) {
            switch (this.speedSelect) {
              case SPEED_ZERO:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
                break;
              case SPEED_ONE:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
                break;
              case SPEED_TWO:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
                break;
              case SPEED_THREE:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
                break;
            }
          }
          callback(this.avPlayer);
          break;
        case 'playing': // play成功调用后触发该状态机上报。
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);
          if (this.count !== 0) {
            if (this.intervalID != -1) {
              clearInterval(this.intervalID)
            }
            this.intervalID = setInterval(() => { // 更新当前时间。
              AppStorage.setOrCreate('durationTime', this.durationTime);
              AppStorage.setOrCreate('currentTime', this.currentTime);
            }, 100);
            let eventDataTrue: emitter.EventData = {
              data: {
                'flag': true
              }
            };
            let innerEventTrue: emitter.InnerEvent = {
              eventId: 2,
              priority: emitter.EventPriority.HIGH
            };
            emitter.emit(innerEventTrue, eventDataTrue);
          } else {
            setTimeout(() => {
              console.info('AVPlayer playing wait to pause');
              this.avPlayer?.pause(); // 播放3s后调用暂停接口暂停播放。
            }, 3000);
          }
          this.count++;
          break;
        case 'completed': // 播放结束后触发该状态机上报。
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);
          let eventDataFalse: emitter.EventData = {
            data: {
              'flag': false
            }
          };
          let innerEvent: emitter.InnerEvent = {
            eventId: 1,
            priority: emitter.EventPriority.HIGH
          };
          emitter.emit(innerEvent, eventDataFalse);
          if (this.intervalID != -1) {
            clearInterval(this.intervalID)
          }
          this.avPlayer.off('bufferingUpdate')
          AppStorage.setOrCreate('currentTime', this.durationTime);
          break;
        case 'released':
          console.info(`${this.tag}: setAVPlayerCallback released called.`);
          break
        case 'stopped':
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);
          break
        case 'error':
          console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);
          break
        case 'paused':
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);
          setTimeout(() => {
            console.info('AVPlayer paused wait to play again');
            this.avPlayer?.play(); // 暂停3s后再次调用播放接口开始播放。
          }, 3000);
          break
        default:
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);
          break;
      }
    });
    // 时间上报监听函数。
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentTime = time;
    });
  3. 设置资源:设置属性url,AVPlayer进入initialized状态。

    下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:

    示例一:播放网络媒体资源

    let url = ''; // 此处仅为示意,请替换为真实资源文件URL。
    if (this.avPlayer == null) {
      return;
    }
    this.avPlayer.url = url;

    示例二:应用沙箱文件播放

    let fdPath = 'fd://'; // 此处仅为示意,请替换为真实资源文件URL。
    let path : string = `${this.context?.filesDir}/${this.fileName}`; // 此处仅为示意,请替换为真实的应用沙箱文件路径。
    let file = await fs.open(path);
    try {
      fdPath = fdPath + file.fd;
      this.avPlayer = await media.createAVPlayer();
      this.avPlayer.url = fdPath;
    } finally {
      fs.closeSync(file);
    }
  4. (可选)设置音频渲染:只允许在initialized状态下,第一次调用prepare()之前设置,以便音频渲染器信息在之后生效。若媒体源包含视频,则usage默认值为STREAM_USAGE_MOVIE,否则usage默认值为STREAM_USAGE_MUSIC。rendererFlags默认值为0。

    为了确保音频行为符合使用预期,建议根据具体业务场景和实际需求,主动配置audio.AudioRendererInfo,为音频选择合适的播放流类型。

    if (this.avPlayer) {
      this.avPlayer.audioRendererInfo = {
        usage: audio.StreamUsage.STREAM_USAGE_MOVIE,
        rendererFlags: 0
      }
    }
  5. 准备播放:调用 prepare()方法进入准备播放阶段,AVPlayer 将切换至 prepared 状态,此时可获取视频时长(duration)并调整音量参数。

    if (this.avPlayer) {
      try {
        this.avPlayer.prepare();
        console.info(`${this.tag}: avPrepare==`);
      } catch (e) {
        console.error(`${this.tag}: avPrepare== ${JSON.stringify(e)}`);
      }
    }
  6. 音频播控:播放play()、暂停pause()、跳转seek()、停止stop() 等操作。

    在API version 23及之后版本中,播放音频时会跳过静音帧。

    if (this.avPlayer) {
      try {
        this.avPlayer.play();
      } catch (e) {
        console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);
      }
    }
    if (this.avPlayer) {
      try {
        this.avPlayer.pause();
        console.info(`${this.tag}: avPause==`);
      } catch (e) {
        console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);
      }
    }
    if (this.avPlayer) {
      try {
        console.info(`${this.tag}: videoSeek  seekTime== ${seekTime}`);
        this.avPlayer.seek(seekTime, 2);
        this.currentTime = seekTime;
      } catch (e) {
        console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);
      }
    }
    if (this.avPlayer) {
      try {
        this.avPlayer.stop();
        console.info(`${this.tag}: avStop==`);
      } catch (e) {
        console.error(`${this.tag}: avStop== ${JSON.stringify(e)}`);
      }
    }
  7. (可选)更换资源:调用reset()方法重置播放资源,AVPlayer重新进入idle状态,此时可重新设置资源url。

    if (this.avPlayer) {
      try {
        this.avPlayer.reset();
      } catch (e) {
        console.error(`${this.tag}: reset== ${JSON.stringify(e)}`);
      }
    }
    // 更换url。
    let url = '';
    if (this.avPlayer == null) {
      return;
    }
    this.avPlayer.url = url;
  8. 退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。

    this.avPlayer.release((err) => {
      if (err == null) {
        console.info(`${this.tag}: videoRelease release success`);
      } else {
        console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
      }
    });

运行完整示例

参考以下示例,完整地播放一首音乐,实现起播后3s暂停,暂停3s重新播放的效果。

  1. 新建工程,下载示例工程,并将示例工程的以下资源复制到对应目录。
    AVPlayerArkTSAudio
    entry/src/main/ets/
    └── pages
        └── Index.ets (播放界面)
    entry/src/main/resources/
    ├── base
    │   ├── element
    │   │   ├── color.json
    │   │   ├── float.json
    │   │   └── string.json
    │   └── media
    │       ├── ic_video_play.svg  (播放键图片资源)
    │       └── ic_video_pause.svg (暂停键图片资源)
    └── rawfile
        └── test_01.mp3 (音频资源)
  2. 编译新建工程并运行。