鸿蒙开发视频播放器和全屏横屏播放实现

566 阅读3分钟

刚开始学习鸿蒙开发,经验分享

最近有做到视频播放这一块的需求,在此记录,标有相应注释。

主要是avplayer和横竖屏事件,页面示例如下。

3805482746_157449442508_screenshot_20241023_165743.jpg

9b4b23e136fb03562ee6bafcd603349d.jpg

fd24fb0a79c8a3bb8ac1249c0476f53a.jpg

1.横竖屏事件代码

// 是否开启临时横屏事件,13-竖屏;14-横屏
setOrientation(orientation: number) {
  window.getLastWindow(getContext(this)).then((win) => {
    win.setPreferredOrientation(orientation).then((data) => {
      console.log('setWindowOrientation: ' + orientation + ' Succeeded. Data: ' + JSON.stringify(data));
    }).catch((err: string) => {
      console.log('setWindowOrientation: Failed. Cause: ' + JSON.stringify(err));
    });
  }).catch((err: string) => {
    console.log('setWindowOrientation: Failed to obtain the top window. Cause: ' + JSON.stringify(err));
  });
}

调用

if(this.isFull) {
  this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE)
} else {
  this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT)
}

2.创建和使用avplayer

播放视频可以用自带的video组件,也可以使用avplayer实现。

2.1定义参数

@State vol: number = 1 // 音量
@State sateVideoState: boolean = false
private  xComponentController: XComponentController = new XComponentController(); // 播放窗口
@State isFull: boolean = false // 是否全屏
@State showController:boolean = false // 是否展示播放器控制器
@State playing: boolean = true // 是否播放中
@State surfaceID: string = '';
@State videoSrc: string = ''//视频地址
@State videoTime:number = 0 // 当前播放时间
@State endTime: number = 0 // 视频总时长
//视频比例
@State videoProportion: number = 0
@State videoHeight: number = 1
@State videoWidth: number = 1
@State windowHeight: number = 1
@State windowWidth: number = 1

2.2创建视频播放器

// 播放器
private avPlayer: media.AVPlayer | null = null;
// avplayer状态机
private setAVPlayerCallback(avPlayer: media.AVPlayer) {
  console.log('状态机初始化')
  // startRenderFrame首帧渲染回调函数
  avPlayer.on('startRenderFrame', () => {
    console.info(`AVPlayer start render frame`);
  })
  // 音量
  avPlayer.on('volumeChange', (vol:number) => {
    this.vol=vol
  })
  // seek操作结果回调函数
  avPlayer.on('seekDone', async (seekDoneTime: number) => {
    console.info('zzz=== avPlayer seek操作',` seek time is ${seekDoneTime}`);
  })
  // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
  avPlayer.on('error', (err: BusinessError) => {
    console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
    avPlayer.reset(); // 调用reset重置资源,触发idle状态
    emitter.emit('VedioError')
  })
  // 时间更新 监听资源播放当前时间,单位为毫秒(ms),用于刷新进度条当前位置,默认间隔100ms时间上报,因用户操作(seek)产生的时间变化会立刻上报。
  avPlayer.on('timeUpdate', async (time:number)=>{
    this.videoTime = time
  })
  // 状态机变化回调函数
  avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
    console.log('zzz=== avPlayer状态变化', `当前状态${state}`)
    switch (state) {
      case 'idle': // 成功调用reset接口后触发该状态机上报
        console.info('AVPlayer state idle called.');
        break;
      case 'initialized': // avplayer 设置播放源后触发该状态上报
        console.log('avPlayer状态机',"设置播放源后")
        avPlayer.url
        avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
        //监听
        emitter.emit('VideoInitialized')
        avPlayer.prepare();
        break;
      case 'prepared': // prepare调用成功后上报该状态机已准备状态,在initialized状态调用prepare()方法,AVPlayer会进入prepared状态,此时播放引擎的资源已准备就绪。
        console.log('avPlayer状态机','进入准备状态');
        this.endTime = avPlayer.duration
        avPlayer.videoScaleType = 1
        avPlayer.setVolume(1)
        this.VideoSize(this.windowHeight,this.windowWidth)
        // this.listenVieoVoice()
        this.doPlay()
        break;
      case 'playing': // play成功调用后触发该状态机上报
        console.info('AVPlayer state playing called.');
        this.playing = true
        //监听
        emitter.emit('VideoPlaying')
        break;
      case 'paused': // pause成功调用后触发该状态机上报
        console.info('AVPlayer state paused called.');
        this.playing = false
        //监听
        emitter.emit('VideoPaused')
        break;
      case 'completed': // 播放结束后触发该状态机上报
        console.info('AVPlayer state completed called.');
        this.playing = false
        //监听
        emitter.emit('VideoCompleted')
        break;
      case 'stopped': // stop接口成功调用后触发该状态机上报
        console.info('AVPlayer state stopped called.');
        avPlayer.reset(); // 调用reset接口初始化avplayer状态
        //监听
        emitter.emit('VideoStopped')
        break;
      case 'released':
        console.info('AVPlayer state released called.');
        break;
      default:
        console.info('AVPlayer state unknown called.');
        break;
    }
  })
  //视频尺寸变化
  avPlayer.on('videoSizeChange',(width:number,height:number)=>{
    this.videoProportion = (width/height)
    console.log( 'myTag', '获取视频比例', this.videoProportion)
    this.VideoSize(this.windowHeight,this.windowWidth)
  })
}

