种地 发表于 2025-2-14 14:57:55

鸿蒙OS 短视频轮播结果及会话控制

代码拜见
gitee: https://gitee.com/jimmy_enjoy/hamonyos_video_demo/tree/master
结果


   鸿蒙模拟器-视频轮播结果

数据定义

export interfaceVideoItem{
name: string
url?: string // 网址播放
video?: string // rawfile 播放
head: Resource
index: number
}
组件

包含Swiper的父组件

Swiper(this.swiperController) {
          ForEach(videoDataList, (item: VideoItem, index: number) => {
            VideoPlayer({
            curSource: item,
            isPageShow: this.isPageShow,
            curTitle: item.name,
            curIndex: this.curVideoIndex,
            index: index,
            total: this.total,
            onShowPrevious: () => this.showPrevious(index),
            onShowNext: () => this.showNext(index)
            })
          })
      }
      .cachedCount(2)
      // .disableSwipe(true)
      .vertical(true)
      .indicator(false)
      .loop(false)
      .curve(Curve.Ease)
      .duration(300)
      .onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
          Logger.info(TAG,
            `onAnimationStart index: ${index},curIndex: ${targetIndex},extraInfo: ${JSON.stringify(extraInfo)}.`);
          this.curVideoIndex = targetIndex;
          // this.curTitle = this.data.getData(this.curVideoIndex)?.name ?? ''
      })
      .width('100%')
      .height('100%')
SwiperItem 子组件

