鸿蒙next开发 根据avplayer 封装的播放器 支持平常播放器大部分常用功能

一:根据鸿蒙官方的avplayer 封装的播放器,根据我自己的理解进行的封装使用,如果您有不同个的见解请多交流哦!!!!

好了,主要代码分了两个ets文件。

1:首先是工具类的封装,不多说,上代码

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';


export default class avplayer{

  private url:string = ''
  private surfaceId:string = ''

  private mineAvPlayer:media.AVPlayer|null = null

  private mineseekTime:number = 0

  DructionGetCallBack?:(druction:number)=>void
  NowDructionCallback?:(nowDruction:number)=>void

  private nowState:string = ''


  seturl(url:string){
    this.url = url
  }
  setsurfaceId(surfaceId:string){
    this.surfaceId = surfaceId
  }


 async prepare(callBack:(prepare:boolean)=>void){
      this.mineAvPlayer = await media.createAVPlayer()

   if (this.mineAvPlayer == null) {

     return
   }

//avplayer 创建成功state状态为 idle 这之后才可设置URL
   if (this.mineAvPlayer.state ==='idle') {

     if (this.mineAvPlayer == null) {
       return
     }
     this.mineAvPlayer.url = this.url;
   }

   this.mineAvPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
     // 开发者根据需要写入业务逻辑。

     console.info('获取当前状态state:',state)

     this.nowState = state

     //设置URL后state状态会转为 initialized 然后设置 surfaceid 调用prepare 即可进行播放
     if (state === 'initialized') {
       if (this.mineAvPlayer == null) {
         return
       }
       if (this.surfaceId === '') {
         return
       }
       this.mineAvPlayer.surfaceId = this.surfaceId

       //准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置 、音量等。
       this.mineAvPlayer?.prepare((err: BusinessError,data) => {
         if (err) {
           console.error('获取 Failed to prepare,error message is :' + err.message);
         } else {
           console.info('获取  Succeeded in preparing');
           if (this.mineAvPlayer == null){
             return
           }
         }
       });

     }else if(state === 'prepared'){
       // 调用prepare 后如果成功 state 会变为 prepared 此时即可返回到类调用组件进行 play调用(这也不可以用反馈给调用的组件 直接调用play也行)
       callBack(true)
     }
   });


   //视频总时长
   this.mineAvPlayer.on('durationUpdate',async (fullDruction:number)=>{
     if (this.DructionGetCallBack) {
       this.DructionGetCallBack(fullDruction)
     }
   })

   //更新当前的时间
   this.mineAvPlayer.on('timeUpdate',async (nowDruction:number)=>{
     if (this.NowDructionCallback) {
       this.NowDructionCallback(nowDruction)
     }
   })
 }

  //播放
  play(){
    if (this.mineAvPlayer == null) {
      return
    }
    // 播放操作。
    this.mineAvPlayer.play().then(() => {
      console.info('Succeeded in playing');
    }, (err: BusinessError) => {
      console.error('Failed to play,error message is :' + err.message);
    });
  }


  pause(){

    // 暂停操作。
    this.mineAvPlayer?.pause((err: BusinessError) => {
      if (err) {
        console.error('Failed to pause,error message is :' + err.message);
      } else {
        console.info('Succeeded in pausing');
      }
    });
  }

//跳转到某个时间
  seekTime(seektime:number){
    //拖动之前先暂停播放
     this.mineseekTime = seektime
    this.mineAvPlayer?.seek(this.mineseekTime, media.SeekMode.SEEK_PREV_SYNC);
  }

  stop(){
    // 停止操作。
    this.mineAvPlayer?.stop((err: BusinessError) => {
      if (err) {
        console.error('Failed to stop,error message is :' + err.message);
      } else {
        console.info('Succeeded in stopping');
      }
    });
  }

  //如果切换视频源 需先调用reset
  reset(){
    this.mineAvPlayer?.reset()
    if (this.mineAvPlayer == null) {
      return
    }
  }

  //退出
  out(){

    this.mineAvPlayer?.release((err: BusinessError) => {
      if (err) {
        console.error('Failed to release,error message is :' + err.message);
      } else {
        console.info('Succeeded in releasing');
      }
    });
  }

  getState():string{
   return this.nowState
  }

  //设置声音
  setvolume(volume:number){
    this.mineAvPlayer?.setVolume(volume)
  }

}





