HarmonyOS NEXT鴻蒙系統(tǒng)下使用AVPlay播放視頻,封裝播放器

鴻蒙系統(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容