【鸿蒙(openHarmony)ETS语言实现视频播放器的详细步骤】
鸿蒙(openHarmony)ETS语言实现视频播放器的详细步骤说明。
我们将创建一个健壮、稳定、易于维护的视频播放器组件,其中融入了您提到的状态检查、队列管理、错误恢复等最佳实践。
鸿蒙ETS视频播放器实现详解
- 基本思路
在鸿蒙系统中,视频播放的核心是 @ohos.multimedia.media 库提供的 media 模块。我们将创建一个自定义组件(@Component)来封装播放器的所有功能,包括初始化、准备、播放、暂停、停止、进度控制以及最关键的状态管理和错误处理。
- 实现步骤
步骤一:导入模块并定义状态
首先,在ETS文件中导入必要的模块,并定义播放器状态枚举,这是进行状态管理的基础。
// 导入媒体模块和UI基础模块
import media from '@ohos.multimedia.media';
import { BusinessError } from '@ohos.base';
import common from '@ohos.app.ability.common';// 定义播放器状态枚举,这是状态管理的核心
enum PlayerState {Idle = 'idle', // 初始或已重置Initialized = 'initialized', // 设置数据源后Prepared = 'prepared', // 准备完成Started = 'started', // 播放中Paused = 'paused', // 已暂停Stopped = 'stopped', // 已停止Error = 'error', // 错误状态Released = 'released' // 资源已释放
}@Component
export struct MyVideoPlayer {// 播放器实例private avPlayer?: media.AVPlayer;// 当前状态private currentState: PlayerState = PlayerState.Idle;// 操作队列标志位(防止并发操作)private isOperationPending: boolean = false;// 上下文private context: common.UIContext = getContext(this) as common.UIContext;// 用于在UI上展示的视频SurfaceID@State surfaceId?: string; // 其他UI相关的状态,如播放进度、总时长、缓冲进度等@State currentTime: number = 0;// ...
}
步骤二:初始化播放器 (aboutToAppear)
在组件即将出现时创建播放器实例并设置监听器。
aboutToAppear(): void {this.initPlayer();
}private async initPlayer(): Promise<void> {try {// 1. 创建AVPlayer实例this.avPlayer = await media.createAVPlayer(this.context);// 2. 立即将状态置为Idlethis.currentState = PlayerState.Idle;// 3. 【关键】设置状态监听器,用于监听状态变化和错误this.avPlayer.on('stateChange', async (state: media.AVPlayerState) => {switch (state) {case media.AVPlayerState.PREPARED:this.currentState = PlayerState.Prepared;console.info('AVPlayer state prepared');// 可以在这里自动开始播放,或者等待用户操作// await this.avPlayer.play();break;case media.AVPlayerState.STARTED:this.currentState = PlayerState.Started;break;case media.AVPlayerState.PAUSED:this.currentState = PlayerState.Paused;break;case media.AVPlayerState.STOPPED:this.currentState = PlayerState.Stopped;break;case media.AVPlayerState.COMPLETED:console.info('AVPlayer state completed');// 播放完成,可以重置或停止await this.resetPlayer();break;case media.AVPlayerState.RELEASED:this.currentState = PlayerState.Released;break;case media.AVPlayerState.ERROR:console.error('AVPlayer state error');this.currentState = PlayerState.Error;// 【关键:错误恢复机制】触发错误恢复逻辑this.tryRecoverFromError();break;default:break;}});// 4. 设置其他监听器,如时间更新、时长更新等this.avPlayer.on('timeUpdate', (time: number) => {this.currentTime = time;});} catch (error) {const err = error as BusinessError;console.error(`Failed to create AVPlayer, error code: ${err.code}, message: ${err.message}`);this.currentState = PlayerState.Error;}
}
步骤三:设置数据源并准备播放 (融入状态检查与操作队列)
这是最核心的步骤,包含了您总结的所有关键修改点。
// 【关键:操作队列管理】使用标志位确保同一时间只有一个准备操作在执行
private async prepareWithCheck(url: string): Promise<void> {if (this.isOperationPending) {console.warn('Another operation is in progress, please wait.');return;}this.isOperationPending = true; // 锁定队列try {// 1. 【关键:状态检查增强】检查当前状态是否允许准备操作if (!this.isStateValidForPrepare()) {// 2. 【关键:错误恢复机制】如果状态无效,尝试自动重置console.warn(`Current state (${this.currentState}) is invalid for prepare. Attempting reset...`);await this.resetPlayer();// 3. 【关键:超时处理改进】重置后再次检查状态,设置一个合理的超时等待const timeoutMs = 2000; // 2秒超时const startTime = new Date().getTime();while (this.currentState !== PlayerState.Idle) {if (new Date().getTime() - startTime > timeoutMs) {throw new Error('Timeout waiting for player to reset to Idle state.');}// 短暂等待,避免阻塞主线程await new Promise(resolve => setTimeout(resolve, 100));}}// 4. 设置数据源(URL)this.avPlayer!.url = url;this.currentState = PlayerState.Initialized; // 更新状态为Initialized// 5. 【关键:SurfaceID 管理优化】确保在准备之前设置了Surface// 如果surfaceId尚未创建或设置,这里需要等待或创建if (!this.surfaceId) {// 通常Surface由XComponent创建,我们需要等待其创建完毕并通过回调设置surfaceIdconsole.warn('SurfaceID is not ready. Waiting for it...');// 这里可以实现一个等待SurfaceID可用的逻辑(例如使用Promise),但通常是在XComponent的onLoad回调中设置好surfaceId后再调用prepare// 假设 surfaceId 已经由XComponent设置,我们直接继续}// 将Surface与播放器关联this.avPlayer!.surfaceId = this.surfaceId;// 6. 调用prepare()await this.avPlayer!.prepare();// 状态监听器会将状态变为 Prepared} catch (error) {const err = error as BusinessError;console.error(`Prepare failed, error code: ${err.code}, message: ${err.message}`);this.currentState = PlayerState.Error;this.tryRecoverFromError(); // 触发错误恢复} finally {this.isOperationPending = false; // 无论如何,最终释放操作锁}
}// 【关键:状态检查增强】辅助函数,检查当前状态是否允许准备
private isStateValidForPrepare(): boolean {const validStates = [PlayerState.Idle, PlayerState.Stopped, PlayerState.Initialized, PlayerState.Error];return validStates.includes(this.currentState);
}// 【关键:错误恢复机制】错误恢复函数
private async tryRecoverFromError(): Promise<void> {console.info('Attempting to recover from error...');try {await this.resetPlayer();this.currentState = PlayerState.Idle;console.info('Recovery successful.');} catch (recoverError) {const err = recoverError as BusinessError;console.error(`Recovery failed, error code: ${err.code}, message: ${err.message}`);// 如果恢复失败,可能需要完全销毁并重新初始化播放器,或者通知用户this.releasePlayer();await this.initPlayer(); // 重新初始化}
}
步骤四:构建UI布局 (XComponent 与控制按钮)
在UI中使用 XComponent 来提供视频绘制的Surface,并添加控制按钮。
build() {Column() {// XComponent 用于显示视频画面XComponent({id: 'video_surface',type: 'surface',libraryname: '',controller: this.xComponentController}).onLoad(() => {// 【关键:SurfaceID 管理优化】在XComponent加载完成后获取SurfaceIDthis.surfaceId = this.xComponentController.getXComponentSurfaceId();console.info(`SurfaceId: ${this.surfaceId}`);// 获取到SurfaceID后,可以尝试与已设置好数据源的播放器关联if (this.avPlayer && this.currentState === PlayerState.Initialized) {this.avPlayer.surfaceId = this.surfaceId;}}).width('100%').height(300) // 设置一个合适的高度// 控制按钮区域Row() {Button('Prepare & Play').onClick(async () => {// 调用我们封装好的准备方法await this.prepareWithCheck('https://example.com/sample.mp4'); // 替换为你的视频URL// 如果准备成功,状态变为Prepared,然后开始播放if (this.currentState === PlayerState.Prepared) {this.play();}})Button('Play').onClick(() => { this.play(); }).margin(5)Button('Pause').onClick(() => { this.pause(); }).margin(5)Button('Stop').onClick(() => { this.stop(); }).margin(5)Button('Reset').onClick(() => { this.resetPlayer(); }).margin(5)}.margin(5).justifyContent(FlexAlign.Center)// 进度条等其他UI控件// ...}
}// 基本的播放控制方法
private async play(): Promise<void> {if (this.avPlayer && (this.currentState === PlayerState.Prepared || this.currentState === PlayerState.Paused)) {await this.avPlayer.play();this.currentState = PlayerState.Started;}
}private async pause(): Promise<void> {if (this.avPlayer && this.currentState === PlayerState.Started) {await this.avPlayer.pause();this.currentState = PlayerState.Paused;}
}private async stop(): Promise<void> {if (this.avPlayer && (this.currentState === PlayerState.Started || this.currentState === PlayerState.Paused || this.currentState === PlayerState.Prepared)) {await this.avPlayer.stop();this.currentState = PlayerState.Stopped; // 监听器也会触发状态变化,这里手动设置一次确保及时更新}
}
步骤五:资源清理 (aboutToDisappear)
在组件销毁时,必须释放播放器资源。
private releasePlayer(): void {if (this.avPlayer) {this.avPlayer.release();this.avPlayer = undefined;this.currentState = PlayerState.Released;}
}aboutToDisappear(): void {this.releasePlayer();
}
实现效果:
总结
您提供的关键修改总结被系统地融入了鸿蒙ETS视频播放器的实现中:
- 状态检查增强:通过 isStateValidForPrepare() 方法在 prepare 前进行严格的状态验证。
- 操作队列管理:使用 isOperationPending 标志位实现了一个简单的操作锁,防止并发操作。
- 错误恢复机制:在 stateChange 监听器的 ERROR 事件和 prepare 的 catch 块中调用 tryRecoverFromError(),尝试通过重置或重新初始化来恢复。
- SurfaceID 管理优化:在 XComponent 的 onLoad 回调中安全地获取 surfaceId,并在准备前确保其已设置给播放器。
- 超时处理改进:在等待重置操作完成时,添加了超时逻辑 (timeoutMs),避免无限循环。
通过这些改进,播放器的稳定性和健壮性得到了极大提升,能够有效处理各种异常场景和状态冲突,从而解决您提到的
“current state is not stopped or initialized, unsupport prepare
operation”
错误。
你的鼓励是我创作的动力,想了解更多内容请关注下方公共号!