鴻蒙系統(tǒng)下使用AVPlay開(kāi)發(fā)一款視頻播放器流程
一. 申請(qǐng)權(quán)限
申請(qǐng)相關(guān)權(quán)限,主要是讀取存儲(chǔ)卡權(quán)限,方便后面掃描視頻用:
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()
}
})
}
二. 獲取本地視頻數(shù)據(jù)
使用 phAccessHelper 掃描本地視頻列表,然后將視頻相關(guān)信息封裝起來(lái)
//獲取本地視頻列表
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+ '| 視頻時(shí)長(zhǎng): '+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相關(guān)接口
初始化AVPlay,并封裝相關(guān)接口,建議單獨(dú)封裝一個(gè)AVPlayViewModel,處理視頻相關(guān)業(yè)務(wù)
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. 封裝播放、暫停、停止等相關(guān)接口
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相關(guān)
// 設(shè)置當(dāng)前播放位置
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. 設(shè)置播放路徑
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. 設(shè)置相關(guān)播放狀態(tài)監(jiān)聽(tīng)
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) => { //設(shè)置'timeUpdate'事件回調(diào)
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. 設(shè)置播放相關(guān)監(jiān)聽(tīng)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: // 成功調(diào)用reset接口后觸發(fā)該狀態(tài)機(jī)上報(bào)
LogUtil.info('AVPlayer state idle called.');
// avPlayer.release(); // 調(diào)用release接口銷(xiāo)毀實(shí)例對(duì)象
break;
case AvplayerStatus.INITIALIZED: // avplayer 設(shè)置播放源后觸發(fā)該狀態(tài)上報(bào)
LogUtil.info('AVPlayer state initialized called. surfaceID: ' + this.surfaceID);
avPlayer.surfaceId = this.surfaceID; // 設(shè)置顯示畫(huà)面,當(dāng)播放的資源為純音頻時(shí)無(wú)需設(shè)置
avPlayer.prepare();
break;
case AvplayerStatus.PREPARED: // prepare調(diào)用成功后上報(bào)該狀態(tài)機(jī)
LogUtil.info('AVPlayer state prepared called.');
this.duration = avPlayer.duration
// this.play(); // 調(diào)用播放接口開(kāi)始播放
LogUtil.info('video duration; ' + this.duration)
break;
case AvplayerStatus.PLAYING: // play成功調(diào)用后觸發(fā)該狀態(tài)機(jī)上報(bào)
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 {
// 當(dāng)播放模式不支持seek操作時(shí)繼續(xù)播放到結(jié)尾
LogUtil.info('AVPlayer wait to play end.');
}
} else {
// avPlayer.pause(); // 調(diào)用暫停接口暫停播放
}
this.count++;
break;
case AvplayerStatus.PAUSED: // pause成功調(diào)用后觸發(fā)該狀態(tài)機(jī)上報(bào)
LogUtil.info('AVPlayer state paused called.');
// avPlayer.play(); // 再次播放接口開(kāi)始播放
break;
case AvplayerStatus.COMPLETED: // 播放結(jié)束后觸發(fā)該狀態(tài)機(jī)上報(bào)
LogUtil.info('AVPlayer state completed called.');
// this.stop()
break;
case AvplayerStatus.STOPPED: // stop接口成功調(diào)用后觸發(fā)該狀態(tài)機(jī)上報(bào)
LogUtil.info('AVPlayer state stopped called.');
this.reset(); // 調(diào)用reset接口初始化avplayer狀態(tài)
break;
case AvplayerStatus.RELEASED:
LogUtil.info('AVPlayer state released called.');
break;
default:
LogUtil.info('AVPlayer state unknown called.');
break;
}
})
三. 繪制頁(yè)面,使用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(() => {
//設(shè)置surfaceID
this.surfaceID = this.mXComponentController.getXComponentSurfaceId()
mVideoPlayVM.setSurfaceID(this.surfaceID)
})
if (this.isLand) {
Blank()
}
}.justifyContent(FlexAlign.Start)
.width(CommonConstants.FULL_PERCENT)
}
3. seek相關(guān)
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. 播放、暫停相關(guān)
// 點(diǎn)擊播放暫停
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)
})
}
}
最后處理一些細(xì)節(jié),比如進(jìn)度條、音量、異常等,一個(gè)基于AVPlay簡(jiǎn)單的鴻蒙播放器就實(shí)現(xiàn)了
播放器效果圖:

image.png