43 AVPlayer 状态机与播放控制:AvPlayerUtil

系列五:直播与画中画 · 第 3 篇

对应工程:common/multishoppingbase/src/main/ets/utils/AvPlayerUtil.ets

引言

AVPlayer 是 HarmonyOS 媒体播放的核心类(@kit.MediaKitmedia 命名空间),它最大的特点是严格的状态机:任何操作(prepareplaypausestoprelease)都只能在特定状态下调用,调错就抛异常。因此工程里几乎不会裸用 AVPlayer,而是封装成 AvPlayerUtil:把"状态迁移 + 资源管理"收敛到一处,UI 层只暴露 createAvPlayer(surfaceId)play()pause()playerStateControl()release() 几个语义化方法。本项目的 AvPlayerUtil 位于公共层 multishoppingbase,被直播页与画中画同时引用,是全项目唯一与媒体底层打交道的类。

状态机全景:九个状态

AVPlayer 的状态机由 media.AVPlayerState 描述,共九个状态:

状态含义进入方式可执行操作

idle空闲createAVPlayer()设置 url/fdSrc/dataSrc
initialized已初始化设置数据源后设置 surfaceIdprepare()
prepared已就绪prepare() 成功play()pause()seek
playing播放中play()pause()stop()
paused已暂停pause()play()stop()
completed播放完成播到结尾stop()
stopped已停止stop()prepare() 重新播放
released已释放release()无(终态)
error出错任意失败reset() 回到 idle

状态迁移图可以简化记忆为一条主链:idle → initialized → prepared → playing ↔ paused → completed → stopped →(prepare 回到 prepared)error 是旁路,released 是终点。任何跳过合法迁移的调用都会报错——这是封装类存在的根本原因。

createAVPlayer 与单例存储

创建播放器用 media.createAVPlayer()(异步返回 Promise):

async createAvPlayer(surfaceId: string): Promise<void> {
  try {
    const playerDead: boolean = this.avPlayer === undefined || this.avPlayer.state === 'released';
    if (!playerDead) {
      if (this.surfaceId === surfaceId) {
        Logger.info('AvPlayer has been created');
        return; // 同一个 Surface 复用实例
      }
      this.release();          // Surface 变了:先释放旧的
      this.avPlayer = undefined;
    }
    this.surfaceId = surfaceId;
    this.avPlayer = await media.createAVPlayer();
    this.url = await this.uiContext?.getHostContext()?.resourceManager.getRawFd(AvPlayerUtil.liveVideoName);
    this.avPlayer.fdSrc = this.url;   // fdSrc 数据源(第 10 篇详述)
    this.setAVPlayerCallback();       // 注册 error + stateChange
  } catch (error) {
    Logger.error(`createAvPlayer catch error, code: ${error.code}, message: ${error.message}`);
  }
}

createAvPlayer 内部做了两个关键判断:

  • Surface 复用surfaceId 相同直接 return,避免反复重建播放器。
  • Surface 变更:先 release() 旧实例再重建——因为 AVPlayer 绑定 Surface 后不能换绑,只能释放重来。

单例存取走 AppStorage,key 经 abilityScopedAppStorageKey 按 Ability 名加后缀(分屏场景下多个 UIAbility 窗口各自持有独立播放器,避免互相抢占 Surface 导致黑屏):

static getAvPlayerUtil(uiContext: UIContext): AvPlayerUtil | undefined {
  const key = abilityScopedAppStorageKey(uiContext, AvPlayerUtil.avPlayerUtilKey); // 'MultiShoppingAvPlayerUtil'
  if (!AppStorage.get<AvPlayerUtil>(key)) {
    AppStorage.setOrCreate(key, new AvPlayerUtil(uiContext));
  }
  return AppStorage.get<AvPlayerUtil>(key);
}

onStateChange:状态驱动的全自动流程

播放器创建后,用 avPlayer.on('stateChange', cb) 注册状态回调。回调是异步函数,AvPlayerUtil 在构造函数中定义,按状态逐个处理——这是整个封装的"心脏":

this.onStateChange = async (state: media.AVPlayerState) => {
  switch (state) {
    case 'idle':
      // 重新拉取 rawfile 描述符并绑定 fdSrc(先关闭上一次的)
      this.url = await hostContext.resourceManager.getRawFd(AvPlayerUtil.liveVideoName);
      this.avPlayer.fdSrc = this.url;
      break;
    case 'initialized':
      this.avPlayer.surfaceId = this.surfaceId;   // 绑定画布
      this.avPlayer.prepare().then(...);           // 异步进入 prepared
      break;
    case 'prepared':
      this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT; // 等比适配
      this.avPlayer.play();
      break;
    case 'playing':
      this.playState = true;
      break;
    case 'paused':
      this.playState = false;
      break;
    case 'completed':
      this.playState = false;
      this.avPlayer.stop();                        // 播完回 stopped,可再 prepare
      break;
    case 'released':
    case 'error':
      hostContext.resourceManager.closeRawFdSync(AvPlayerUtil.liveVideoName); // 关文件描述符
      break;
  }
};

注意两个"自驱动"设计:

  • idle → fdSrc:状态机回到 idle 时(如 reset() 后)会自动重新拉取 RawFileDescriptor 并设置 fdSrc,让播放器自动回到 initialized
  • prepared → 自动 playprepared 即自动 play(),所以调用方只需要 createAvPlayer(surfaceId) 一件事,后面全程由状态机驱动到播放。直播页"进页面即播"的体验就是这么来的。

播放控制:playerStateControl / play / pause

控制方法都先校验状态再操作,杜绝非法调用:

playerStateControl(): void {
  if (this.avPlayer === undefined) return;
  if (this.avPlayer.state === 'stopped') {
    this.avPlayer.prepare();   // 播完后再点:重新准备
    return;
  }
  if (!this.playState) {
    this.avPlayer.play();
  } else {
    this.avPlayer.pause();
  }
}

play(): void {
  if (this.avPlayer !== undefined && !this.playState) {
    this.avPlayer.play();
  }
}

pause(): void {
  if (this.avPlayer !== undefined && this.playState) {
    this.avPlayer.pause();
  }
}

playState 是内部布尔(初始 true),由 playing/paused 状态回调维护,作为"当前是否在播"的唯一事实源。playerStateControl 是 UI 和 PiP 控制面板共用的入口(第 4、9 篇会反复出现)。

release 与回调注销

释放必须"先注销回调、再释放实例",否则释放后回调仍可能被触发:

release(): void {
  if (this.avPlayer !== undefined && this.avPlayer.state !== 'released') {
    this.avPlayer.off('error');
    this.avPlayer.off('stateChange');
    this.avPlayer.release();
  }
}

注意事项与总结

  • 状态校验前置:每个公开方法都先查 avPlayer === undefined 和当前 state,这是 AVPlayer 状态机编程的第一纪律。
  • 回调里别同步做重活onStateChange 是异步回调,内部 getRawFd 用了 await,但要避免在其中再注册监听器。
  • error 后必须 reseterror 状态下播放器不可用,onError 里调用 avPlayer.reset() 让它回到 idle,之后状态机又能从 idle 重新走一遍(第 9 篇展开)。
  • 单例别跨 Ability 共享abilityScopedAppStorageKey 按 Ability 名隔离 key,分屏/多窗口时必须遵守,否则两个窗口的播放器会打架。
AVPlayer 的九态状态机是"视频能播起来"的底层保证,AvPlayerUtil 用"状态回调自驱动 + 公开方法状态校验"把复杂度封装完毕。下一篇看它如何被画中画 PipWindowUtil 调用,实现"直播飞出页面"。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