鸿蒙系统下使用AVPlay播放视频,封装播放器

embedded/2024/11/22 10:16:58/

AVPlay_0">鸿蒙系统下使用AVPlay开发一款视频播放器流程

一. 申请权限

申请相关权限,主要是读取存储卡权限,方便后面扫描视频用:

getPermission(): void {let array: Array<Permissions> = ['ohos.permission.WRITE_DOCUMENT','ohos.permission.READ_DOCUMENT','ohos.permission.READ_MEDIA','ohos.permission.WRITE_MEDIA','ohos.permission.MEDIA_LOCATION','ohos.permission.READ_IMAGEVIDEO','ohos.permission.WRITE_IMAGEVIDEO','ohos.permission.DISTRIBUTED_DATASYNC','ohos.permission.DISTRIBUTED_SOFTBUS_CENTER',];let context = this.context;let atManager = abilityAccessCtrl.createAtManager();atManager.requestPermissionsFromUser(context, array).then((data) => {let isAgreeAllPermissions = truedata.authResults.forEach((result: number) => {if (result != 0) {isAgreeAllPermissions = false}})if (isAgreeAllPermissions) {this.updatePlayStatus()}})}

二. 获取本地视频数据

使用 phAccessHelper 扫描本地视频列表,然后将视频相关信息封装起来

 //获取本地视频列表async getRawFileList(callback: Function) {let videoListSrc: Array<VideoFile> = []const context = getContext(this);let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);// console.log('console is  == phAccessHelper', JSON.stringify(phAccessHelper))let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();let fetchOptions: photoAccessHelper.FetchOptions = {// fetchColumns: [],fetchColumns: [photoAccessHelper.PhotoKeys.SIZE,photoAccessHelper.PhotoKeys.DATE_ADDED,photoAccessHelper.PhotoKeys.DATE_MODIFIED,photoAccessHelper.PhotoKeys.POSITION,photoAccessHelper.PhotoKeys.WIDTH,photoAccessHelper.PhotoKeys.HEIGHT,],predicates: predicates};phAccessHelper.getAssets(fetchOptions, async (err, fetchResult) => {if (fetchResult != undefined) {let sortList: Array<string> = []for (let i = 0; i < fetchResult.getCount(); i++) {let fileAsset: photoAccessHelper.PhotoAsset = await fetchResult.getNextObject();if (fileAsset == undefined) {continue}await fileAsset.open('r').then((fd: number) => {let size = fs.statSync(fd).sizeif (fileAsset.photoType == photoAccessHelper.PhotoType.VIDEO) {let mVideoFile = new VideoFile()mVideoFile.fileFD = fdmVideoFile.fileSize = sizelet filePath = this.getFileNamePath(fileAsset.uri) + fileAsset.displayNamemVideoFile.filePath = filePathmVideoFile.uri = fileAsset.uriPersistentStorage.persistProp(filePath,0)mVideoFile.duration = AppStorage.get(filePath) as number// LogUtil.info('读取的key: '+filePath+ '| 视频时长: '+mVideoFile.duration)mVideoFile.displayName = this.getShowFileName(fileAsset.displayName)// mVideoFile.photoType = fileAsset.photoTypemVideoFile.photoType = 'video/mp4'mVideoFile.videoWidth = fileAsset.get(photoAccessHelper.PhotoKeys.WIDTH) as numbermVideoFile.videoHeight = fileAsset.get(photoAccessHelper.PhotoKeys.HEIGHT) as numbermVideoFile.size = fileAsset.get(photoAccessHelper.PhotoKeys.SIZE) as NumbermVideoFile.dimensions = fileAsset.get(photoAccessHelper.PhotoKeys.WIDTH).toString() + 'x' + fileAsset.get(photoAccessHelper.PhotoKeys.HEIGHT).toString()videoListSrc.push(mVideoFile)sortList.push(fileAsset.displayName)}})}if (callback != null) {callback(videoListSrc)}}});}

AVPlay_111">三.封装AVPlay相关接口

初始化AVPlay,并封装相关接口,建议单独封装一个AVPlayViewModel,处理视频相关业务

