#HarmonyOS NEXT体验官#在鸿蒙中使用AVPlayer完成视频的播放 原创
在鸿蒙中使用AVPlayer完成视频的播放
在HarmonyOS系统中,提供两种视频播放开发的方案:
- AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
- Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。
本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整地播放一个视频作为示例,实现端到端播放原始媒体资源。
开发指导
播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on(‘stateChange’)方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
图1 播放状态变化示意图
当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。
名称 | 类型 | 说明 |
---|---|---|
idle | string | 闲置状态,AVPlayer刚被创建createAVPlayer()或者调用了reset()方法之后,进入Idle状态。 首次创建createAVPlayer(),所有属性都为默认值。 调用reset()方法,url9+ 或 fdSrc9+或dataSrc10+属性及loop属性会被重置,其他用户设置的属性将被保留。 |
initialized | string | 资源初始化,在Idle 状态设置 url9+ 或 fdSrc9+属性,AVPlayer会进入initialized状态,此时可以配置窗口、音频等静态属性。 |
prepared | string | 已准备状态,在initialized状态调用prepare()方法,AVPlayer会进入prepared状态,此时播放引擎的资源已准备就绪。 |
playing | string | 正在播放状态,在prepared/paused/completed状态调用play()方法,AVPlayer会进入playing状态。 |
paused | string | 暂停状态,在playing状态调用pause方法,AVPlayer会进入paused状态。 |
completed | string | 播放至结尾状态,当媒体资源播放至结尾时,如果用户未设置循环播放(loop = 1),AVPlayer会进入completed状态,此时调用play()会进入playing状态和重播,调用stop()会进入stopped状态。 |
stopped | string | 停止状态,在prepared/playing/paused/completed状态调用stop()方法,AVPlayer会进入stopped状态,此时播放引擎只会保留属性,但会释放内存资源,可以调用prepare()重新准备,也可以调用reset()重置,或者调用release()彻底销毁。 |
released | string | 销毁状态,销毁与当前AVPlayer关联的播放引擎,无法再进行状态转换,调用release()方法后,会进入released状态,结束流程。 |
error | string | 错误状态,当播放引擎发生不可逆的错误,详见错误分类,则会转换至当前状态,可以调用reset()重置,也可以调用release()销毁重建。 注意: 区分error状态和 on(‘error’) : 1、进入error状态时,会触发on(‘error’)监听事件,可以通过on(‘error’)事件获取详细错误信息; 2、处于error状态时,播放服务进入不可播控的状态,要求客户端设计容错机制,使用reset()重置或者release()销毁重建; 3、如果客户端收到on(‘error’),但未进入error状态: 原因1:客户端未按状态机调用API或传入参数错误,被AVPlayer拦截提醒,需要客户端调整代码逻辑; 原因2:播放过程发现码流问题,导致容器、解码短暂异常,不影响连续播放和播控操作的,不需要客户端设计容错机制。 |
开发步骤及注意事项
1.创建实例createAVPlayer(),AVPlayer初始化idle状态。
2.设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
seekDone | 响应API调用,监听seek()请求完成情况。 当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
speedDone | 响应API调用,监听setSpeed()请求完成情况。 当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
volumeChange | 响应API调用,监听setVolume()请求完成情况。 当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。 当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 |
availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 |
bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 |
startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。 |
videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 |
audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。 如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
3.设置资源:设置属性url,AVPlayer进入initialized状态。
说明:
下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
- 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源.
- 如果使用网络播放路径,需申请相关权限:ohos.permission.INTERNET。
- 如果使用ResourceManager.getRawFd打开HAP资源文件描述符。
- 需要使用支持的播放格式与协议。
4.设置窗口:获取并设置属性SurfaceID,用于设置显示画面。 应用需要从XComponent组件获取surfaceID,获取方式请参考XComponent。
5.准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。
6.视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
7.(可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
8.退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。
完整代码:
今天我们来看一下视频如何正常播放。
这里面我用的是网络视频,大家记得申请权限之后使用哦。
import { common } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { media } from '@kit.MediaKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { CacheListener } from '@ohos/video-cache';
import GlobalProxyServer from '../model/GlobalProxyServer';
const ORIGIN_URL: string = 'http://192.168.1.15:4000/video';
const TAG: string = 'AVPlayManager';
export default class AvPlayManager {
private static instance: AvPlayManager | null = null;
private avPlayer: media.AVPlayer = {} as media.AVPlayer;
private surfaceID: string = '';
public static getInstance(): AvPlayManager {
if (!AvPlayManager.instance) {
AvPlayManager.instance = new AvPlayManager();
}
return AvPlayManager.instance;
}
/**
* Init player.
* @param context
* @param surfaceId
* @param callback
* @returns
*/
async initPlayer(context: common.UIAbilityContext, surfaceId: string,
callback: (avPlayer: media.AVPlayer) => void): Promise<void> {
hilog.info(0x0000, TAG, `initPlayer==initCamera surfaceId== ${surfaceId}`);
this.surfaceID = surfaceId;
try {
this.avPlayer = await media.createAVPlayer();
await this.setAVPlayerCallback(callback);
this.cacheAndPlayVideo(context);
} catch (err) {
hilog.error(0x0000, TAG, `initPlayer initPlayer err:${JSON.stringify(err)}`);
}
}
/**
* Set callback.
* @param callback
* @returns
*/
async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void): Promise<void> {
hilog.info(0x0000, TAG, `setAVPlayerCallback start`);
if (this.avPlayer === null) {
hilog.info(0x0000, TAG, 'avPlayer has not init');
return;
}
this.avPlayer.on('seekDone', (seekDoneTime) => {
hilog.info(0x0000, TAG, `AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
this.avPlayer.on('error', (err) => {
hilog.error(0x0000, TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
this.avPlayer.reset();
})
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle':
hilog.info(0x0000, TAG, 'AVPlayer state idle called.');
this.videoRelease();
break;
case 'initialized':
hilog.info(0x0000, TAG, 'AVPlayer state initialized called.');
if (this.surfaceID) {
this.avPlayer.surfaceId = this.surfaceID;
hilog.info(0x0000, TAG, `setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
this.avPlayer.prepare();
}
break;
case 'prepared':
hilog.info(0x0000, TAG, 'AVPlayer state prepared called.');
callback(this.avPlayer);
hilog.info(0x0000, TAG, 'AVPlayer state prepared duration.' + this.avPlayer.duration);
this.avPlayer.play();
break;
case 'playing':
hilog.info(0x0000, TAG, 'AVPlayer state playing called.');
AppStorage.setOrCreate('playStatus', 'playing');
break;
case 'paused':
hilog.info(0x0000, TAG, 'AVPlayer state paused called.');
break;
case 'completed':
hilog.info(0x0000, TAG, 'AVPlayer state completed called.');
AppStorage.setOrCreate('playStatus', 'completed');
break;
case 'stopped':
hilog.info(0x0000, TAG, 'AVPlayer state stopped called.');
break;
case 'released':
hilog.info(0x0000, TAG, 'AVPlayer state released called.');
break;
default:
hilog.info(0x0000, TAG, 'AVPlayer state unknown called.');
break;
}
})
}
/**
* Cache and play video.
* @param context
* @returns
*/
async cacheAndPlayVideo(context: common.UIAbilityContext): Promise<void> {
hilog.info(0x0000, TAG, `cacheAndPlayVideo start`);
class MyCacheListener implements CacheListener {
onCacheAvailable(cacheFilePath: string, url: string, percentsAvailable: number): void {
AppStorage.setOrCreate('currentCachePercent', percentsAvailable);
}
}
GlobalProxyServer?.getInstance()?.getServer()?.registerCacheListener(new MyCacheListener(), ORIGIN_URL);
let proxyUrl: string | undefined = await GlobalProxyServer?.getInstance()?.getServer()?.getProxyUrl(ORIGIN_URL);
if (proxyUrl?.startsWith(context.cacheDir)) {
const file = fs.openSync(proxyUrl, fs.OpenMode.READ_ONLY);
proxyUrl = `fd://${file.fd}`;
}
hilog.info(0x0000, TAG, `proxyUrl ${proxyUrl}`);
this.avPlayer.url = proxyUrl;
}
/**
* Play video.
*/
videoPlay(): void {
hilog.info(0x0000, TAG, `videoPlay start`);
if (this.avPlayer !== null) {
try {
this.avPlayer.play();
} catch (err) {
hilog.error(0x0000, TAG, `videoPlay = ${JSON.stringify(err)}`);
}
}
}
/**
* Pause video.
*/
videoPause(): void {
hilog.info(0x0000, TAG, `videoPause start`);
if (this.avPlayer !== null) {
try {
this.avPlayer.pause();
} catch (err) {
hilog.info(0x0000, TAG, `videoPause== ${JSON.stringify(err)}`);
}
}
}
/**
* Release video.
*/
videoRelease(): void {
hilog.info(0x0000, TAG, `videoRelease start`);
if (this.avPlayer !== null) {
try {
this.avPlayer.release();
} catch (err) {
hilog.info(0x0000, TAG, `videoRelease== ${JSON.stringify(err)}`);
}
}
}
}
大家可以直接使用这段代码,经过测试,这段代码是ok的。
彩蛋
支持的视频播放格式和主流分辨率如下:
视频容器规格 | 规格描述 | 分辨率 |
---|---|---|
mp4 | 视频格式:H26510+/H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
mkv | 视频格式:H26510+/H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
ts | 视频格式:H26510+/H264/MPEG2/MPEG4 音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
webm | 视频格式:VP8 音频格式:VORBIS | 主流分辨率,如4K/1080P/720P/480P/270P |
支持的协议如下:
协议类型 | 协议描述 |
---|---|
本地点播 | 协议格式:支持file descriptor,禁止file path |
网络点播 | 协议格式:支持http/https/hls |
网络直播 | 协议格式:支持hls |