HarmonyOS 「校园二手交易商城」App应用实战 49:视频播放控制与异常处理
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));
}
}
三个关键点:
stopped特判:播放完成后状态机停在stopped,此时play()非法,必须先prepare()回到prepared才能播。这对应"播完再点重新播放"的交互。playState是唯一事实源:内部布尔由playing/paused状态回调维护,比反复读avPlayer.state更轻量、更可靠。- 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 状态拉回 idle。reset() 后状态机自动进入 idle,onStateChange 的 idle 分支会重新拉取 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 的收尾
状态机里 error 与 released 两个终态都要做资源收尾——关闭文件描述符:
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');
}
}
为什么必须先 off 再 release?因为 release() 后对象进入 released 终态,若回调仍挂着,后续任何状态变化(或释放过程本身)都可能触发回调访问已释放的对象。off('error') + off('stateChange') 把两个监听器摘干净再释放,杜绝"野回调"。state !== 'released' 的守卫防止重复释放。
createAvPlayer 里还有个隐藏的释放点:Surface 变更时先 this.release() 再置 this.avPlayer = undefined 重建——复用同一个 release(),保证任何路径的销毁都走同一条回收链。
注意事项与总结
- 错误统一走 reset:
onError与 Promise rejection 两条通道最终都reset()回idle,配合状态机的idle分支自动重建数据源,实现自愈。 - 先 off 后 release:释放顺序反了会留下野回调,
release()后对象不可再用。 - closeRawFdSync 要 try/catch:fd 可能已被关闭,重复关闭抛异常属于预期,吞掉并记日志。
- 状态守卫防非法调用:
stopped不能直接play、released不能重复release,每个公开方法都要先查状态。 - playState 与真实状态解耦:控制逻辑依赖内部布尔而非频繁读
avPlayer.state,兼顾性能与正确性。
播放控制与异常处理让播放器"皮实"了。最后一篇揭开数据源的秘密:rawfile 与 fdSrc 的资源管理。
更多推荐



所有评论(0)