一、前言:

大家好,我是完美句号!欢迎来到 HarmonyOS 5 开发实战系列。本系列致力于为开发者提供实用的技术方案和即拿即用的代码示例,帮助大家快速掌握 HarmonyOS Next 应用开发中的核心功能。在HarmonyOS 5鸿蒙系统中,基于AVPlayer实现短视频播放是一个重要的功能。本文将详细介绍如何使用AVPlayer来实现这一功能。

AVPlayer作为HarmonyOS平台上的强大音视频播放组件,具备出色的兼容性,能轻松应对多种主流格式。它支持AAC、MP3等音频解码格式,以及264/AVC、H.265/HEVC等视频解码格式,同时也能处理MP4、M4A等封装格式。无论是在线视频还是本地媒体,AVPlayer都能游刃有余地播放。 AVPlayer兼容多种音频和视频格式,适用于多种应用场景。


二、AVPlayer简介:

AVPlayer是HarmonyOS平台的核心音视频播放组件,支持主流音频/视频格式的本地及在线播放,广泛应用于系统级应用与第三方开发场景,基于AVPlayer系统播放器实现,适用于短视频播放类应用的开发,指导开发者实现短视频流畅切换,提炼一套可复制的方案,帮助开发者交付极速、流畅的短视频播放体验。

以下是其核心特性:

  • 支持AAC、MP3等音频格式,H.264/AVC、H.265/HEVC等视频编码格式,兼容MP4、M4A等封装格式。典型应用包括图库、华为视频、美团等系统级应用。 ‌

  • 提供两种开发方式:

    • ‌ArkTS‌:快速构建播放界面,支持状态监听、错误捕获和流程控制一体化;
      ‌ - C/C++ NDK‌:适用于高性能播放或自定义渲染场景,可深度整合硬件资源。 ‌
  • 适用场景:

    • 适用于本地视频播放、在线流媒体播放、音视频转码等场景,支持通过USB/WiFi传输文件,并可扩展至直播资源访问与多任务处理。 其典型应用场景涵盖图库、华为视频、华为音乐等众多领域。

三、基于AVPlayer实现视频基础播控功能:

如何基于AVPlayer系统播放器实现播放本地视频相关功能,指导开发者实现视频加载、播放、暂停、退出、跳转播放、静音播放、循环播放、窗口缩放模式设置、倍速设置、音量设置、字幕挂载等开发场景。

img

├──entry/src/main/ets                             // 代码区
│  ├──common
│  │  ├──constants
│  │  │  └──CommonConstants.ets                   // 公共常量
│  │  └──utils
│  │     ├──GlobalContext.ets                     // 公共工具类
│  │     └──TimeUtils.ts                          // 视频时间帮助类
│  ├──views
│  │  ├──languageDialog.ets                       // 弹幕语言切换弹窗
│  │  ├──ScaleDialog.ets                          // 窗口缩放模式设置弹窗
│  │  ├──SetVolumn.ets                            // 设置音量组件
│  │  ├──SpeedDialog.ets                          // 播放倍速弹窗
│  │  └──VideoOperate.ets                         // 视频操作组件
│  ├──controller 
│  │  └──AvPlayerController.ets                   // avplayer公共控制类
│  ├──entryability
│  │  └──EntryAbility.ets                         // 应用入口Ability
│  ├──model
│  │  └──VideoData.ets                            // 视频数据类
│  └──pages
│     └──Index.ets                                // 首页视频界面
└──entry/src/main/resources                       // 应用资源目录

3.1 具体实现:

  • 使用media.createAVPlayer()来获取AVPlayer对象;
  • 倍速切换:选择不同倍速时调用avPlayer.setSpeed(speed: PlaybackSpeed);
  • 暂停、播放:点击暂停、播放按钮时调用avPlayer.pause()、avPlayer.play();
  • 视频跳转:在拖动滑动条时调用avPlayer.seek();
  • 静音播放:点击静音按钮时调用avPlayer.setMediaMuted();
  • 音量设置:为元素添加手势上下滑动监听PanGesture,滑动时时显示AVVolumePanel组件并根据滑动距离计算音量volume值;
  • 窗口缩放模式设置:选择不同的窗口缩放模式时设置avPlayer的videoScaleType属性值;
  • 长按倍速:为元素添加手势长按监听LongPressGesture,长按时调用avPlayer.setSpeed(speed: PlaybackSpeed);
  • 循环播放:在视频prepared状态下,设置avPlayer的loop属性值为true。
  • 字幕挂载:视频初始化时调用avPlayer.addSubtitleFromFd()设置外挂字幕资源。

