HarmonyOS 「校园二手交易商城」App应用实战 43 AVPlayer 状态机与播放控制:AvPlayerUtil
43 AVPlayer 状态机与播放控制:AvPlayerUtil
系列五:直播与画中画 · 第 3 篇
对应工程:
common/multishoppingbase/src/main/ets/utils/AvPlayerUtil.ets
引言
AVPlayer 是 HarmonyOS 媒体播放的核心类(@kit.MediaKit 的 media 命名空间),它最大的特点是严格的状态机:任何操作(prepare、play、pause、stop、release)都只能在特定状态下调用,调错就抛异常。因此工程里几乎不会裸用 AVPlayer,而是封装成 AvPlayerUtil:把"状态迁移 + 资源管理"收敛到一处,UI 层只暴露 createAvPlayer(surfaceId)、play()、pause()、playerStateControl()、release() 几个语义化方法。本项目的 AvPlayerUtil 位于公共层 multishoppingbase,被直播页与画中画同时引用,是全项目唯一与媒体底层打交道的类。
状态机全景:九个状态
AVPlayer 的状态机由 media.AVPlayerState 描述,共九个状态:
| 状态 | 含义 | 进入方式 | 可执行操作 |
idle | 空闲 | createAVPlayer() 后 | 设置 url/fdSrc/dataSrc |
initialized | 已初始化 | 设置数据源后 | 设置 surfaceId、prepare() |
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 → 自动 play:
prepared即自动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 后必须 reset:
error状态下播放器不可用,onError里调用avPlayer.reset()让它回到idle,之后状态机又能从idle重新走一遍(第 9 篇展开)。 - 单例别跨 Ability 共享:
abilityScopedAppStorageKey按 Ability 名隔离 key,分屏/多窗口时必须遵守,否则两个窗口的播放器会打架。
AvPlayerUtil 用"状态回调自驱动 + 公开方法状态校验"把复杂度封装完毕。下一篇看它如何被画中画 PipWindowUtil 调用,实现"直播飞出页面"。
更多推荐


所有评论(0)