AVPlay_113">1、 初始化AVPlay
 initAVPlay() {media.createAVPlayer((error: BusinessError, video: media.AVPlayer) => {if (video != null) {this.avPlayer = video;avPlayer = videothis.setAVPlayerCallBack(this.avPlayer)this.setScreenOnWhilePlaying(true)} else {}});}
2. 封装播放、暂停、停止等相关接口
 prepared(): Promise<void> {return this.avPlayer.prepare();}start() {this.avPlayer.play()}play() {this.avPlayer.play()}pause(): Promise<void> {return this.avPlayer.pause()}stop(): Promise<void> {return this.avPlayer.stop();}reset(): Promise<void> {return this.avPlayer.reset()}release() {this.avPlayer.release()}isPlaying() {return this.mCurrentPlayStatus == AvplayerStatus.PLAYING}getDuration(): number {return this.avPlayer.duration}
3. seek相关
// 设置当前播放位置setSeekTime(value: number) {this.seekTime = value * this.duration / CommonConstants.ONE_HUNDRED;if (this.avPlayer !== null) {this.avPlayer.seek(value, media.SeekMode.SEEK_NEXT_SYNC);}}
4. 设置播放路径
 async setDataSrc(fileSize: number, fileFD: number) {let src: media.AVDataSrcDescriptor = {fileSize: fileSize,callback: (buf: ArrayBuffer, length: number, pos: number | undefined) => {let num = 0;if (buf == undefined || length == undefined || pos == undefined) {return -1;}num = fileIo.readSync(fileFD, buf, { offset: pos, length: length });if (num > 0 && (fileSize >= pos)) {return num;}return -1;}}this.isSeek = true; // 支持seek操作avPlayer.dataSrc = src;}
5. 设置相关播放状态监听
 setOnSeekCompleteListener(listener: OnSeekCompleteListener) {this.avPlayer.on('seekDone', (seekDoneTime: number) => {listener.onSeekComplete()})}setOnErrorListener(listener: OnErrorListener): void {this.avPlayer.on('error', (err: BusinessError) => {listener.onError(err.code, err.message)});}setOnDurationUpdateListener(listener: OnDurationUpdateListener) {avPlayer.on('durationUpdate', (duration: number) => {listener.onDurationUpdate(duration)})}setOnTimeUpdateListener(listener: OnTimedTextListener) {this.avPlayer.on('timeUpdate', (seekDoneTime: number) => { //设置'timeUpdate'事件回调if (seekDoneTime == null) {return;}listener.onTimedText(seekDoneTime + '')});}setOnVideoSizeChangeListener(listener: OnVideoSizeChangedListener): void {this.avPlayer.on('videoSizeChange', (width: number, height: number) => {listener.onVideoSizeChanged(width, height)})}setOnStartRenderFrameListener(listener: OnTimedTextListener) {this.avPlayer.on('startRenderFrame', () => {});}
6. 设置播放相关监听Callback
  avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {this.mCurrentPlayStatus = stateif (this.mOnStateChangeListener != null) {this.mOnStateChangeListener.onStateChange(state)}switch (state) {case AvplayerStatus.IDLE: // 成功调用reset接口后触发该状态机上报LogUtil.info('AVPlayer state idle called.');// avPlayer.release(); // 调用release接口销毁实例对象break;case AvplayerStatus.INITIALIZED: // avplayer 设置播放源后触发该状态上报LogUtil.info('AVPlayer state initialized called.  surfaceID: ' + this.surfaceID);avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置avPlayer.prepare();break;case AvplayerStatus.PREPARED: // prepare调用成功后上报该状态机LogUtil.info('AVPlayer state prepared called.');this.duration = avPlayer.duration// this.play(); // 调用播放接口开始播放LogUtil.info('video duration; ' + this.duration)break;case AvplayerStatus.PLAYING: // play成功调用后触发该状态机上报LogUtil.info('AVPlayer state playing called.');if (this.count !== 0) {if (this.isSeek) {LogUtil.info('AVPlayer start to seek.');// avPlayer.seek(avPlayer.duration); //seek到视频末尾} else {// 当播放模式不支持seek操作时继续播放到结尾LogUtil.info('AVPlayer wait to play end.');}} else {// avPlayer.pause(); // 调用暂停接口暂停播放}this.count++;break;case AvplayerStatus.PAUSED: // pause成功调用后触发该状态机上报LogUtil.info('AVPlayer state paused called.');// avPlayer.play(); // 再次播放接口开始播放break;case AvplayerStatus.COMPLETED: // 播放结束后触发该状态机上报LogUtil.info('AVPlayer state completed called.');// this.stop()break;case AvplayerStatus.STOPPED: // stop接口成功调用后触发该状态机上报LogUtil.info('AVPlayer state stopped called.');this.reset(); // 调用reset接口初始化avplayer状态break;case AvplayerStatus.RELEASED:LogUtil.info('AVPlayer state released called.');break;default:LogUtil.info('AVPlayer state unknown called.');break;}})

三. 绘制页面,使用XComponent渲染视频

1.主界面布局
 build() {Column() {Stack() {Column() {this.video()}.justifyContent(this.isLand ? FlexAlign.Center : FlexAlign.Start).padding({ top: this.isLand ? 0 : 50 }).height(CommonConstants.FULL_PERCENT)if (this.isLand) {this.LandScreenView() //横屏} else {this.VerticalScreenView() //竖屏}this.buildLoading()}.backgroundColor($r('app.color.black')).height(CommonConstants.FULL_PERCENT).width(CommonConstants.FULL_PERCENT)}.backgroundColor($r('app.color.black')).height(CommonConstants.FULL_PERCENT).width(CommonConstants.FULL_PERCENT)}
2. 视频ivideo布局
@Buildervideo() {Row() {XComponent({id: 'xComponentId',type: XComponentType.SURFACE,libraryname: 'nativerender',controller: this.mXComponentController}).width(this.isLand ? this.isVideoFullScreen ? '100%' : '75%' : CommonConstants.FULL_PERCENT).height(this.isLand ?this.isVideoFullScreen ? mScreenUtils.getScreenWidth() * this.videoHeight / this.videoWidth : mScreenUtils.getScreenWidth() * 0.75 * this.videoHeight / this.videoWidth :mScreenUtils.getScreenWidth() * this.videoHeight / this.videoWidth).onLoad(() => {//设置surfaceID this.surfaceID = this.mXComponentController.getXComponentSurfaceId()mVideoPlayVM.setSurfaceID(this.surfaceID)})if (this.isLand) {Blank()}}.justifyContent(FlexAlign.Start).width(CommonConstants.FULL_PERCENT)}
3. seek相关
  Slider({ value: this.currentProgress, min: 0, max: this.duration }).layoutWeight(1).trackColor('#eeeeee').selectedColor('#ff0c4ae7').onChange(this.sliderChangeCallback)sliderChangeCallback = (value: number, mode: SliderChangeMode) => {this.stopProgressTask();this.currentProgress = value;LogUtil.info(`currentprogress: ${this.currentProgress}`)if (mode === SliderChangeMode.End || mode === SliderChangeMode.Moving) {if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PREPARED ||mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PLAYING ||mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PAUSED) {this.seek(value)} else if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.IDLE) {this.tempOnStopSeekValue = valuethis.onPlayClick()} else if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.COMPLETED) {this.seek(value)this.startPlayOrResumePlay()}}}
4. 播放、暂停相关
// 点击播放暂停onPlayClick() {LogUtil.info(`onPlayClick isPlaying= ${this.isPlaying}`)if (this.isPlaying) {this.pause()} else {this.startPlayOrResumePlay()}}private startPlayOrResumePlay() {this.mDestroyPage = false;this.videoPlayStateImage = $r('app.media.icon_video_pause')this.stopProgressTask();this.startProgressTask();this.stopHideVideoControlViewTask()this.isPlaying = true;if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.IDLE) {this.play();}if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PAUSED ||mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.COMPLETED) {mVideoPlayVM.start();}}//播放private play() {this.showLoadIng()this.setListener()if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.INITIALIZED) {mVideoPlayVM.reset().then(() => {mVideoPlayVM.setDataSrc(this.fileSize, this.fileFD)})} else {mVideoPlayVM.setDataSrc(this.fileSize, this.fileFD)}}//停止private stop() {if (mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PREPARED ||mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PLAYING ||mVideoPlayVM.getCurrentPlayState() == AvplayerStatus.PAUSED) {this.isClickStopSeek = truethis.seek(0)})}}

最后处理一些细节,比如进度条、音量、异常等,一个基于AVPlay简单的鸿蒙播放器就实现了

播放器效果图:

在这里插入图片描述


http://www.ppmy.cn/embedded/139593.html

相关文章

django从入门到实战(一)——路由的编写规则与使用

Django 路由的编写规则与使用 在 Django 中&#xff0c;路由&#xff08;URLconf&#xff09;是将 URL 映射到视图函数的机制。它允许我们定义网站的 URL 结构&#xff0c;并将请求分发到相应的处理函数。以下是关于 Django 路由的定义规则及使用的详细介绍。 1. Django 的路…

如何更改手机GPS定位

你是否曾想过更改手机GPS位置以保护隐私、玩游戏或访问受地理限制的内容&#xff1f;接下来我将向你展示如何使用 MagFone Location Changer 更改手机GPS 位置&#xff01;无论是在玩Pokmon GO游戏、发布社媒贴子&#xff0c;这种方法都快速、简单且有效。 第一步&#xff1a;下…

word设置交叉引用快捷键和居中快捷键

1Word 设置页码从指定页开始的详细步骤&#xff01; - 知乎 2居中快捷键是CTRLE。 3word页码从正文是1&#xff1f; 首先把光标移到正文开始处&#xff0c;然后【布局】-“分割符”-“下一页”&#xff0c;点一下。 然后就可以设置页码从指定页开始计数1234。。。但是需要注…

SpringBoot,IOC,DI,分层解耦,统一响应

目录 详细参考day05 web请求 1、BS架构流程 2、RequestParam注解 完成参数名和形参的映射 3、controller接收json对象&#xff0c;使用RequestBody注解 4、PathVariable注解传递路径参数 5、ResponseBody&#xff08;return 响应数据&#xff09; RestController源码 6、统一响…

鸿蒙学习高效开发与测试-ArkUI 框架(2)

文章目录 1、声明式开发范式2、类 Web 开发范式3、可视可说 ArkUI 是鸿蒙生态原生的 UI 开发框架。主体结构如下图所示&#xff1a; ArkUI 框架提供给开发者两种开发方式&#xff1a;基于 ArkTS 的声明式开发范式和基于 JS 扩展的类 Web 开发范式。声明式开发范式更加简洁高效…

贴代码框架PasteForm特性介绍之select,selects,lselect和reload

简介 PasteForm是贴代码推出的 “新一代CRUD” &#xff0c;基于ABPvNext&#xff0c;目的是通过对Dto的特性的标注&#xff0c;从而实现管理端的统一UI&#xff0c;借助于配套的PasteBuilder代码生成器&#xff0c;你可以快速的为自己的项目构建后台管理端&#xff01;目前管…

详解Rust的数据类型和语法

文章目录 基本数据类型复杂数据类型字符串基本语法 Rust是一种强调安全性和性能的系统编程语言。它的设计目标之一是防止内存安全错误同时提供丰富的功能和灵活的语法。下面介绍一下Rust语言的基本数据类型和语法。 基本数据类型 1.整数类型 有符号整数: i8, i16, i32, i64, i…

设计模式:4、命令模式(双重委托)

目录 0、定义 1、命令模式包括四种角色 2、命令模式的UML类图 3、代码示例 0、定义 将一个请求封装为一个对象&#xff0c;从而使用户可用不同的请求对客户进行参数化&#xff1b;对请求排队或记录请求日志&#xff0c;以及支持可撤销的操作。 1、命令模式包括四种角色 接…