img


3.2 音视频播放流程示例(以ArkTS为例):

  • 调用createAVPlayer创建AVPlayer实例,并初始化至idle状态。

  • 设置业务所需的事件监听,特别关注AVPlayer的状态变化。

  • 根据状态变化执行相应逻辑,如切换画面、控制播放等。

img

import { common } from '@kit.AbilityKit';
import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, emitter } from '@kit.BasicServicesKit';
import { CommonConstants, VideoDataType } from '../common/constants/CommonConstants';
import { VideoData } from '../model/VideoData';

const TAG = '[AvPlayerController]';
const CASE_ZERO = 0;
const CASE_ONE = 1;
const CASE_TWO = 2;
const CASE_THREE = 3;

@Observed
export class AvPlayerController {
  @Track surfaceID: string = '';
  @Track isPlaying: boolean = false;
  @Track isReady: boolean = false;
  @Track currentTime: number = 0;
  @Track currentBufferTime: number = 0;
  @Track isLoading: boolean = false;
  @Track duration: number = 0;
  @Track durationTime: number = 0;
  @Track currentCaption: string = '';
  private avPlayer?: media.AVPlayer;
  private curSource?: VideoData;
  private context: common.UIAbilityContext | undefined = AppStorage.get('context');
  private seekTime?: number;
  private isMuted: boolean | undefined = undefined;
  private speedSelect: number = 0;
  private windowScaleSelect: number = 0;
  private index: number = 0;

