鸿蒙Next使用AVPlayer播放视频 原创 精华

auhgnixgnahz
发布于 2025-7-11 21:49
浏览
0收藏

AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。
本文介绍一下AVPlayer的基本使用,播放网络视频,实现播放暂停功能、进度条拖拽功能,看一下演示效果:
鸿蒙Next使用AVPlayer播放视频-鸿蒙开发者社区

基础概念介绍:

当使用AVPlayer开发视频应用播放视频时,AVPlayer与外部模块的交互关系如图所示:
鸿蒙Next使用AVPlayer播放视频-鸿蒙开发者社区
支持的协议如下:

协议类型 协议格式描述
本地点播 支持file descriptor,禁止file path
网络点播 支持http/https/hls/dash
网络直播 支持hls/http-flv

支持的视频播放格式主流分辨率如下:

视频容器规格 规格描述 分辨率
mp4 视频格式:H265/H264 主流分辨率,如4K/1080P/720P/480P/270P
mkv 视频格式:H265/H264 主流分辨率,如4K/1080P/720P/480P/270P
ts 视频格式:H265/H264 主流分辨率,如4K/1080P/720P/480P/270P

播放的全流程包含:

步骤 方法
创建AVPlayer media.createAVPlayerr(callback: AsyncCallback<AVPlayer>): void
设置播放资源和窗口 url/fdSrc XComponent.onLoad(callback: OnNativeLoadCallback )
设置播放参数(音量/倍速/缩放模式) setVolume、setVolume、videoScaleType
播放控制(播放/暂停/跳转/停止) 播放play(),暂停pause(),跳转seek(),停止stop()
重置 reset()
销毁资源 release()

播放状态变化示意图
鸿蒙Next使用AVPlayer播放视频-鸿蒙开发者社区
AVPlayer监听回调事件

事件类型 说明
stateChange 必要事件,监听播放器的state属性改变。
error 必要事件,监听播放器的错误信息。
durationUpdate 用于进度条,监听进度条长度,刷新资源时长。
timeUpdate 用于进度条,监听进度条当前位置,刷新当前时间。
seekDone 响应API调用,监听seek()请求完成情况。
speedDone 响应API调用,监听setSpeed()请求完成情况。
volumeChange 响应API调用,监听setVolume()请求完成情况。
bitrateDone 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。
availableBitrates 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。
bufferingUpdate 用于网络播放,监听网络播放缓冲信息。
startRenderFrame 用于视频播放,监听视频播放首帧渲染时间。
videoSizeChange 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。
audioInterrupt 监听音频焦点切换信息,搭配属性audioInterruptMode使用。

源码:

import { media } from '@kit.MediaKit';
import { display } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { TimeFormatUtil } from '../utils/TimeFormatUtil';
import { AlignRules } from '../utils/AlignRules';