2: 接下来是播放界面的建议封装和对工具类的调用,其中很遗憾的是弹幕功能并为开发完毕,有些思路(感觉一般般),还没想到一个好的方法,

(1)说实话,这次的封转并不完美,简单应用还是没问题的,如果您有更好的方法,请积极交流,感谢

继续上代码!!!这是第二个ets文件

import avplayer from './utils/avplayerUtil'
import { JSON } from '@kit.ArkTS';
import { formatDuration } from './utils/toolsUtill'; //哦  这里是一个将视频时长转为时分秒的工具 这个ets文件后,下面会贴出代码
import { audio } from '@kit.AudioKit';   //这里是对声音控制的
import { window } from '@kit.ArkUI';     //这里是对应用内亮度控制的



// @Entry  设置成组件 这个要去掉

//这里直接设置成了组件 这样就可以之直接在其他页面应用了  @ComponentV2 和 @Component 的区别请去官方查看吧
@ComponentV2
export struct avPlayer {

  myXComponentController: XComponentController = new XComponentController();

  private mineAvplayer:avplayer = new avplayer()
  @Local nowPlayTime:number = 0
  @Local fulldruction:number = 0
  @Local nowState:string = 'paused'

  @Local nowPlayTimestr:string = ''
  @Local fullDrucStr:string = ''

  @Local currentVolume$:number = 0
  @Local currentLight$:number = 0

  @Local MAX_VOLUME:number = 100
  @Local MIN_VOLUME:number = 0

  @Local showlight:boolean = false
  @Local showVolume:boolean = false

  @Local offsetY: number = 0;

  @Local hideViews:boolean = false
  @Local showTextinut:boolean = false


