# 基于AVPlayer基础播控实践

#### 概述

本文适用于视频播放类应用的开发，针对市场上主流视频播放类应用的常见场景，介绍了如何基于AVPlayer系统播放器实现视频播放应用。

本文指导开发者基于HarmonyOS提供的媒体和ArkUI等能力，实现视频播放、暂停、跳转播放、静音播放、循环播放、窗口缩放模式设置、倍速设置、音量设置等基本开发场景，可以为视频播放应用提供灵活的交互体验和良好的观看效果。

在阅读本文之前，建议开发者先熟悉视频播放器《使用AVPlayer播放视频(ArkTS)》。

#### 场景分析

| 场景名称 | 描述 | 实现方案 |
| --- | --- | --- |
| 基础播控 | 视频资源的加载、播放、暂停、退出等操作。 | 使用AVPlayer接口实现。 |
| 跳转播放 | 滑动进度条精准跳转到指定时间进行播放。 | 使用Slider组件实现进度条，在其onChange回调中触发进度调节。 |
| 静音播放 | 点击按钮设置静音播放。 | 使用AVPlayer的setMediaMuted()方法控制静音状态。 |
| 循环播放 | 视频播放结束后会从初始位置再次播放。 | 通过在prepared状态下，设置视频播放器AVPlayer的loop属性值为true，实现视频循环播放。 |
| 窗口缩放模式设置 | 设置窗口缩放模式体验不同的缩放效果。 | 通过设置AVPlayer的videoScaleType属性设置窗口缩放模式。 |
| 倍速设置 | 可以通过点击按钮选择倍速或者通过长按手势调节倍速。 | 使用AVPlayer的setSpeed()方法设置倍速。通过添加按钮和弹窗实现通过按钮调节倍速；通过给组件绑定长按手势实现长按倍速。 |
| 音量设置 | 可以滑动屏幕调节音量。 | 使用AVVolumePanel组件显示音量，通过给组件绑定手势滑动监听实现调节音量。 |
| 字幕挂载与切换 | 视频下方显示字幕。 | 使用AVPlayer的addSubtitleFromFd()方法设置外挂字幕资源，并通过AVPlayer实例注册字幕回调函数on('subtitleUpdate')；通过切换字幕资源并使用AVPlayer的reset()方法重置播放实现切换字幕。 |

#### 基础播控

#### 场景描述

通过AVPlayer实现核心视频播放控制能力，包括视频资源加载、播放、暂停、停止及退出等操作。

#### 实现原理

本开发指导将介绍如何使用AVPlayer开发视频播放功能，以完整地播放一个视频作为示例，实现端到端播放原始媒体资源。

播放的全流程包含：创建AVPlayer，设置播放资源和窗口，设置播放参数（音量/倍速/缩放模式），播放控制（播放/暂停/跳转/停止），重置，销毁资源。在进行应用开发的过程中，开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作，系统可能会抛出异常或生成其他未定义的行为。

原理详情可查看《使用AVPlayer播放视频(ArkTS)》。

#### 开发步骤

1.  创建实例：调用createAVPlayer()接口创建AVPlayer实例，初始化进入idle状态。
2.  设置视频资源：设置AVPlayer实例的url或者fdSrc属性值，进入initialized状态。
3.  准备播放：调用prepare()接口，进入prepared状态。
4.  播放：调用play()接口，进入playing状态。
5.  暂停：调用pause()接口，进入paused状态。
6.  停止：调用stop()接口，进入stopped状态。
7.  销毁实例：调用release()接口销毁实例，AVPlayer进入released状态，退出播放。

#### 跳转播放

#### 场景描述

进度条是视频应用的一个基础能力，可以通过点击或拖动进度条精准跳转到指定时间进行播放。

#### 实现原理

基于Slider组件和视频播放器AVPlayer的seek()方法实现跳转播放。

#### 开发步骤

采用Slider组件实现进度条功能，根据Slider组件属性设置进度条样式，并在其onChange()事件中触发视频播放器AVPlayer的seek()方法跳转到指定播放位置，实现视频进度的控制。

```
/**
 * Progress slider
 */
Slider({
  value: this.currentTime,
  min: 0,
  max: this.durationTime,
  style: SliderStyle.OutSet
})
  .id('Slider')
  .blockColor(Color.White)
  .trackColor(Color.Gray)
  .selectedColor($r('app.color.slider_selected'))
  .showTips(false)
  .onChange((value: number, mode: SliderChangeMode) => {
    if (mode === SliderChangeMode.Begin) {
      this.isSwiping = true;
      this.avPlayerController.videoPause();
    }
    this.avPlayerController.videoSeek(value);
    this.currentTime = value;
    if (mode === SliderChangeMode.End) {
      this.isSwiping = false;
      this.flag = true;
      this.avPlayerController.videoPlay();
    }
  })
```

