
回复
AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。
本文介绍一下AVPlayer的基本使用,播放网络视频,实现播放暂停功能、进度条拖拽功能,看一下演示效果:
当使用AVPlayer开发视频应用播放视频时,AVPlayer与外部模块的交互关系如图所示:
支持的协议如下:
协议类型 | 协议格式描述 |
---|---|
本地点播 | 支持file descriptor,禁止file path |
网络点播 | 支持http/https/hls/dash |
网络直播 | 支持hls/http-flv |
支持的视频播放格式和主流分辨率如下:
视频容器规格 | 规格描述 | 分辨率 |
---|---|---|
mp4 | 视频格式:H265/H264 | 主流分辨率,如4K/1080P/720P/480P/270P |
mkv | 视频格式:H265/H264 | 主流分辨率,如4K/1080P/720P/480P/270P |
ts | 视频格式:H265/H264 | 主流分辨率,如4K/1080P/720P/480P/270P |
播放的全流程包含:
步骤 | 方法 |
---|---|
创建AVPlayer | media.createAVPlayerr(callback: AsyncCallback<AVPlayer>): void |
设置播放资源和窗口 | url/fdSrc XComponent.onLoad(callback: OnNativeLoadCallback ) |
设置播放参数(音量/倍速/缩放模式) | setVolume、setVolume、videoScaleType |
播放控制(播放/暂停/跳转/停止) | 播放play(),暂停pause(),跳转seek(),停止stop() |
重置 | reset() |
销毁资源 | release() |
播放状态变化示意图
AVPlayer监听回调事件
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
seekDone | 响应API调用,监听seek()请求完成情况。 |
speedDone | 响应API调用,监听setSpeed()请求完成情况。 |
volumeChange | 响应API调用,监听setVolume()请求完成情况。 |
bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。 |
availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 |
bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 |
startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。 |
videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 |
audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。 |
import { media } from '@kit.MediaKit';
import { display } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { TimeFormatUtil } from '../utils/TimeFormatUtil';
import { AlignRules } from '../utils/AlignRules';
@Entry
@ComponentV2
struct AVPlayerTest{
private xComponentController: XComponentController = new XComponentController();
private avPlayer: media.AVPlayer | null = null;
private surfaceID: string = '';
private sliderClickValue:number=0 //记录点击滑块时的进度值
@Local XComponentHeight: number = display.getDefaultDisplaySync().height;
@Local XComponentWidth: number = display.getDefaultDisplaySync().width;
@Local isSliderMoving: boolean = false;
@Local isPlaying: boolean = true;
@Local currentTime: number = 0;// 视频当前时间
@Local durationTime: number = 0; // 视频总时长
@Local currentStringTime: string = '00:00';
@Local durationStringTime: string = '00:00';
//创建AVPlayer
async initAVPlayer() {
media.createAVPlayer().then((video: media.AVPlayer) => {
if (video === undefined) {
return;
}
this.avPlayer = video;
this.setAVPlayerCallback(this.avPlayer);
this.avPlayer.url='http://t-cdn.kaiyanapp.com/1751377904927_21de26f9.mp4'
})
}
//avPlayer 回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
//监听seek生效的事件
avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
});
avPlayer.on('speedDone', (speed) => {
console.info(`setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
});
//监听资源播放当前时间,单位为毫秒(ms)
avPlayer.on('timeUpdate', (time: number) => {
if (!this.isSliderMoving) {
this.currentTime =time;
this.currentStringTime = TimeFormatUtil.formatToHMS(this.currentTime);
}
})
//监听视频播放宽高变化事件
avPlayer.on('videoSizeChange', (width: number, height: number) => {
console.info(`setAVPlayerCallback AVPlayer videoSizeChange, width is ${width} height is ${height} `);
display.getAllDisplays((err, data) => {
let screenWidth = data[0].width;
let screenHeight = data[0].width;
let videoRatio: number = Number(width) / Number(height);
let screenRatio = screenWidth / screenHeight;
if (videoRatio > screenRatio) {
this.XComponentWidth = this.getUIContext().px2vp(screenWidth);
this.XComponentHeight = this.getUIContext().px2vp(height * screenWidth / width);
} else {
this.XComponentWidth = this.getUIContext().px2vp(width * screenHeight / height);
this.XComponentHeight = this.getUIContext().px2vp(screenHeight);
}
});
})
//监听错误事件,该事件仅用于错误提示,不需要用户停止播控动作
avPlayer.on('error', (err: BusinessError) => {
console.info(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}.` +
`----state: ${avPlayer.state}`);
avPlayer.reset();
})
//状态机切换事件回调类型
avPlayer.on('stateChange', async (state: string) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(` setAVPlayerCallback AVPlayer state idle called.`);
break;
case 'initialized':// avplayer 设置播放源后触发该状态上报
console.info(`setAVPlayerCallback AVPlayer state initialized called.`);
avPlayer.surfaceId = this.surfaceID;
avPlayer.prepare();
break;
case 'prepared':// prepare调用成功后上报该状态机
console.info(`setAVPlayerCallback AVPlayer state prepared called.`);
this.durationTime = avPlayer.duration;
this.currentTime = avPlayer.currentTime;
this.durationStringTime = TimeFormatUtil.formatToHMS(this.durationTime);
avPlayer.play();
break;
case 'playing':// play成功调用后触发该状态机上报
console.info(`setAVPlayerCallback AVPlayer state playing called.`);
this.isPlaying = true;
break;
case 'paused':
console.info(`setAVPlayerCallback AVPlayer state paused called.`);
this.isPlaying = false;
break;
case 'stopped':
console.info(`setAVPlayerCallback AVPlayer state stopped called.`);
this.isPlaying = false;
break;
case 'completed':// 播放结束后触发该状态机上报
console.info(`setAVPlayerCallback AVPlayer state completed called.`);
this.isPlaying = false;
break;
case 'released':
console.info(`setAVPlayerCallback released called.`);
break;
case 'error':
console.error(`setAVPlayerCallback AVPlayer state error called.`);
this.isPlaying = false;
avPlayer.reset();
break;
default:
console.info(` setAVPlayerCallback AVPlayer state unknown called.`);
break;
}
})
}
build() {
RelativeContainer(){
XComponent({
// 装载视频容器
id: 'xComponent',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.alignRules(AlignRules.centerInParent)
.onLoad(() => {
this.surfaceID = this.xComponentController.getXComponentSurfaceId();
this.initAVPlayer()
})
.width(this.XComponentWidth)
.height(this.XComponentHeight)
//控制组件
this.playControl()
if (!this.isPlaying) {
Image($r('app.media.ic_paused'))
.width('56vp')
.height('56vp')
.alignRules(AlignRules.centerInParent)
}
}
.backgroundColor(Color.Black)
.height('100%')
.width('100%')
.onClick(()=>{
if (this.isPlaying) {
if (this.avPlayer === undefined) {
console.info(`pauseVideo error! avPlayer undefined! .`);
return;
}
this.avPlayer!.pause();
}else {
if (this.avPlayer === undefined) {
console.info(`playVideo error! avPlayer undefined! .`);
return;
}
if (this.avPlayer!.state === 'prepared' ||this.avPlayer!.state === 'paused' ||
this.avPlayer!.state === 'completed') {
this.avPlayer!.play();
}
}
})
}
@Builder
playControl() {
Column() {
Row() {
Text(this.currentStringTime)
.fontSize('20vp')
.fontColor(Color.White)
.margin({ left: '2vp' })
.width('45%')
.textAlign(TextAlign.End)
.zIndex(3)
Divider()
.vertical(true)
.height('14vp')
.width('2vp')
.backgroundBlurStyle(BlurStyle.Regular, { colorMode: ThemeColorMode.LIGHT })
.color(Color.White)
.opacity(0.5)
.margin({ left: 8, right: 8 })
.rotate({
x: 0,
y: 0,
z: 1,
centerX: '50%',
centerY: '50%',
angle: 30
})
Text( this.durationStringTime)
.fontSize('20vp')
.fontColor(Color.White)
.margin({ left: '2vp' })
.width('45%')
.textAlign(TextAlign.Start)
.opacity(0.5)
.zIndex(3)
}
.width('100%')
.opacity(this.isSliderMoving?1:0)
Row(){
Text(this.currentStringTime)
.fontColor(Color.White)
.textAlign(TextAlign.End)
.fontWeight(FontWeight.Regular)
.margin({ left:10})
Slider({
value: this.currentTime,
min: 0,
max: this.durationTime,
style: SliderStyle.NONE
})
.layoutWeight(1)
.height(20)
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor('#007DFF')
.showSteps(false)
.showTips(false)
.trackThickness(4)
.trackBorderRadius(2)
.zIndex(3)
.onChange((value: number, mode: SliderChangeMode) => {
console.info(`Slider onChange value ${value} `);
if (mode === SliderChangeMode.Begin) {
this.isSliderMoving = true;
this.sliderClickValue = value
}
if (mode === SliderChangeMode.Moving) {
this.currentStringTime = TimeFormatUtil.formatToHMS(value)
this.sliderClickValue = value
}
if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) { //离开滑块 点击滑动条
this.avPlayer!.seek( this.sliderClickValue, media.SeekMode.SEEK_NEXT_SYNC);
this.isSliderMoving = false;
}
})
Text( this.durationStringTime)
.fontColor(Color.White)
.fontWeight(FontWeight.Regular)
}
}.alignRules(AlignRules.alignParentBottomCenter)
}
aboutToDisappear() {
if (this.avPlayer == null) {
console.info(`avPlayer has not init aboutToDisappear`);
return;
}
this.avPlayer.release((err) => {
if (err == null) {
console.info(`videoRelease release success`);
} else {
console.error(`videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
}
});
}
onPageHide() {
this.avPause();
this.isPlaying = false;
}
avPause(): void {
if (this.avPlayer) {
try {
this.avPlayer.pause();
console.info(` avPause==`);
} catch (e) {
console.error(`avPause== ${JSON.stringify(e)}`);
}
}
}
}
注意:
1.监听videoSizeChange事件,获取到视频的长宽,根据长宽比重新设置XComponent的长宽,让视频充满长或宽,显示完整
2.Slider点击滑动条会触发3次onChange事件,分别是按下,移动/点击,结束,因此这里需要特殊处理一下