  //这里就是我为什么用@ComponentV2 的原因 :
  // @Param @Require videourl:string
  //   @Monitor('videourl')
  //检测videourl 变化进而执行 onurlChanged 用于视频源改变后 播放相关操作
  @Param @Require videourl:string
  @Monitor('videourl')
  onurlChanged(monitor:IMonitor){

    //这里是查看是否改变了
    monitor.dirty.forEach((path: string) => {
      console.info(`${path} 获取 changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    });
    //url 发生变化 则 进行reset
    this.mineAvplayer.reset()

    //reset 后 再次 load 进行视频播放
    this.reonload()

  }

  windowStage: window.WindowStage = AppStorage.get('windowStage') as window.WindowStage;
  // 获取主窗口的方式
  mainWin: window.Window = this.windowStage.getMainWindowSync();

  controller: TextInputController = new TextInputController();

  @Local textinputtext:string = ''



  aboutToAppear(): void {
    this.currentLight$ = this.mainWin.getWindowProperties().brightness

  }

  //获取应用音量
  getVolume(){
    let audioManager = audio.getAudioManager();
    let audioVolumeManager = audioManager.getVolumeManager();
    // 查询应用音量。
    audioVolumeManager.getAppVolumePercentage().then((value: number) => {
      console.info(`获取 app volume is: ${value}.`);
      this.currentVolume$ = value

    });
  }



  getState(){

  this.nowState = this.mineAvplayer.getState()
  }

//设置应用亮度
  setlight(){
    // 获取最上层窗口的方式
    window.getLastWindow(getContext(this));
    try {
      this.mainWin.setWindowBrightness(this.currentLight$, (err) => {
        if (err.code) {
          console.error('获取 Failed to set the brightness. Cause: ' + JSON.stringify(err));
          return;
        }
        console.info('获取 Succeeded in setting the brightness.');
      });
    } catch (exception) {
      console.error('获取 Failed to set the brightness. Cause: ' + JSON.stringify(exception));
    }
  }

//屏幕亮度相关
  @Builder
  lightpanSliderView(color:Color|string){

    Stack(){
      Slider({
        value:$$this.currentLight$,
        min:0,
        max:1,
        style: SliderStyle.NONE,
        direction: Axis.Vertical,
        reverse:true //是否取反向
      })
      //定义滑动条的宽度
        .trackThickness('100%')
        .height('100%')
        .trackColor('#ADD8E6')
        .selectedColor('#4169E1')
        .showTips(false)
        .onChange((value:number)=>{

          this.setlight()

        })
    }
    .width(50)
    .height(200)
    .opacity(this.showlight? 1:0)
  }

//设置声音
  setVolume(){
    this.mineAvplayer.setvolume(this.currentVolume$)

  }

  //声音相关
  @Builder
  VolumpanSliderView(color:Color|string){

    Stack(){
      Slider({
        value:$$this.currentVolume$,
        min:0,
        max:1,
        style: SliderStyle.NONE,
        direction: Axis.Vertical,
        reverse:true //是否取反向

      })
        //定义滑动条的宽度
        .trackThickness('100%')
        .height('100%')
        .trackColor('#ADD8E6')
        .selectedColor('#4169E1')
        .showTips(false)
        .onChange((value:number)=>{
          this.setVolume()
        })
    }
    .width(50)
    .height(200)
    .opacity(this.showVolume? 1:0)

  }


  reonload(){

    let surfaceId: string = this.myXComponentController.getXComponentSurfaceId();
    console.info("XComponent SurfaceId: " + surfaceId);

    this.mineAvplayer.seturl(this.videourl)
    this.mineAvplayer.setsurfaceId(surfaceId)
    this.mineAvplayer.prepare((prepare:boolean)=>{
      if (prepare) {
        this.mineAvplayer.play()
        this.getVolume() //获取音量
      }
    })

    this.mineAvplayer.DructionGetCallBack = (druction)=>{
      this.fulldruction = druction
      console.info('获取总时长:',JSON.stringify(druction))
      //转为时分秒
      this.fullDrucStr = formatDuration(this.fulldruction)
    }

    this.mineAvplayer.NowDructionCallback = (nowdruction)=>{
      this.nowPlayTime = nowdruction
      console.info('播放当前时间:',JSON.stringify(nowdruction))
      //转为时分秒
      this.nowPlayTimestr = formatDuration(this.nowPlayTime)
    }
  }

  build() {
    Stack(){
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
        XComponent({
          type: XComponentType.SURFACE,
          controller: this.myXComponentController
        })
          .onLoad(() => {

            this.reonload()

          })
      }

      //操作界面
      Column(){

        if (!this.hideViews){
          Row(){
            Button('back').onClick((event: ClickEvent) => {
              this.mineAvplayer.out()
              this.getUIContext().getRouter().back()
            })
          }
          .backgroundColor('rgba(0,0,0,0.7)')
          .width('100%')
          .height(48)
        }

        //滑动改变亮度和声音
        Row(){

          //左侧滑动区域 控制亮度
          Column(){
            this.lightpanSliderView('')
          }
          .width('50%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Start)
          .padding({left:20})
          .gesture(
            PanGesture()
              .onActionStart(() => {
                console.info("获取 Pan start");
                this.showlight = true

                this.offsetY = 0
              })
              .onActionUpdate((event: GestureEvent) => {
                // 单指上下滑动
                if (event.source === SourceType.TouchScreen) {

                  this.offsetY = event.offsetY
                  console.info("获取 sss offset "+`${this.offsetY}`+'和'+`${this.currentLight$}`);
                  this.currentLight$ += -this.offsetY*0.0001

                }
              })

              .onActionEnd(()=>{
                console.info("获取  offset "+`${this.offsetY}`+'和'+`${this.currentLight$}`);
                this.showlight = false
                this.offsetY = 0
                this.setlight()

              })
          )

          //右侧滑动区域 控制音量
          Column(){
            this.VolumpanSliderView('')
          }
          .width('50%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.End)
          .padding({right:20})
          .gesture(
            PanGesture()
              .onActionStart(() => {
                console.info("获取 Pan start");
                this.showVolume = true

                this.offsetY = 0

              })
              .onActionUpdate((event: GestureEvent) => {
                // 单指上下滑动
                if (event.source === SourceType.TouchScreen) {
                  console.info("获取 finger move triggered PanGesture");

                  this.offsetY = event.offsetY
                  this.currentVolume$ += -this.offsetY*0.0001

                }
              })
              .onActionEnd(()=>{

                this.offsetY = 0
                this.showVolume = false
                this.setVolume()

              })
          )
        }
        .backgroundColor(Color.Transparent)
        .width('100%')
        .layoutWeight(1)
        .gesture(
          //组合手势的三种中的其中一种mode:  Exclusive:互斥识别,注册的手势同时识别,若有一个手势识别成功,则结束手势识别,其他手势识别均失败。
                                       //所以 count :2 放在前面  否则先识别 count:1 的然后就结束
          GestureGroup(GestureMode.Exclusive,

            //双击暂停、开始
            TapGesture({count:2})
              .onAction((event:GestureEvent|undefined)=>{

                if (event) {
                  this.getState()
                   if (this.nowState === 'playing') {
                    this.mineAvplayer.pause()
                  }else{
                    this.mineAvplayer.play()
                  }
                }
              }),
            TapGesture({count:1})
              .onAction((event:GestureEvent|undefined)=>{
                if (event) {
                  this.hideViews = ! this.hideViews
                }
              })
          )
        )

        if (!this.hideViews){
          //播放控制
          Row(){

            if (!this.showTextinut) {

              Button(this.nowState == 'paused'? '暂停':'开始').onClick((event: ClickEvent) => {

                if (this.mineAvplayer.getState() == 'paused') {
                  this.mineAvplayer.play()

                }else{
                  this.mineAvplayer.pause()
                }
                this.getState()

              })
                .width(100)
                .height(50)
                .fontColor(Color.White)

              //进度条
              Slider({
                value:this.nowPlayTime,
                min:0,
                max:this.fulldruction
              })
                .height(10)
                .layoutWeight(1)
                .onChange((value:number,mode:SliderChangeMode)=>{

                  if (mode === SliderChangeMode.Begin) {
                    this.mineAvplayer.pause();  // 开始拖拽时暂停播放

                  }
                  this.mineAvplayer.seekTime(this.nowPlayTime)
                  // 执行跳转
                  this.nowPlayTime = value;  // 更新当前时间显示

                  if (mode === SliderChangeMode.End) {
                    this.mineAvplayer.play();   // 拖拽结束恢复播放
                  }


                })

              //时间显示
              Text(`${this.nowPlayTimestr}/${this.fullDrucStr}`)
                .fontColor(Color.White)
                .width(160)
                .height(50)
                .textAlign(TextAlign.Center)


              Button('发弹幕').onClick((event:ClickEvent)=>{
                this.showTextinut = true
              })
                .width('80')
                .height(50)


            }else{

              Row(){
                TextInput({text:this.textinputtext,placeholder:'请输入文字',controller:this.controller})
                  .backgroundColor(Color.White)
                  .height(40)
                  .layoutWeight(1)
                  .padding({left:10,right:10})
                  .onSubmit(()=>{

                    this.showTextinut = false

                  })
                  .onEditChange((isedit:boolean)=>{
                    if (isedit) {

                    }else{
                      this.showTextinut = false
                    }
                  })
              }
            }
          }
          .backgroundColor('rgba(0,0,0,0.7)')
          .width('100%')
          .height(68)
        }
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

3:这个是时间转时分秒的工具代码

export function formatDuration(durationMs: number): string {
  // 1. 计算总秒数(毫秒转秒)
  const totalSeconds = Math.floor(durationMs / 1000);

  // 2. 提取小时、分钟、秒
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;

  // 3. 格式化为两位数字符串(补零)
  const pad = (num: number) => num.toString().padStart(2, '0');

  // 4. 拼接结果
  return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}

4:接下来就是对封转的avplayer组件的调用了

代码上!!!

import { avPlayer } from './avPlayer'


@Entry
@ComponentV2
struct AvplayerUse{

  @Local videourl:string = 'https://yunceyan.oss-cn-beijing.aliyuncs.com/nhii-archive/webmagic-homework/images-video/2025-09-14/c283b8/1.1-319821-1.mp4'

  build() {

    Column(){

      Stack(){
        avPlayer({
          videourl:this.videourl
        })
          .width('100%')
          .height('100%')

        Column(){
          Button('切换视频源').onClick((event: ClickEvent) => {
            this.videourl = 'http://vjs.zencdn.net/v/oceans.mp4'
          })

          Button('切换视频源22').onClick((event: ClickEvent) => {
            this.videourl = 'https://yunceyan.oss-cn-beijing.aliyuncs.com/nhii-archive/webmagic-homework/images-video/2025-09-14/c283b8/1.1-319821-1.mp4'
          })
        }
      }


    }
    .width('100%')
    .height('100%')

  }
}

OK!!!就这些,有些地方我遇到的问题有简单写注释,在代码里

OK!!!!!!!!可以的话点个关注赞和收藏把!!!谢谢咯~~~

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容