@Entry
@ComponentV2
struct AVPlayerTest{
  private xComponentController: XComponentController = new XComponentController();
  private avPlayer: media.AVPlayer | null = null;
  private surfaceID: string = '';
  private sliderClickValue:number=0  //记录点击滑块时的进度值
  @Local XComponentHeight: number = display.getDefaultDisplaySync().height;
  @Local XComponentWidth: number = display.getDefaultDisplaySync().width;
  @Local isSliderMoving: boolean = false;
  @Local isPlaying: boolean = true;
  @Local currentTime: number = 0;// 视频当前时间
  @Local durationTime: number = 0;  // 视频总时长
  @Local currentStringTime: string = '00:00';
  @Local durationStringTime: string = '00:00';
  //创建AVPlayer
  async initAVPlayer() {
    media.createAVPlayer().then((video: media.AVPlayer) => {
      if (video === undefined) {
        return;
      }
      this.avPlayer = video;
      this.setAVPlayerCallback(this.avPlayer);
      this.avPlayer.url='http://t-cdn.kaiyanapp.com/1751377904927_21de26f9.mp4'
    })
  }
  //avPlayer 回调函数
  setAVPlayerCallback(avPlayer: media.AVPlayer) {
    //监听seek生效的事件
    avPlayer.on('seekDone', (seekDoneTime) => {
      console.info(`setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    });
    avPlayer.on('speedDone', (speed) => {
      console.info(`setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
    });
    //监听资源播放当前时间,单位为毫秒(ms)
    avPlayer.on('timeUpdate', (time: number) => {
      if (!this.isSliderMoving) {
        this.currentTime =time;
        this.currentStringTime = TimeFormatUtil.formatToHMS(this.currentTime);
      }
    })
    //监听视频播放宽高变化事件
    avPlayer.on('videoSizeChange', (width: number, height: number) => {
      console.info(`setAVPlayerCallback AVPlayer videoSizeChange, width is ${width} height is ${height}  `);
      display.getAllDisplays((err, data) => {
        let screenWidth = data[0].width;
        let screenHeight = data[0].width;
        let videoRatio: number = Number(width) / Number(height);
        let screenRatio = screenWidth / screenHeight;
        if (videoRatio > screenRatio) {
          this.XComponentWidth = this.getUIContext().px2vp(screenWidth);
          this.XComponentHeight = this.getUIContext().px2vp(height * screenWidth / width);
        } else {
          this.XComponentWidth = this.getUIContext().px2vp(width * screenHeight / height);
          this.XComponentHeight = this.getUIContext().px2vp(screenHeight);
        }
      });
    })

    //监听错误事件,该事件仅用于错误提示,不需要用户停止播控动作
    avPlayer.on('error', (err: BusinessError) => {
      console.info(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}.` +
        `----state: ${avPlayer.state}`);
      avPlayer.reset();
    })

    //状态机切换事件回调类型
    avPlayer.on('stateChange', async (state: string) => {
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          console.info(` setAVPlayerCallback AVPlayer state idle called.`);
          break;
        case 'initialized':// avplayer 设置播放源后触发该状态上报
          console.info(`setAVPlayerCallback AVPlayer state initialized called.`);
          avPlayer.surfaceId = this.surfaceID;
          avPlayer.prepare();
          break;
        case 'prepared':// prepare调用成功后上报该状态机
          console.info(`setAVPlayerCallback AVPlayer state prepared called.`);
          this.durationTime = avPlayer.duration;
          this.currentTime = avPlayer.currentTime;
          this.durationStringTime = TimeFormatUtil.formatToHMS(this.durationTime);
          avPlayer.play();
          break;
        case 'playing':// play成功调用后触发该状态机上报
          console.info(`setAVPlayerCallback AVPlayer state playing called.`);
          this.isPlaying = true;
          break;
        case 'paused':
          console.info(`setAVPlayerCallback AVPlayer state paused called.`);
          this.isPlaying = false;
          break;
        case 'stopped':
          console.info(`setAVPlayerCallback AVPlayer state stopped called.`);
          this.isPlaying = false;
          break;
        case 'completed':// 播放结束后触发该状态机上报
          console.info(`setAVPlayerCallback AVPlayer state completed called.`);
          this.isPlaying = false;
          break;
        case 'released':
          console.info(`setAVPlayerCallback released called.`);
          break;
        case 'error':
          console.error(`setAVPlayerCallback AVPlayer state error called.`);
          this.isPlaying = false;
          avPlayer.reset();
          break;
        default:
          console.info(` setAVPlayerCallback AVPlayer state unknown called.`);
          break;
      }
    })
  }

  build() {
    RelativeContainer(){
      XComponent({
        // 装载视频容器
        id: 'xComponent',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .alignRules(AlignRules.centerInParent)
        .onLoad(() => {
          this.surfaceID = this.xComponentController.getXComponentSurfaceId();
          this.initAVPlayer()
        })
        .width(this.XComponentWidth)
        .height(this.XComponentHeight)
      //控制组件
      this.playControl()
      if (!this.isPlaying) {
        Image($r('app.media.ic_paused'))
          .width('56vp')
          .height('56vp')
          .alignRules(AlignRules.centerInParent)
      }
    }
    .backgroundColor(Color.Black)
    .height('100%')
    .width('100%')
    .onClick(()=>{
      if (this.isPlaying) {
        if (this.avPlayer === undefined) {
          console.info(`pauseVideo error! avPlayer undefined! .`);
          return;
        }
        this.avPlayer!.pause();
      }else {
        if (this.avPlayer === undefined) {
          console.info(`playVideo error! avPlayer undefined! .`);
          return;
        }
        if (this.avPlayer!.state === 'prepared' ||this.avPlayer!.state === 'paused' ||
          this.avPlayer!.state === 'completed') {
          this.avPlayer!.play();
        }
      }
    })
  }
  @Builder
  playControl() {
    Column() {
      Row() {
        Text(this.currentStringTime)
          .fontSize('20vp')
          .fontColor(Color.White)
          .margin({ left: '2vp' })
          .width('45%')
          .textAlign(TextAlign.End)
          .zIndex(3)

        Divider()
          .vertical(true)
          .height('14vp')
          .width('2vp')
          .backgroundBlurStyle(BlurStyle.Regular, { colorMode: ThemeColorMode.LIGHT })
          .color(Color.White)
          .opacity(0.5)
          .margin({ left: 8, right: 8 })
          .rotate({
            x: 0,
            y: 0,
            z: 1,
            centerX: '50%',
            centerY: '50%',
            angle: 30
          })

        Text( this.durationStringTime)
          .fontSize('20vp')
          .fontColor(Color.White)
          .margin({ left: '2vp' })
          .width('45%')
          .textAlign(TextAlign.Start)
          .opacity(0.5)
          .zIndex(3)
      }
      .width('100%')
      .opacity(this.isSliderMoving?1:0)
      Row(){
        Text(this.currentStringTime)
          .fontColor(Color.White)
          .textAlign(TextAlign.End)
          .fontWeight(FontWeight.Regular)
          .margin({ left:10})
        Slider({
          value: this.currentTime,
          min: 0,
          max: this.durationTime,
          style: SliderStyle.NONE
        })
          .layoutWeight(1)
          .height(20)
          .blockColor(Color.White)
          .trackColor(Color.Gray)
          .selectedColor('#007DFF')
          .showSteps(false)
          .showTips(false)
          .trackThickness(4)
          .trackBorderRadius(2)
          .zIndex(3)
          .onChange((value: number, mode: SliderChangeMode) => {
            console.info(`Slider onChange value ${value} `);
            if (mode === SliderChangeMode.Begin) {
              this.isSliderMoving = true;
              this.sliderClickValue = value
            }

            if (mode === SliderChangeMode.Moving) {
              this.currentStringTime = TimeFormatUtil.formatToHMS(value)
              this.sliderClickValue = value
            }
            if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) {  //离开滑块 点击滑动条
              this.avPlayer!.seek( this.sliderClickValue, media.SeekMode.SEEK_NEXT_SYNC);
              this.isSliderMoving = false;
            }
          })
        Text( this.durationStringTime)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Regular)
      }

    }.alignRules(AlignRules.alignParentBottomCenter)
  }
  aboutToDisappear() {
    if (this.avPlayer == null) {
      console.info(`avPlayer has not init aboutToDisappear`);
      return;
    }
    this.avPlayer.release((err) => {
      if (err == null) {
        console.info(`videoRelease release success`);
      } else {
        console.error(`videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
      }
    });
  }

  onPageHide() {
    this.avPause();
    this.isPlaying = false;
  }

  avPause(): void {
    if (this.avPlayer) {
      try {
        this.avPlayer.pause();
        console.info(` avPause==`);
      } catch (e) {
        console.error(`avPause== ${JSON.stringify(e)}`);
      }
    }
  }
}

注意:
1.监听videoSizeChange事件,获取到视频的长宽,根据长宽比重新设置XComponent的长宽,让视频充满长或宽,显示完整
2.Slider点击滑动条会触发3次onChange事件,分别是按下,移动/点击,结束,因此这里需要特殊处理一下

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
已于2025-7-14 15:00:53修改
3
收藏
回复
举报
回复
    相关推荐