Stack({alignContent: Alignment.BottomStart}){
      Column(){
      XComponent({
          id: 'XComponent',
          type: XComponentType.SURFACE,
          controller: this.xComponentController
      })
          .onLoad(async () => {
            // this.xComponentController.setXComponentSurfaceRect({
            //   surfaceWidth: CommonConstants.SURFACE_WIDTH,
            //   surfaceHeight: CommonConstants.SURFACE_HEIGHT
            // })
            this.surfaceID = this.xComponentController.getXComponentSurfaceId()
            hilog.info(0x0000, TAG,
            'surfaceID=' + this.surfaceID + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
            this.initAVPlayer()
          })
          .aspectRatio(CommonConstants.ASPECT)
          .width('100%')
          .height(240)
      }
      .justifyContent(FlexAlign.Center)
      .width(CommonConstants.WIDTH_FULL_PERCENT)
      .height(CommonConstants.HEIGHT_FULL_PERCENT)
      .zIndex(CommonConstants.Z_INDEX_BASE)

      // 播放控制,这里需要设置更高的Z-Index,防止事件被最上层的手势监听覆盖
      Column(){
      this.PlayControl()
      }
      .zIndex(CommonConstants.Z_INDEX_MAX)
    }
    .gesture(
    // 监听滑动手势
      PanGesture({direction: PanDirection.Horizontal})
      .onActionStart((event: GestureEvent) => {
          this.isSliderGesture = true
          this.panStartX = event.offsetX
          this.panStartTime = this.currentTime
          this.sliderOnchange(this.panStartTime, SliderChangeMode.Begin)
      })
      .onActionUpdate((event: GestureEvent) => {
          this.isSliderGesture = true
          let panTime = this.panStartTime +
            (this.panStartX + event.offsetX) / this.slideWidth * this.durationTime
          this.panEndTime = Math.min(Math.max(0, panTime), this.durationTime)
          this.sliderOnchange(this.panEndTime, SliderChangeMode.Moving)
      })
      .onActionEnd(() => {
          this.sliderOnchange(this.panEndTime, SliderChangeMode.End)
          this.isSliderGesture = false
      })
    )

@Builder
PlayControl(){
    Column(){
    // 滑动时显示,当前时长/总时长
      Row(){
      Text(this.currentStringTime)
          .fontSize($r('app.float.font_size_14'))
          .fontColor(Color.White)
          .opacity($r('app.float.opacity_9'))
          .margin({ left: CommonConstants.TEXT_MARGIN_LEFT })
          .width(CommonConstants.TEXT_LEFT_WIDTH)
          .textAlign(TextAlign.End)
          .zIndex(CommonConstants.SLIDER_INDEX)
      Divider()
          .vertical(true)
          .height($r('app.float.padding_14'))
          .width(CommonConstants.DIVIDER_WIDTH)
          .backgroundBlurStyle(BlurStyle.Regular, { colorMode: ThemeColorMode.LIGHT })
          .color(Color.White)
          .opacity($r('app.float.opacity_9'))
          .margin({ left: $r('app.float.margin_small'), right: $r('app.float.margin_small') })
          .rotate({
            x: CommonConstants.DIVIDER_X,
            y: CommonConstants.DIVIDER_Y,
            z: CommonConstants.DIVIDER_Z,
            centerX: CommonConstants.DIVIDER_CENTER_X,
            centerY: CommonConstants.DIVIDER_CENTER_Y,
            angle: CommonConstants.DIVIDER_ANGLE
          })
      Text(this.durationStringTime)
          .fontSize($r('app.float.font_size_14'))
          .fontColor(Color.White)
          .margin({ right: CommonConstants.TEXT_MARGIN_LEFT })
          .width(CommonConstants.TEXT_LEFT_WIDTH)
          .textAlign(TextAlign.Start)
          .opacity($r('app.float.opacity_4'))
          .zIndex(CommonConstants.SLIDER_INDEX)
      }
      .margin({ bottom: $r('app.float.margin_small') })
      .alignItems(VerticalAlign.Center)
      .opacity(this.isTimeDisplay)

      Column() {
      Row() {
          Image(this.isPlaying ? $r('sys.media.ohos_ic_public_pause') :
          $r('app.media.ic_video_menu_play'))
            .width($r('app.float.size_24'))
            .height($r('app.float.size_24'))
            .fillColor(Color.White)
            .margin({ right: $r('app.float.margin_small') })
            .onClick(() => {
            this.iconOnclick();
            })
            .visibility(
            // this.isFloatWindow || this.isFullLandscapeScreen || this.isFullScreen ||
            this.isSliderDragging ? Visibility.None : Visibility.Visible)
          Slider({
            value: this.isSliderGesture ? this.panEndTime : this.currentTime,
            step: CommonConstants.SLIDER_STEP,
            min: CommonConstants.SLIDER_MIN,
            max: this.durationTime,
            style: this.sliderStyle
          })
            .id('video_slider')
            .height(this.isSliderDragging ? $r('app.float.side_width') : $r('app.float.size_24'))
            .trackColor($r('app.color.white_opacity_1_color'))
            .showSteps(false)
            .blockSize({ width: this.blockSize, height: this.blockSize })
            .blockColor($r('sys.color.background_primary'))
            .layoutWeight(1)
            .trackThickness(this.trackThicknessSize)
            .trackBorderRadius(CommonConstants.TRACK_BORDER_RADIUS)
            .selectedBorderRadius(CommonConstants.TRACK_BORDER_RADIUS)
            .zIndex(CommonConstants.SLIDER_INDEX)
            .onAreaChange(() => {
            let videoSlider: componentUtils.ComponentInfo = componentUtils.getRectangleById('video_slider');
            this.slideWidth = px2vp(videoSlider.size.width);
            // this.offsetY = px2vp(videoSlider.localOffset.y);
            // this.beginX = px2vp(videoSlider.localOffset.x);
            })
            .onChange((value: number, mode: SliderChangeMode) => {
            this.sliderOnchange(value, mode);
            })
      }
      .width(CommonConstants.WIDTH_FULL_PERCENT)
      }

    }
}
AVPlayer

创建AVPlayer

async initAVPlayer(){
    hilog.info(0x0000, TAG, 'createAVPlayer begin')
    media.createAVPlayer().then((video: media.AVPlayer) => {
      if(video !== null){
      Logger.info(TAG, 'initAVPlayer video')
      this.avPlayer = video
      this.avPlayerStart(this.curSource, this.avPlayer)
      this.setAVPlayerCallback(this.avPlayer)
      }
    }).catch((error: BusinessError) => {
      Logger.error(TAG, `AVPlayer catchCallback, error message:${error.message}`);
    })
}
注册AVPlayer回调函数

setAVPlayerCallback(avPlayer: media.AVPlayer){
        // 更新视频当前时间状态
    avPlayer.on('timeUpdate', (time: number) => {
      if(time > this.currentTime * 1000){
      animateTo({duration: 1000, curve: Curve.Linear}, () => {
          this.currentTime = Math.floor(time / 1000)
      })
      } else {
      this.currentTime = Math.floor(time / 1000)
      }
      this.currentStringTime = secondToTime(Math.floor(time / CommonConstants.SECOND_TO_MS))
    })

        // 错误监听
    avPlayer.on('error', (err: BusinessError) => {
      hilog.error(0x0000, TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}` +
      `----state:${avPlayer.state} this.curIndex:${this.curIndex} this.index: ${this.index}`);
      avPlayer.reset();
    })
    this.setAVPlayerStateListen(avPlayer)
}

setAVPlayerStateListen(avPlayer: media.AVPlayer){
        // 监听状态变化
    avPlayer.on('stateChange', async (state: string) => {
      Logger.info(TAG, 'avplayer stateChange')
      switch (state){
      case 'idle': // call reset into idle state
          hilog.info(0x0000, TAG, 'AVPlayer state idle called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`)
          break
      case 'initialized':
          hilog.info(0x0000, TAG,
            'AVPlayer state initialized called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          avPlayer.surfaceId = this.surfaceID;
          avPlayer.prepare();
          break;
      case 'prepared':
          avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE;
          hilog.info(0x0000, TAG,
            'AVPlayer state prepared called.' +
            ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          this.flag = true;
          avPlayer.loop = true;
          this.duration = avPlayer.duration;
          this.durationTime = Math.floor(this.duration / CommonConstants.SECOND_TO_MS);
          this.durationStringTime = secondToTime(this.durationTime);
          if (this.curIndex === this.index) {
            this.playVideo()
            // this.firstFlag = false;
          }
          break;
      case 'completed':
          hilog.info(0x0000, TAG,
            'AVPlayer state completed called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          this.isPlaying = false;
          break;
      case 'playing':
          this.isPlaying = true;
          hilog.info(0x0000, TAG,
            'AVPlayer state playing called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}` +
            ', source=' + this.curSource);
          break;
      case 'paused':
          this.isPlaying = false
          Logger.info(TAG,
            'AVPlayer state paused called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          break;
      case 'stopped':
          Logger.info(TAG,
            'AVPlayer state stopped called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          break;
      case 'released':
          Logger.info(TAG,
            'AVPlayer state released called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          break;
      case 'error':
          Logger.info(TAG, 'AVPlayer state error called' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          // avPlayer.reset();

          break;
      default:
          Logger.info(TAG,
            'AVPlayer state unknown called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
          break;
      }
    })
}
AVSession会话控制



[*]需要申请长时使命
[*]在通知栏可以由系统视频控制器举行视频的控制,包括进度条滑动、快进、快退、暂停、下一首、上一首。
后台长时使命申请

需要在module.json中注册
      {
      "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
      "reason": "$string:reason_background",
      "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
      }
      }
export class BackgroundTaskManager{

public static startContinuousTask(context?: common.UIAbilityContext): void{
    if(!context) return
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
      {
          bundleName: context.abilityInfo.bundleName,
          abilityName: context.abilityInfo.name
      }
      ],
      actionType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      actionFlags:
    }

    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
      try {
      // 启动后台任务
      backgroundTaskManager.startBackgroundRunning(context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
          wantAgentObj).then(() => {
          Logger.info(TAG, 'startBackgroundRunning succeeded');
      }).catch((err: BusinessError) => {
          Logger.error(TAG, `startBackgroundRunning failed Cause:${JSON.stringify(err)}`);
      })
      }catch (error){
      Logger.error(TAG, `Operation startBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
      }
    })
}

public static stopContinuousTask(context?: common.UIAbilityContext){
    try {
      if (!context) {
      return
      }
      backgroundTaskManager.stopBackgroundRunning(context).then(() => {
      Logger.info(TAG, 'stopBackgroundRunning succeeded');
      }).catch((err: BusinessError) => {
      Logger.error(TAG, `stopBackgroundRunning failed Cause:${JSON.stringify(err)}`);
      });
    } catch (error) {
      Logger.error(TAG, `stopBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
    }
}
}
AVSession

AVSession 在整个应用中,全局共享实例。
创建

export class AvSessionController{
private static instance: AvSessionController | null
private context: common.UIAbilityContext | undefined = undefined;
private avSession: avSession.AVSession | undefined = undefined;
private avSessionMetadata: avSession.AVMetadata | undefined = undefined;

private constructor() {
    this.initAvSession()
}

public getAvSession() {
    return this.avSession;
}

public getAvSessionMetadata() {
    return this.avSessionMetadata;
}

public static getInstance(): AvSessionController{
    if(!AvSessionController.instance){
      AvSessionController.instance = new AvSessionController()
    }
    return AvSessionController.instance
}

// 初始化
public initAvSession(){
    this.context = AppStorage.get('context')
    if(!this.context){
      Logger.info(TAG, `session create failed : conext is undefined`)
      return
    }
    avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => {
      this.avSession = avSession
      Logger.info(TAG, `session create successed : sessionId : ${this.avSession.sessionId}`);
      // 启动后台长任务
      BackgroundTaskManager.startContinuousTask(this.context);
      // 设置点击后启动的activity
      this.setLaunchAbility();
      this.avSession.activate();
    })
}

private setLaunchAbility(){
    if(!this.context) return
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
      {
          bundleName: this.context.abilityInfo.bundleName,
          abilityName: this.context.abilityInfo.name
      }
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags:
    }
    wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
      if(this.avSession) this.avSession.setLaunchAbility(agent)
    })
}
在AVPlayer中结合AVSession

初始化

async initAVPlayer(){
    hilog.info(0x0000, TAG, 'createAVPlayer begin')
    media.createAVPlayer().then((video: media.AVPlayer) => {
      if(video !== null){
      // ...
      if(this.curIndex === this.curSource.index){
              // 设置媒体显示信息
          this.avSessionController.setAVMetadata(this.curSource, 0)
      }
      this.setAvSessionListener()
      }
    }).catch((error: BusinessError) => {
      Logger.error(TAG, `AVPlayer catchCallback, error message:${error.message}`);
    })
}
        // 设置会话控制的监听,进行相应媒体播放器调整
public async setAvSessionListener(){
    if(!this.avSessionController) return
    this.avSessionController.getAvSession()?.on('play', () => this.sessionPlayCallback())
    this.avSessionController.getAvSession()?.on('pause', () => this.sessionPauseCallback())
    this.avSessionController.getAvSession()?.on('stop', () => this.sessionStopCallback())
    this.avSessionController.getAvSession()?.on('fastForward',
      (time?: number) => this.sessionFastForwardCallback(time))
    this.avSessionController.getAvSession()?.on('rewind', (time?: number) =>                 this.sessionRewindCallback(time))
    this.avSessionController.getAvSession()?.on('seek', (seekTime: number) => this.sessionSeekCallback(seekTime))
    if(this.index < this.total - 1){
      this.avSessionController.getAvSession()?.on('playNext', () => this.sessionPlayNextCallback())
    }else {
      this.avSessionController.getAvSession()?.off('playNext')
    }
    if(this.index > 0){
      this.avSessionController.getAvSession()?.on('playPrevious', () => this.sessionPlayPreviousCallback())
    }else {
      this.avSessionController.getAvSession()?.off('playPrevious')
    }
}
上一首和下一首实现

通过父容器传递的showNext 和 showPrevious,具体为通过SwiperController实现切换
showNext(index: number): void{
    if(this.swiperController && index < this.total - 1){
      this.swiperController.changeIndex(index + 1, true)
      getContext(this).eventHub.emit('videoShow', index + 1)
    }
}

showPrevious(index: number): void{
    if(this.swiperController && index > 0){
      this.swiperController.changeIndex(index - 1, true)
      getContext(this).eventHub.emit('videoShow', index + 1)
    }
}
重要公共方法实现

// 播放视频
playVideo(){
    if(this.avPlayer && this.curIndex === this.index){
      if(this.avPlayer.state != AVPlayerState.PREPARED && this.avPlayer.state != AVPlayerState.PAUSED &&
      this.avPlayer.state != AVPlayerState.COMPLETED){
      return
      }
      // 重置会话监听状态
      this.setAvSessionListener()
      this.avSessionController.setAVMetadata(this.curSource, this.duration)
      this.updateIsPlay(true)
      this.avPlayer.play((err: BusinessError) => {
      if(err){
          this.updateIsPlay(false)
          Logger.error(TAG, `playVideo failed, code is ${err.code}, message is ${err.message}`);
      } else {
          Logger.info(TAG, `playVideo success , this.curIndex:${this.curIndex}`);
      }
      })
    }
}

// 更新播放状态
updateIsPlay(isPlay: boolean){
    if(this.curIndex !== this.curSource.index) return
    this.isPlaying = isPlay
    // 更新会话控制中,媒体播放状态。
    this.avSessionController.setAvSessionPlayState({
      state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
      position: {
      elapsedTime: this.currentTime * 1000, // 已播放时长
      updateTime: new Date().getTime() // 更新时间
      },
      duration: this.duration
    })
}



[*]完备代码
[*]可以用LazyForEach结合Swiper的cachecount实现懒加载与缓存。
[*]这里没有实现屏幕旋转的环境。
[*]更完备的示例可以参考 鸿蒙官方示例

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 鸿蒙OS 短视频轮播结果及会话控制