基于AVPlayer播放
基于AVPlayer视频播放 前言 AVPlayer:功能较完善的音视频播放Ark/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适
基于AVPlayer视频播放
前言
AVPlayer:功能较完善的音视频播放Ark/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于媒体资源进行端到端播放的场景,可直接播放MP4,mkv等格式的视频文件在进行应用应用开发过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或者使用on(‘stateChange’)方法监听变化。
开发步骤
调用createAVPlayer() 创建AVPlayer实例,初始化进入idle状态设置业务需要的监听事件
stateChange:监听播放器state属性变化videoSizeChange:用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小,比例 设置资源:设置url,AVPayer进入initialized状态设置窗口:获取并设置属性&&,用于设置显示画面【从XComponent组件中获取surfaceID】准备播放:调用prepare( ),AVPlayer进入prepared状态视频播放控制:播放play( ) 暂停pause( ) 跳转seek( ) 停止stop等操作(可选)更换资源:调用reset( )重置资源,AVPlayer重新进入idle状态,允许更换资源url退出播放:调用release( )销毁实例,AVPayer进入released状态,退出播放
样例
utils
import { media } from "@kit.MediaKit"
import { common } from "@kit.AbilityKit"
//视频横竖屏切换的播放工具类
export class AVPlayerUtil{
//窗口id 为将来画布渲染视频做准备
private surfaceId:string = ''
//为媒体源提供播放API
private avPlayer: media.AVPlayer | undefined = undefined
setSurfaceId(surfaceId:string){
this.surfaceId = surfaceId
}
//用于在屏幕宽高改变时获取比值
private callBack:Function = () => {}
//设置播放器状态机
setAVPlayCallback(avPlayer:media.AVPlayer){
avPlayer.on('stateChange',async (state:string) => {
switch (state){
case 'idle':
console.log(`播放状态为空闲`)
avPlayer.release()
break
case 'initialized':
console.log(`播放状态为初始化`)
//将变量中的id给予给视频播放窗口id属性
//先给窗口id在进入准备状态
avPlayer.surfaceId = this.surfaceId
avPlayer.prepare()
break
case 'prepared':
console.log(`播放状态为准备`)
//播放
avPlayer.play()
break
case 'playing':
console.log(`播放状态为播放中`)
break
case 'paused':
console.log(`播放状态为暂停`)
break
case 'completed':
console.log(`播放状态为完成播放`)
//再次播放
avPlayer.play()
break
case 'stopped':
console.log(`播放状态为完成停止`)
//重置播放状态
avPlayer.reset()
break
case 'released':
console.log(`播放状态为释放`)
break
default:
break
}
})
avPlayer.on('videoSizeChange',(width:number,height:number) => {
console.log(`>>>>视频宽高被改变,宽为${width},高为${height}`)
//将变化后的宽高比值保存住
this.callBack(height/width)
})
}
//初始化播放器
async initPlayer(url:string,callBack:Function){
this.avPlayer = await media.createAVPlayer()
this.callBack = callBack
//调用播放器状态函数,并传入状态实例
this.setAVPlayCallback(this.avPlayer)
//获取context
let context = getContext(this) as common.UIAbilityContext
//文件描述符, 用户获取rawfile/resource 目录下对应文件的信息
let fileDescriptor = await context.resourceManager.getRawFd(url)
//视频文件信息
let avFileDescriptor: media.AVFileDescriptor = {
//fd:资源句柄 通过resourceManager.getRawFd获取
fd:fileDescriptor.fd,
//资源偏移量
offset:fileDescriptor.offset,
//资源长度
length:fileDescriptor.length
}
//fdSrc 本地文件路径 此刻如果媒体资源路径正确,则状态机直接由闲置转为初始化
this.avPlayer.fdSrc = avFileDescriptor
console.log(`文件路径:${JSON.stringify(avFileDescriptor)}`)
}
}
pages
VideoPlayView
import { display, window } from "@kit.ArkUI"
import { AVPlayerUtil } from "../utils/AVPlayerUtil";
import { common } from "@kit.AbilityKit";
import { BusinessError } from "@kit.BasicServicesKit";
const context:Context = getContext(this)
@Component
export struct VideoPlayView{
//默认视频高度/宽度比值值
@State aspect:number = 9 / 16
//px2vp 将px单位的数值转换为以vp为单位的数值。
//display 屏幕属性提供管理显示设备的一些基础能力,包括获取默认显示设备的信息,获取所有显示设备的信息以及监听显示设备的插拔行为。
//获取当前默认的display对象。
//获取当前屏幕的宽度
@State xComponentWidth:number = px2vp(display.getDefaultDisplaySync().width)
//获取视频所需的高度 屏幕宽度 * 比例值
@State xComponentHeight: number = px2vp(display.getDefaultDisplaySync().width * this.aspect);
//是否处于全屏播放状态
@State isLandscape: boolean = false;
//录像回放是否被锁定
@State isVideoLock: boolean = false;
//控制中心开关旋转,值1:开启0:关闭
@State orientationLockState: string = '1';
//XComponent 提供用于图形绘制和媒体数据写入的外表,XComponent负责将其嵌入到视图中,支持应用自定义外表位置和大小。
//XComponent组件的控制器,可以将此对象绑定至XComponent组件,然后通过控制器来调用组件方法。
private xComponentController: XComponentController = new XComponentController();
//播放工具
private player?: AVPlayerUtil;
//获取该WindowStage实例下的主窗口。
private windowClass = (context as common.UIAbilityContext).windowStage.getMainWindowSync()
//是展开的还是半折叠的
isExpandedOrHalfFolded(): boolean {
//getFoldStatus 获取可折叠设备的当前折叠状态
//FOLD_STATUS_EXPANDED 表示设备当前折叠状态为完全展开
//FOLD_STATUS_HALF_FOLDED 表示设备当前折叠状态为半折叠。半折叠指完全展开和折叠之间的状态。
return display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED ||
display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_HALF_FOLDED
}
aboutToAppear(): void {
//应用窗口尺寸变化的监听
this.windowClass.on('windowSizeChange',(size) =>{
let viewWidth = px2vp(size.width);
let viewHeight = px2vp(size.height);
console.log(`>>>折叠状态${this.isExpandedOrHalfFolded()}`)
console.log(`>>>应用窗口宽度${viewWidth}`)
console.log(`>>>应用窗口高度${viewHeight}`)
if(this.isExpandedOrHalfFolded()){
this.xComponentWidth = viewWidth;
this.xComponentHeight = viewWidth * this.aspect;
}else {
if (viewWidth > viewHeight) {
this.xComponentWidth = viewHeight / this.aspect;
this.xComponentHeight = viewHeight;
this.isLandscape = true;
//隐藏底部导航栏
//setSpecificSystemBarEnabled 设置主窗口三键导航栏、状态栏、底部导航条的显示和隐藏
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', false);
}else{
this.xComponentHeight = viewWidth * this.aspect;
this.xComponentWidth = viewWidth;
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', true); // show bottom navigation bar
this.isLandscape = false;
}
}
})
}
//设置方向
setOrientation(orientation: number) {
//设置主窗口的显示方向属性,使用Promise异步回调
this.windowClass.setPreferredOrientation(orientation).then(() => {
console.log(`>>>>设置成功`)
}).catch((err: BusinessError) => {
console.log(`>>>>设置失败`)
});
}
build() {
Stack(){
//视频本体
Column(){
//提供用于图形绘制和媒体数据写入的表面,XComponent负责将其嵌入到视图中,支持应用自定义Surface位置和大小。
XComponent({
id: 'video_player_id',
//用于指定XComponent组件类型。 用于EGL/OpenGLES和媒体数据写入,开发者定制的绘制内容单独展示到屏幕上。背景色设置为黑色时会走显示子系统
type:XComponentType.SURFACE,
controller: this.xComponentController
})
//注册回调,加载完成后将会触发此回调。
.onLoad(() =>{
//创建视频播放工具对象
this.player = new AVPlayerUtil();
//getXComponentSurfaceId() : 获取XComponent对应Surface的ID,供@ohos接口使用
this.player.setSurfaceId(this.xComponentController.getXComponentSurfaceId());
this.player.initPlayer('videoTest.mp4', (aspect: number) => {
this.aspect = aspect;
this.xComponentHeight = px2vp(display.getDefaultDisplaySync().width * aspect);
this.xComponentWidth = px2vp(display.getDefaultDisplaySync().width);
});
})
.width(this.xComponentWidth)
.height(this.xComponentHeight)
}
//视频图标
RelativeContainer(){
//是否被锁定
if (!this.isVideoLock){
Image($r('app.media.icon_back'))
.height(24)
.width(24)
.margin({
left: 16,
top: this.isLandscape ? 0 : 12
})
.onClick(() => {
if (this.isExpandedOrHalfFolded()) {
this.isLandscape = false;
} else {
//Orientation 窗口显示方向类型枚举。 USER_ROTATION_PORTRAIT 调用时临时旋转到竖屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定
this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT);
}
})
}
//如果为全屏
if (this.isLandscape){
//是否被锁定
Image(this.isVideoLock ? $r('app.media.icon_lock') : $r('app.media.icon_lock_open'))
.height(24)
.width(24)
.fillColor(Color.White)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Center },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.margin({ left: (AppStorage.get
.offset({ y: -12 })
.onClick(() => {
//将锁定进行切换
this.isVideoLock = !this.isVideoLock;
if (this.isExpandedOrHalfFolded() || this.orientationLockState === '0') {
return;
}
if (this.isVideoLock) {
//跟随传感器自动横向旋转,可以旋转到横屏、反向横屏,无法旋转到竖屏、反向竖屏。
this.setOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE);
} else {
//跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定(如在某种设备,可以旋转到竖屏、横屏、反向横屏三个方向,无法旋转到反向竖屏)。
this.setOrientation(window.Orientation.AUTO_ROTATION_UNSPECIFIED);
}
})
}
//如果不为全屏
if(!this.isLandscape){
Image($r('app.media.icon_zoom_in'))
.height(24)
.width(24)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.margin({
right: 16,
bottom: 8
})
.onClick(() => {
if (this.isExpandedOrHalfFolded()) {
this.isLandscape = true;
} else {
//调用时临时旋转到横屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。
this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE);
}
})
}
}
.width('100%')
.height('100%')
}
.width('100%')
.height(this.isLandscape ? '100%' : this.xComponentHeight)
.backgroundColor(Color.Black)
}
}
VideoDetail
//视频详情页
import { VideoPlayView } from './VideoPlayView';
import { window } from '@kit.ArkUI';
@Entry
@Component
struct VideoDetail {
@State message: string = 'Hello World';
build() {
Column(){
//视频播放组件
VideoPlayView()
Scroll(){
Column(){
//视频相关推荐组件
//评论列表组件
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.padding({ bottom: 16 })
//底部操作栏组件
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
EntryAbility
// 在onWindowStageCreate中设置
//视频横竖屏播放。获取屏幕安全区域,并将安全区域高度存入AppStorage中
let windowClass:window.Window = windowStage.getMainWindowSync()
//getWindowAvoidArea 获取当前窗口内容规避区域,系统栏、刘海屏、手势、软键盘等可能与窗口内容重叠需要内容避让的区域
//TYPE_SYSTEM 系统默认区域 包含状态栏、导航栏等
let area:window.AvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
//将获取到的顶部避让区域的高度存入AppStorage
AppStorage.setOrCreate('statusBarHeight',px2vp(area.topRect.height))
结尾
功能实现思路
技术栈实现
VideoPlayView
获取windowStage实例下的主窗口
// 折叠屏相关
// 获取该windowStage实例下的主窗口
private windowClass = (context as common.UIAbilityContext).windowStage.getMainWindowSync()
应用窗口尺寸更新变化监听
this.windowClass.on('windowSizeChange',callBack)
设置主窗口三键导航栏,状态栏,底部导航条显示和隐藏
// false:不显示 true:显示
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', false)
AVplayerUtil
AVPlayer状态监听
avPlayer.on('stateChange',async(state:string)=>{})
AVPlayer视频尺寸监听
avPlayer.on('videoSizeChange',(width:number,height:number)=>{})
获取rawfile目录下的文件
// 获取context
let context = getContext(this) as common.UIAbilityContext
// 文件描述符,用户获取rawfile/resource 目录下对应文件的信息
// 找到该值是否存在,如果不存在使用网络路径 this.avPlayer.url
// context.resourceManager:访问应用资源的能力
let fileDescriptor = await context.resourceManager.getRawFd(url)
封装为视频文件信息
// 视频文件信息
// AVFileDescriptor音频文件资源描述
let avFileDescriptor:media.AVFileDescriptor = {
// 资源句柄,通过resourceManager.getRawFd获取
fd:fileDescriptor.fd,
// 资源偏移量
offset:fileDescriptor.offset,
// 资源长度
length:fileDescriptor.length,
}
this.avPlayer.fdSrc = avFileDescriptor
Descriptor = await context.resourceManager.getRawFd(url) ```
封装为视频文件信息
// 视频文件信息
// AVFileDescriptor音频文件资源描述
let avFileDescriptor:media.AVFileDescriptor = {
// 资源句柄,通过resourceManager.getRawFd获取
fd:fileDescriptor.fd,
// 资源偏移量
offset:fileDescriptor.offset,
// 资源长度
length:fileDescriptor.length,
}
this.avPlayer.fdSrc = avFileDescriptor