49 视频播放控制与异常处理

系列五:直播与画中画 · 第 9 篇
对应工程:common/multishoppingbase/src/main/ets/utils/AvPlayerUtil.ets

引言

视频能"放起来"只是及格线,能"受控地停、干净地释放、异常地降级"才是生产级。本篇聚焦 AvPlayerUtil 的三大韧性设计:playerStateControl 的播放/暂停总开关、onError 等错误回调的兜底、以及 release/closeRawFdSync 的资源回收链。这些代码平时不显眼,但正是它们在异常场景下保证应用不黑屏、不卡死、不泄漏。

播放/暂停总开关:playerStateControl

playerStateControl 是 UI 与 PiP 控制面板共用的入口(第 2 篇点视频、第 4 篇控制面板事件都调它),逻辑只有四步:

playerStateControl(): void {
  try {
    if (this.avPlayer === undefined) {
      Logger.error('AvPlayer is undefined');
      return;
    }
    if (this.avPlayer.state === 'stopped') {
      this.avPlayer.prepare();    // 播完后再点:重新准备,从头再播
      return;
    }
    if (!this.playState) {
      this.avPlayer.play();       // 暂停态 → 继续播
    } else {
      this.avPlayer.pause();      // 播放态 → 暂停
    }
  } catch (exception) {
    Logger.error('Failed to playerStateControl. Code: ' + JSON.stringify(exception));
  }
}

三个关键点:

  1. stopped 特判:播放完成后状态机停在 stopped,此时 play() 非法,必须先 prepare() 回到 prepared 才能播。这对应"播完再点重新播放"的交互。
  2. playState 是唯一事实源:内部布尔由 playing/paused 状态回调维护,比反复读 avPlayer.state 更轻量、更可靠。
  3. try/catch 兜底:AVPlayer 的方法返回 Promise 也可能同步抛错,统一捕获记日志,不让异常冒泡到 UI 层。

配套的 play()/pause() 是"有状态守卫"的简化版,只在状态允许时动作:

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

错误回调:onError 与 prepare 失败

AVPlayer 的错误有两条通道,AvPlayerUtil 两条都堵住了。

通道一:on('error') 注册的全局错误回调。播放器运行中任何内部错误(解码失败、资源异常等)都会触发:

private onError: (err: BusinessError) => void = (err: BusinessError) => {
  Logger.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
  if (this.avPlayer === undefined) {
    Logger.error('AvPlayer is undefined');
    return;
  }
  this.avPlayer.reset().catch(() => {
    Logger.error(`avPlayer reset error`);
  });
};

核心动作是 reset():把播放器从 error 状态拉回 idlereset() 后状态机自动进入 idleonStateChangeidle 分支会重新拉取 rawfile 描述符并设置 fdSrc——一次错误触发一次完整的"重置 → 重新绑定数据源"自愈流程,这正是第 3 篇说的"状态机自驱动"。

通道二:异步方法的 Promise 失败回调prepare() 等方法的失败不会进 onError,而是走 Promise 的 rejection:

this.avPlayer.prepare().then(() => {
  Logger.info('AVPlayer prepare succeeded.');
}, (err: BusinessError) => {
  Logger.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
  this.avPlayer?.reset().catch(() => {
    Logger.error(`avPlayer reset error`);
  });
});

两通道殊途同归:失败一律 reset,让状态机回到 idle 重新来过。如果连续失败,idle 分支每次都会重新 getRawFd,形成有限重试。

状态异常降级:error 与 released 的收尾

状态机里 errorreleased 两个终态都要做资源收尾——关闭文件描述符:

case 'released':
  try {
    uiContext.getHostContext()?.resourceManager.closeRawFdSync(AvPlayerUtil.liveVideoName);
  } catch (error) {
    Logger.info('closeRawFdSync called.');
  }
  break;
case 'error':
  Logger.error('AVPlayer state error called.');
  try {
    uiContext.getHostContext()?.resourceManager.closeRawFdSync(AvPlayerUtil.liveVideoName);
  } catch (error) {
    Logger.info('closeRawFdSync called.');
  }
  break;

closeRawFdSync 是同步关闭 rawfile 文件描述符的 API(第 10 篇详述)。error 状态下 fd 已不可靠,主动关闭避免 fd 泄漏;released 表示播放器已销毁,同样关闭。两处都包 try/catch——fd 可能已被系统回收,重复关闭会抛异常,吞掉即可。

release 资源回收链

release() 是页面退出时调用的总回收口,顺序有讲究——先注销回调、再释放实例

release(): void {
  if (this.avPlayer !== undefined && this.avPlayer.state !== 'released') {
    try {
      this.avPlayer.off('error');
      this.avPlayer.off('stateChange');
      this.avPlayer.release();
    } catch (exception) {
      Logger.error('Failed to unregister the error and state callback. Code: ' + JSON.stringify(exception));
    }
  } else {
    Logger.info('AvPlayer release failed');
  }
}

为什么必须先 offrelease?因为 release() 后对象进入 released 终态,若回调仍挂着,后续任何状态变化(或释放过程本身)都可能触发回调访问已释放的对象。off('error') + off('stateChange') 把两个监听器摘干净再释放,杜绝"野回调"。state !== 'released' 的守卫防止重复释放。

createAvPlayer 里还有个隐藏的释放点:Surface 变更时先 this.release() 再置 this.avPlayer = undefined 重建——复用同一个 release(),保证任何路径的销毁都走同一条回收链。

注意事项与总结

  • 错误统一走 resetonError 与 Promise rejection 两条通道最终都 reset()idle,配合状态机的 idle 分支自动重建数据源,实现自愈。
  • 先 off 后 release:释放顺序反了会留下野回调,release() 后对象不可再用。
  • closeRawFdSync 要 try/catch:fd 可能已被关闭,重复关闭抛异常属于预期,吞掉并记日志。
  • 状态守卫防非法调用stopped 不能直接 playreleased 不能重复 release,每个公开方法都要先查状态。
  • playState 与真实状态解耦:控制逻辑依赖内部布尔而非频繁读 avPlayer.state,兼顾性能与正确性。

播放控制与异常处理让播放器"皮实"了。最后一篇揭开数据源的秘密:rawfile 与 fdSrc 的资源管理。

Logo

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

更多推荐