鸿蒙系统下使用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相关接口
初始化AVPlay,并封装相关接口,建议单独封装一个AVPlayViewModel,处理视频相关业务
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简单的鸿蒙播放器就实现了