在鸿蒙中使用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;
}
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)}`);
}
}
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;
}
})
}
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;
}
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)}`);
}
}
}
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)}`);
}
}
}
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)}`);
}
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
大家可以直接使用这段代码,经过测试,这段代码是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 |