  // [Start create_instance]
  // Create an AVPlayer instance
  public async initAVPlayer(source: VideoData, surfaceId: string, avPlayer?: media.AVPlayer) {
    if (!this.context) {
      hilog.info(CommonConstants.LOG_DOMAIN, TAG, `initPlayer failed context not set`);
      return
    }
    this.curSource = source;
    if (source.seekTime) {
      this.seekTime = source.seekTime;
    }
    if (source.isMuted) {
      this.isMuted = source.isMuted;
    }
    if (source.index) {
      this.index = source.index;
    }
    if (!this.curSource) {
      return;
    }
    this.surfaceID = surfaceId;

    try {
      // Creates the avPlayer instance object.
      this.avPlayer = avPlayer ? avPlayer : await media.createAVPlayer()
      // Creates a callback function for state machine changes.
      this.setAVPlayerCallback();

      if (!this.context) {
        hilog.info(CommonConstants.LOG_DOMAIN, TAG, `initPlayer failed context not set`);
        return
      }
      switch (this.curSource.type) {
        case VideoDataType.RAW_FILE:
          let fileDescriptor = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
          this.avPlayer.fdSrc = fileDescriptor;
          break;

        case VideoDataType.URL:
          this.avPlayer.url = this.curSource.videoSrc;
          break;

        case VideoDataType.RAW_M3U8_FILE:
          let m3u8Fd = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
          let fdUrl = 'fd://' + m3u8Fd.fd + '?offset=' + m3u8Fd.offset + '&size=' + m3u8Fd.length;
          let mediaSource = media.createMediaSourceWithUrl(fdUrl);
          mediaSource.setMimeType(media.AVMimeTypes.APPLICATION_M3U8);
          let playbackStrategy: media.PlaybackStrategy = { preferredBufferDuration: 20, showFirstFrameOnPrepare: true };
          await this.avPlayer.setMediaSource(mediaSource, playbackStrategy);
          break;
        case VideoDataType.RAW_MP4_FILE:
          let mp4Fd = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
          let mp4FdUrl = 'fd://' + mp4Fd.fd;
          this.avPlayer.url = mp4FdUrl;
          break;
        default:
          break;
      }
      // [Start AddCaption]
      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}`);
          });
      }
      // [End AddCaption]
    } catch (err) {
      hilog.error(CommonConstants.LOG_DOMAIN, TAG,
        `initPlayer failed, code is ${err.code}, message is ${err.message}`);
    }
  }

  // [End create_instance]

  private setAVPlayerCallback() {
    if (!this.avPlayer) {
      return;
    }
    this.avPlayer!.on('error', (err: BusinessError) => {
      hilog.error(CommonConstants.LOG_DOMAIN, TAG, `AVPlayer error, code is ${err.code}, message is ${err.message}`);
      this.avPlayer!.reset().catch((err: BusinessError) => {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `reset failed, code is ${err.code}, message is ${err.message}`);
      });
    });
    this.avPlayer!.on('durationUpdate', (time: number) => {
      this.duration = time;
      AppStorage.setOrCreate('DurationTime', time);
    });
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentTime = time;
      AppStorage.setOrCreate('CurrentTime', time);
    });

    // The error callback function is triggered when an error occurs during avPlayer operations,
    // at which point the reset interface is called to initiate the reset process
    this.avPlayer.on('error', (err: BusinessError) => {
      if (!this.avPlayer) {
        return;
      }
      hilog.error(CommonConstants.LOG_DOMAIN, TAG,
        `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
      this.avPlayer.reset().catch((err: BusinessError) => {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `reset failed, code is ${err.code}, message is ${err.message}`);
      });
    })
    this.subtitleUpdateFunction();
    this.setStateChangeCallback();
  }

  private setStateChangeCallback() {
    if (!this.avPlayer) {
      return;
    }
    // [Start loop_playback]
    // Callback function for state machine changes
    this.avPlayer.on('stateChange', async (state) => {
      if (!this.avPlayer) {
        return;
      }
      switch (state) {
        // [StartExclude loop_playback]
        case 'idle': // This state machine is triggered after the reset interface is successfully invoked.
          hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'setAVPlayerCallback AVPlayer state idle called.');
          break;
        case 'initialized': // This status is reported after the playback source is set on the AVPlayer.
          // Set the display screen. This parameter is not required when the resource to be played is audio-only.
          this.avPlayer.surfaceId = this.surfaceID;
          this.avPlayer.prepare().catch((err: BusinessError) => {
            hilog.error(CommonConstants.LOG_DOMAIN, TAG,
              `prepare failed, code is ${err.code}, message is ${err.message}`);
          });
          break;
        // [EndExclude loop_playback]
        case 'prepared': // This state machine is reported after the prepare interface is successfully invoked.
          this.isReady = true;
          this.avPlayer.loop = true
          // [StartExclude loop_playback]
          this.durationTime = this.avPlayer.duration;
          this.currentTime = this.avPlayer.currentTime;
          this.avPlayer.audioInterruptMode = audio.InterruptMode.SHARE_MODE;
          if (this.seekTime) {
            this.avPlayer!.seek(this.seekTime!, media.SeekMode.SEEK_CLOSEST);
          }
          let eventData: emitter.EventData = {
            data: {
              'percent': this.avPlayer.width / this.avPlayer.height
            }
          };
          emitter.emit(CommonConstants.AVPLAYER_PREPARED, eventData);
          if (this.isMuted) {
            try {
              await this.avPlayer!.setMediaMuted(media.MediaType.MEDIA_TYPE_AUD, this.isMuted!)
            } catch (err) {
              hilog.error(CommonConstants.LOG_DOMAIN, TAG,
                `setMediaMuted failed, code is ${err.code}, message is ${err.message}`);
            }
          }
          this.setWindowScale();
          if (this.index === 0) {
            this.avPlayer.play().catch((err: BusinessError) => {
              hilog.error(CommonConstants.LOG_DOMAIN, TAG,
                `play failed, code is ${err.code}, message is ${err.message}`);
            });
          }
          this.setVideoSpeed();
          // [EndExclude loop_playback]
          break;
        // [StartExclude loop_playback]
        case 'playing': // After the play interface is successfully invoked, the state machine is reported.
          this.isPlaying = true;
          let eventDataTrue: emitter.EventData = {
            data: {
              'flag': true
            }
          };
          let innerEventTrue: emitter.InnerEvent = {
            eventId: 2,
            priority: emitter.EventPriority.HIGH
          };
          emitter.emit(innerEventTrue, eventDataTrue);
          break;
        case 'completed': // This state machine is triggered to report when the playback ends.
          this.currentTime = 0;
          let eventDataFalse: emitter.EventData = {
            data: {
              'flag': false
            }
          };
          let innerEvent: emitter.InnerEvent = {
            eventId: 1,
            priority: emitter.EventPriority.HIGH
          };
          emitter.emit(innerEvent, eventDataFalse);
          break;
        default:
          hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'setAVPlayerCallback AVPlayer state unknown called.');
          break;
        // [EndExclude loop_playback]
      }
    });
    // [End loop_playback]
  }

  private setWindowScale() {
    switch (this.windowScaleSelect) {
      case CASE_ZERO:
        this.videoScaleFit();
        break;
      case CASE_ONE:
        this.videoScaleFitCrop();
        break;
      default:
        break;
    }
  }

  private setVideoSpeed() {
    switch (this.speedSelect) {
      case CASE_ZERO:
        this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
        break;
      case CASE_ONE:
        this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
        break;
      case CASE_TWO:
        this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
        break;
      case CASE_THREE:
        this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
        break;
      default:
        break;
    }
  }

  videoPlay(): void {
    if (this.avPlayer) {
      this.avPlayer.play().catch((err: BusinessError) => {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `play failed, code is ${err.code}, message is ${err.message}`);
      });
      this.isPlaying = true;
    }
  }

  videoPause(): void {
    if (this.avPlayer) {
      this.avPlayer.pause().catch((err: BusinessError) => {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `addSubtitleFromFd failed, code is ${err.code}, message is ${err.message}`);
      });
      this.isPlaying = false;
    }
  }

  // Toggle play/pause state
  videoStop(): void {
    if (this.avPlayer) {
      this.avPlayer.stop().catch((err: BusinessError) => {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `videoPause failed, code is ${err.code}, message is ${err.message}`);
      });
      this.isPlaying = false;
    }
  }

  // [Start video_muted_fun]
  /**
   * 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}`);
      }
    }
  }

  // [End video_muted_fun]

  // [Start video_speed_fun]
  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}`);
      }
    }
  }

  // [End video_speed_fun]

  videoSeek(seekTime: number): void {
    if (this.avPlayer) {
      try {
        this.avPlayer.seek(seekTime, media.SeekMode.SEEK_CLOSEST);
      } catch (err) {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `videoSeek failed, code is ${err.code}, message is ${err.message}`);
      }
    }
  }

  async videoReset(): Promise<void> {
    if (!this.avPlayer) {
      return;
    }
    try {
      await this.avPlayer.reset();
    } catch (err) {
      hilog.error(CommonConstants.LOG_DOMAIN, TAG, `videoReset failed, code is ${err.code}, message is ${err.message}`);
    }

  }

  async videoRelease(): Promise<void> {
    if (!this.avPlayer) {
      return;
    }
    this.avPlayer.release((err) => {
      if (err === null) {
        hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'videoRelease release success');
      } else {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `videoRelease release filed,  code is ${err.code}, message is ${err.message}`);
      }
    });
  }

  getDurationTime(): number {
    return this.durationTime;
  }

  getCurrentTime(): number {
    return this.currentTime;
  }

  // [Start window_scale_fun]
  /**
   * 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}`);
      }
    }
  }

  // [End window_scale_fun]
  subtitleUpdateFunction(): void {
    if (this.avPlayer) {
      try {
        // [Start RegisterCaptionCallBack]
        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');
          }
        });
        // [End RegisterCaptionCallBack]
      } catch (err) {
        hilog.error(CommonConstants.LOG_DOMAIN, TAG,
          `subtitleUpdateFunction failed, code is ${err.code}, message is ${err.message}`);
      }
    }
  }

  // [Start languageSwitch]
  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}`);
      }
    }
  }

  // [End languageSwitch]
}

3.2 开发建议与最佳实践:

img

  • 严格遵循状态机规范:在视频处于prepared、paused或completed状态时,才应调用play方法开始播放;若视频已在播放,则应调用pause方法暂停。

  • 重视资源释放:播放完成后,务必调用release方法,以确保内存、线程等系统资源得到妥善释放,避免潜在的资源泄漏问题。

  • 妥善管理权限:若应用程序需要访问网络或播放媒体,应提前申请相应的权限,以确保应用的正常运行和用户的良好体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