2.3视频播放相关方法

横竖屏变化修改视频窗口宽高

//视频比例适应
VideoSize(height: number, width: number) {
  console.log('myTag', '视频比例调整')
  console.log('视频高',height);
  this.videoHeight = height
  if(this.isFull) {
    this.videoWidth = height*this.videoProportion
  } else {
    this.videoWidth = width
  }
}

播放暂停

async doPlay(){
  let mp = this.avPlayer
  console.log('zzz=== doPlay方法',`mp状态:${mp?.state};`)
  if (mp?.state && (mp?.state == "prepared" || mp?.state == "paused" || mp?.state == "completed")) {
    mp.play()
  } else {
    // ToastUtil.showToast('视频播放出现了点问题')
  }
}

async doPause(){
  let mp = this.avPlayer
  console.log('zzz=== doPause方法',`mp状态:${mp?.state};`)
  if (mp?.state == 'playing' ){
    mp.pause()
  }
}

2.4页面结构

2.4.1播放窗口
build() {
  NavDestination() {
    Column() {
      Stack({alignContent: Alignment.Top}) {
        // 视频播放组件
        XComponent({type:XComponentType.SURFACE,controller:this.xComponentController})
          .onLoad(async e => {
            this.surfaceID = this.xComponentController.getXComponentSurfaceId()
          })
          .height(this.videoHeight).width(this.videoWidth)
          .zIndex(11)
          .onClick(()=> {
            animateTo({ duration: 600 }, () => {
              this.showController = !this.showController
              // 2秒后隐藏控件
              setTimeout(()=> {
                this.changeController()
              }, 2000)
            })
          })
      }
      .width('100%').zIndex(11)
      .backgroundColor('#333333')
      .height(this.isFull ? '100%' : '422lpx')
      .onAreaChange((oldValue: Area, newValue: Area) => {
        this.windowHeight = Number(newValue.height)
        this.windowWidth = Number(newValue.width)
        this.VideoSize(Number(newValue.height), Number(newValue.width))
      })
    }
    .width('100%')
    .height('100%')
    .padding({top: this.isFull ? '0lpx' : '80lpx', bottom: '0lpx'})
    .backgroundColor(Color.White)
  }
  .hideTitleBar(true)
  .onHidden(()=>{
    this.onPageHide()
  })
  .onReady((context: NavDestinationContext) => {

  })
}
2.4.2播放控件以及对应事件
// 播放器自定义控件
if(this.showController) {
  Row() {
    Image($r('app.media.ic_arrow_left_02')).width('44lpx').onClick(()=>{
      if(this.isFull) {
        this.isFull = !this.isFull
        this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT)
      } else {
        this.pathStack.pop()
      }
      // this.pathStack.pop()
    })
  }.padding({left: '25lpx', right: '25lpx', top: '25lpx'}).width('100%')
  .zIndex(11)
  .transition({ type: TransitionType.Delete, opacity: 0 })
  Row() {
    if(this.playing) {
      Image($r('app.media.ic_play_on')).width('36lpx').margin({right: '24lpx'}).onClick(()=> {
        this.playing = false
        this.doPause()
      })
    } else {
      Image($r('app.media.ic_play_off')).width('36lpx').margin({right: '24lpx'}).onClick(()=> {
        this.playing = true
        this.doPlay()
      })
    }
    Text(this.getTimeString(this.videoTime)).fontColor(Color.White).fontSize('22lpx')
    Stack() {
      Slider({
        value:this.videoTime,
        min:0,
        max:this.endTime,
        step: 0.01,
      })
        .width('438lpx')
        .videoSlider()
        .showSteps(false)
          //控制播放进度
        .onChange((value: number, mode: SliderChangeMode) => {
          if (mode==0) {
            console.log('zzz=== 进度条点击事件')
            this.sateVideoState = this.playing
            this.doPause()
          }
          if (mode==1) {
            this.avPlayer?.seek(value,media.SeekMode.SEEK_CLOSEST)
          }
          if (mode==2){
            console.log('zzz=== 进度条松开事件')
            this.avPlayer?.seek(value,media.SeekMode.SEEK_CLOSEST)
            if (this.sateVideoState){
              this.doPlay()
            }
          }
        })
      // Column().width('438lpx').height('4lpx').borderRadius('2lpx').backgroundColor('#fff')
    }
    Text(this.getTimeString(this.endTime)).fontColor(Color.White).fontSize('22lpx')
    Image($r('app.media.ic_full_screen')).width('36lpx').margin({left: '24lpx'}).onClick(()=> {
      console.log('全屏事件')
      if (this.avPlayer) {
        this.isFull = !this.isFull
        if(this.isFull) {
          this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE)
        } else {
          this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT)
        }
      }
    })
  }.width('100%').height('68lpx').position({bottom: 0, left: 0})
  .backgroundColor('rgba(23,23,26,0.8)').alignItems(VerticalAlign.Center)
  .padding({left: '24lpx', right: '24lpx'})
  .justifyContent(FlexAlign.SpaceBetween)
  .zIndex(11)
  .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: '0lpx', x: 0 } })
  .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: '0lpx', x: 0 } })
}
2.4.3播放进度条样式
// 进度条样式
@Extend(Slider)
function videoSlider() {
  .trackColor('#ffffff')
  .trackThickness('5lpx')
  .selectedColor('#2ACF6F')
  .blockBorderColor('rgba(44, 211, 215, 0.3)')
  .blockBorderWidth('8lpx')
  .blockColor('#2CD3D7')
  .blockSize({ width: '25lpx', height: '25lpx' })
}

2.5初始化并播放

async  aboutToAppear() {
  this.avPlayer = await media.createAVPlayer();
  this.setAVPlayerCallback(this.avPlayer);
  setTimeout(()=>{
    this.init()
  },300)
}
init() {
  if(this.avPlayer) {
    this.avPlayer.url = "http:www.abc.link.mp4" // 赋值你的视频地址
    this.doPlay() // 执行播放
  }
}

注意事项

退出页面需要暂停播放器

async onPageHide() {
  this.doPause()
  this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT) // 回复竖屏
}

切换视频源,也需要重置播放状态

this.videoPlay = false
// 切换视频,对播放器初始化
this.doPause()
this.avPlayer?.reset()
this.avPlayer.url = "http:www.abc.link.mp4" // 重新赋值地址
setTimeout(()=>{
  this.init()
},300)