#### 静音播放

#### 场景描述

通过界面按钮快捷切换视频播放静音状态，实现一键开启或关闭静音，提升媒体播放的交互便捷性。

#### 实现原理

通过视频播放器AVPlayer的setMediaMuted()方法，实现控制视频静音状态。

#### 开发步骤

1.  在底部操作栏添加Button组件，按钮显示为icon图标；根据Button组件属性设置按钮样式，并在其onClick()事件中触发视频管理接口AvPlayerController.ets文件中封装的videoMuted()方法。
    
    ```
    /**
     * Video Muted Button
     */
    Button() {
      Image(this.isMuted ? $r('app.media.ic_video_speaker_slash') : $r('app.media.ic_video_speaker'))
        .width($r('app.float.size_30'))
        .height($r('app.float.size_30'))
    }
    .type(ButtonType.Normal)
    .width($r('app.float.size_30'))
    .height($r('app.float.size_30'))
    .borderRadius($r('app.float.size_20'))
    .backgroundColor('rgba(0, 0, 0, 0)')
    .margin({ left: $r('app.float.size_5') })
    .fontColor(Color.White)
    .onClick(() => {
      this.isMuted = !this.isMuted;
      this.avPlayerController.videoMuted(this.isMuted)
    })
    ```
    
2.  videoMuted()方法中调用了视频播放器AVPlayer的setMediaMuted()方法，实现控制视频静音状态，其中第一个参数mediaType选择媒体类型为MEDIA\_TYPE\_AUD表示音频，第二个参数是静音开关。
    
    ```
    /**
     * Video muted
     * @param isMuted
     * @returns
     */
    async videoMuted(isMuted: boolean): Promise<void> {
      if (this.avPlayer) {
        try {
          this.isMuted = isMuted;
          await this.avPlayer!.setMediaMuted(media.MediaType.MEDIA_TYPE_AUD, isMuted)
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `videoMuted failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    ```
    

#### 循环播放

#### 场景描述

本功能可以用于在视频播放结束后自动将播放器重置至初始状态，使用户能够立即重新开始播放视频内容，实现无缝循环观看体验。

#### 开发步骤

在视频prepared状态下，设置视频播放器AVPlayer的loop属性值为true，实现视频循环播放。

```
// Callback function for state machine changes
this.avPlayer.on('stateChange', async (state) => {
  if (!this.avPlayer) {
    return;
  }
  switch (state) {
    // ...
    case 'prepared': // This state machine is reported after the prepare interface is successfully invoked.
      this.isReady = true;
      this.avPlayer.loop = true
      // ...
      break;
    // ...
  }
});
```

#### 窗口缩放模式设置

#### 场景描述

通过窗口缩放模式设置功能，用户可根据实际观看需求灵活调整视频内容的显示方式。该功能在未设置视频固定宽高时，在窗口尺寸频繁调整、不同宽高比视频源适配、全屏/窗口模式切换及多屏协作等场景下较为重要。

点击按钮即可弹出设置弹窗，可选择"拉伸至与窗口等大"模式，视频拉伸至与窗口等大，适合需要充分利用显示区域且对比例变化不敏感的场景；选择"缩放至最短边填满窗口"模式，视频将保持原始宽高比并以最短边为基准进行缩放，适合需要保持画面比例不变的场景。

**图1** 拉伸至与窗口等大模式  

**图2** 缩放至最短边填满窗口  

#### 实现原理

通过设置视频播放器AVPlayer的videoScaleType属性值，实现窗口缩放模式的切换。由于VIDEO\_SCALE\_TYPE\_SCALED\_ASPECT（缩放至长边填满窗口）属性值从APIversion20开始才支持在元服务中使用，因此在这之前的版本中可根据屏幕大小为视频设置固定宽高来实现。

在未设置视频固定宽高的情况下，即未设置XComponent的height和width为固定值时，设置AVPlayer的videoScaleType属性值才能生效。

#### 开发步骤

1.  可选择拉伸至与窗口等大/缩放至最短边填满窗口模式，选择后调用封装的videoScaleFit()/videoScaleFitCrop()方法。
    
    ```
    List() {
      ForEach(this.scaleList, (item: Resource, index) => {
        ListItem() {
          Column() {
            Row() {
              Text(item)
              // ...
              Blank()
              Image(this.windowScaleSelect === index ? $r('app.media.ic_radio_selected') :
                $r('app.media.ic_radio'))
              // ...
            }
            // ...
          }
          .width('90%')
        }
        .width('100%')
        .height($r('app.float.size_48'))
        .onClick(() => {
          this.windowScaleSelect = index;
          switch (this.windowScaleSelect) {
            case ZERO:
              this.avPlayerController.videoScaleFit();
              break;
            case ONE:
              this.avPlayerController.videoScaleFitCrop();
              break;
            default:
              break;
          }
          this.controller.close();
        })
      })
    }
    ```
    
2.  videoScaleFit()/videoScaleFitCrop()方法内设置视频播放器AVPlayer的videoScaleType属性值。VIDEO\_SCALE\_TYPE\_FIT表示拉伸至与窗口等大；VIDEO\_SCALE\_TYPE\_FIT\_CROP表示缩放至最短边填满窗口。
    
    ```
    /**
     * Set window scale mode
     */
    videoScaleFit(): void {
      if (this.avPlayer) {
        try {
          this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `videoScaleType_0 failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    
    videoScaleFitCrop(): void {
      if (this.avPlayer) {
        try {
          this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT_CROP
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `videoScaleType_1 failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    ```
    

#### 点击按钮选择倍速

#### 场景描述

通过点击按钮选择预设倍速实现倍速设置，为用户提供灵活的视频播放速率控制。

#### 实现原理

根据选择的倍速调用视频播放器AVPlayer的setSpeed()方法设置对应值，实现视频播放倍速设置。

#### 开发步骤

1.  可选择1.0X、1.25X、1.75X、2.0X，选择后调用对应的设置倍速方法。
    
    ```
      ForEach(this.speedList, (item: Resource, index) => {
        ListItem() {
          Column() {
            Row() {
              Text(item)
              // ...
              Blank()
              Image(this.speedSelect === index ? $r('app.media.ic_radio_selected') :
              $r('app.media.ic_radio'))
              // ...
            }
            // ...
          }
          .width('90%')
        }
        .width('100%')
        .height($r('app.float.size_48'))
        .onClick(() => {
          this.speedSelect = index;
          switch (this.speedSelect) {
            case ZERO:
              this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
              break;
            case ONE:
              this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
              break;
            case TWO:
              this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
              break;
            case THREE:
              this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
              break;
            default:
              break;
          }
          this.controller.close();
        })
      }, (item: Resource, index) => index + '_' + JSON.stringify(item))
    }
    ```
    
2.  设置倍速方法内调用视频播放器AVPlayer的setSpeed()方法实现设置倍速。
    
    ```
    videoSpeed(speed: number): void {
      if (this.avPlayer) {
        try {
          this.avPlayer.setSpeed(speed);
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `videoSpeed failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    ```
    

#### 长按手势调节倍速

#### 场景描述

通过长按手势实现长按屏幕时2倍速播放，长按结束时恢复1倍速播放。

#### 实现原理

通过为元素绑定长按手势事件，在长按手势开始时调用视频播放器AVPlayer的setSpeed()方法设置值为media.PlaybackSpeed.SPEED\_FORWARD\_2\_00\_X，实现长按时2倍速播放；长按结束后调用视频播放器AVPlayer的setSpeed()方法设置值为media.PlaybackSpeed.SPEED\_FORWARD\_1\_00\_X，恢复播放速度为1倍速。

#### 开发步骤

1.  为元素绑定长按手势LongPressGesture事件，并调用封装的videoSpeed()方法，长按手势开始时传递参数为media.PlaybackSpeed.SPEED\_FORWARD\_2\_00\_X，设置播放速度为2倍速；长按手势结束时传递参数为media.PlaybackSpeed.SPEED\_FORWARD\_1\_00\_X，恢复播放速度为1倍速。
    
    ```
    .gesture(
      LongPressGesture({ repeat: true })
        .onAction(() => {
          this.speedSelect = CASE_THREE
          this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
        })
        .onActionEnd(() => {
          this.speedSelect = CASE_ZERO
          this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
        })
    )
    ```
    
2.  设置倍速方法内调用视频播放器AVPlayer的setSpeed()方法实现设置倍速。
    
    ```
    videoSpeed(speed: number): void {
      if (this.avPlayer) {
        try {
          this.avPlayer.setSpeed(speed);
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `videoSpeed failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    ```
    

#### 音量设置

#### 场景描述

滑动调节音量是一项非常实用的功能，它允许用户在不离开视频播放界面的情况下快速调整音量，以获得更好的观看体验。该功能位于窗口左侧，通过上下滑动手势即可调整音量。

#### 实现原理

用AVVolumePanel组件显示系统音量面板，为元素绑定PanGesture滑动手势事件，设置滑动方向为竖直方向，当手势在移动时，上滑增加音量，下滑减少音量，实现控制系统音量功能。

#### 开发步骤

1.  添加AVVolumePanel组件显示系统音量面板。
    
    ```
    import { AVVolumePanel } from '@kit.AudioKit';
    
    @Component
    export struct SetVolume {
      @Prop volume: number = 5
      @Prop volumeVisible: boolean = false
    
      build() {
        Column() {
          AVVolumePanel({
            volumeLevel: this.volume,
            volumeParameter: {
              position: {
                x: 50,
                y: 1000
              }
            }
          })
            .width(10)
        }
        .visibility(this.volumeVisible ? Visibility.Visible : Visibility.Hidden)
        .height('50%')
      }
    }
    ```
    
2.  为元素绑定PanGesture滑动手势事件，并根据手势滑动距离计算音量值volume。
    
    ```
    .gesture(
      PanGesture({ direction: PanDirection.Vertical })
        .onActionStart(() => {
        })
        .onActionUpdate((event: GestureEvent) => {
          this.volumeVisible = true;
          let curVolume = this.volume - this.getUIContext().vp2px(event.offsetY) / this.windowHeight;
          curVolume = curVolume >= 15.0 ? 15.0 : curVolume;
          curVolume = curVolume <= 0.0 ? 0.0 : curVolume;
          this.volume = curVolume;
        })
        .onActionEnd(() => {
          this.setVolumeTimer();
        })
    )
    ```
    

#### 字幕挂载与切换

#### 场景描述

在视频播放前，用户可设置外挂字幕文件，字幕将精准同步显示于视频画面下方，并可以通过按钮切换字幕语言，提升观看体验。

#### 实现原理

通过AVPlayer视频播放器的addSubtitleFromFd()方法加载外挂字幕，并使用on('subtitleUpdate')方法注册字幕回调函数。在回调函数中获取字幕文本，通过状态变量刷新Text组件显示内容。使用Text组件显示字幕并设置字体格式。

切换字幕语言需依据所选语言加载相应的字幕资源，然后调用AVPlayer的reset()方法重置播放器并重新初始化。

#### 开发步骤

1.  使用AVPlayer实例的addSubtitleFromFd()方法为视频添加外挂字幕资源。
    
    ```
    if (this.curSource.caption) {
      let fileDescriptorSub = await this.context.resourceManager.getRawFd(this.curSource.caption);
      this.avPlayer.addSubtitleFromFd(fileDescriptorSub.fd, fileDescriptorSub.offset, fileDescriptorSub.length)
        .catch((err: BusinessError) => {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `addSubtitleFromFd failed, code is ${err.code}, message is ${err.message}`);
        });
    }
    ```
    
2.  通过AVPlayer的on('subtitleUpdate')方法订阅获取外挂字幕事件，当有外挂字幕时，会通过订阅的回调方法通知用户。用户只能订阅一个外挂字幕事件的回调方法，若用户重复订阅，则以最后一次订阅的回调接口为准。回调中可以获取字幕文本、当前时间等信息，从而可以通过状态变量刷新Text组件显示的内容。
    
    ```
    this.avPlayer.on('subtitleUpdate', (info: media.SubtitleInfo) => {
      if (info) {
        let text = (!info.text) ? '' : info.text;
        this.currentCaption = text; //update current caption content
      } else {
        this.currentCaption = '';
        hilog.error(CommonConstants.LOG_DOMAIN, TAG, 'subtitleUpdate info is null');
      }
    });
    ```
    
3.  使用状态变量刷新Text组件的内容，并通过修改Text属性调整字幕字体格式。
    
    ```
    Stack({ alignContent: Alignment.Center }) {
      Text(this.avPlayerController.currentCaption)
        .fontColor(Color.White)
        .fontSize($r('app.float.size_20'))
        .fontFamily('Sans')
    }
    .width('100%')
    .position({ x: $r('app.float.size_zero'), y: $r('app.float.size_216') })
    .zIndex(1)
    ```
    
4.  根据所选语言切换对应字幕资源，并调用AVPlayer的reset()方法重置播放器并重新初始化。
    
    ```
    async languageChange(languageSelect: number = 0): Promise<void> {
      if (this.avPlayer) {
        try {
          if (this.curSource && this.curSource.caption) {
            this.curSource.caption = languageSelect === 0 ? 'captions.srt' : 'en_captions.srt'
            this.curSource.seekTime = this.avPlayer.currentTime;
            await this.avPlayer.reset();
            this.initAVPlayer(this.curSource, this.surfaceID, this.avPlayer);
          }
        } catch (err) {
          hilog.error(CommonConstants.LOG_DOMAIN, TAG,
            `languageChange failed, code is ${err.code}, message is ${err.message}`);
        }
      }
    }
    ```
    

#### 示例代码

*   基于AVPlayer实现视频基础播控功能