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

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

一. 申请权限

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

javascript 复制代码
  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 扫描本地视频列表,然后将视频相关信息封装起来

javascript 复制代码
 //获取本地视频列表
 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
javascript 复制代码
 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. 封装播放、暂停、停止等相关接口
javascript 复制代码
 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相关
javascript 复制代码
// 设置当前播放位置
  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. 设置播放路径
javascript 复制代码
 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. 设置相关播放状态监听
javascript 复制代码
 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
javascript 复制代码
  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.主界面布局
javascript 复制代码
 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布局
javascript 复制代码
@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相关
javascript 复制代码
  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. 播放、暂停相关
javascript 复制代码
// 点击播放暂停
  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简单的鸿蒙播放器就实现了

播放器效果图:
相关推荐
SameX1 小时前
HarmonyOS Next ohpm-repo私有仓库的配置与优化
前端·harmonyos
ChinaDragon1 小时前
HarmonyOS:DevEco Studio的使用
harmonyos
ChinaDragon1 小时前
HarmonyOS:声明式UI语法
harmonyos
林钟雪2 小时前
HarmonyNext实战案例:基于ArkTS的实时多人协作白板应用开发
harmonyos
轻口味4 小时前
【每日学点HarmonyOS Next知识】获取资源问题、软键盘弹起、swiper更新、C给图片设置位图、读取本地Json
c语言·json·harmonyos·harmonyosnext
林钟雪5 小时前
HarmonyNext 实战:基于 ArkTS 的高级跨设备数据同步方案
harmonyos
陈无左耳、7 小时前
HarmonyOS学习第18天:多媒体功能全解析
学习·华为·harmonyos
IT乐手7 小时前
2.6、媒体查询(mediaquery)
harmonyos
麦田里的守望者江7 小时前
Kotlin/Native 给鸿蒙使用(二)
kotlin·harmonyos
IT乐手7 小时前
2.5、栅格布局(GridRow/GridCol)
harmonyos