鸿蒙系统下使用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 = true
data.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).size
if (fileAsset.photoType == photoAccessHelper.PhotoType.VIDEO) {
let mVideoFile = new VideoFile()
mVideoFile.fileFD = fd
mVideoFile.fileSize = size
let filePath = this.getFileNamePath(fileAsset.uri) + fileAsset.displayName
mVideoFile.filePath = filePath
mVideoFile.uri = fileAsset.uri
PersistentStorage.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.photoType
mVideoFile.photoType = 'video/mp4'
mVideoFile.videoWidth = fileAsset.get(photoAccessHelper.PhotoKeys.WIDTH) as number
mVideoFile.videoHeight = fileAsset.get(photoAccessHelper.PhotoKeys.HEIGHT) as number
mVideoFile.size = fileAsset.get(photoAccessHelper.PhotoKeys.SIZE) as Number
mVideoFile.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 = video
this.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 = state
if (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布局
@Builder
video() {
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 = value
this.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 = true
this.seek(0)
})
}
}
最后处理一些细节,比如进度条、音量、异常等,一个基于AVPlay简单的鸿蒙播放器就实现了