#HarmonyOS NEXT体验官#在鸿蒙中使用AVPlayer完成视频的播放 原创

小方头狮子
发布于 2024-8-6 11:48
浏览
0收藏

在鸿蒙中使用AVPlayer完成视频的播放

#HarmonyOS NEXT体验官#在鸿蒙中使用AVPlayer完成视频的播放-鸿蒙开发者社区

在HarmonyOS系统中,提供两种视频播放开发的方案:

  • AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
  • Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。

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

开发指导

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

图1 播放状态变化示意图

#HarmonyOS NEXT体验官#在鸿蒙中使用AVPlayer完成视频的播放-鸿蒙开发者社区

当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。

名称 类型 说明
idle string 闲置状态,AVPlayer刚被创建createAVPlayer()或者调用了reset()方法之后,进入Idle状态。 首次创建createAVPlayer(),所有属性都为默认值。 调用reset()方法,url9+ 或 fdSrc9+或dataSrc10+属性及loop属性会被重置,其他用户设置的属性将被保留。
initialized string 资源初始化,在Idle 状态设置 url9+ 或 fdSrc9+属性,AVPlayer会进入initialized状态,此时可以配置窗口、音频等静态属性。
prepared string 已准备状态,在initialized状态调用prepare()方法,AVPlayer会进入prepared状态,此时播放引擎的资源已准备就绪。
playing string 正在播放状态,在prepared/paused/completed状态调用play()方法,AVPlayer会进入playing状态。
paused string 暂停状态,在playing状态调用pause方法,AVPlayer会进入paused状态。
completed string 播放至结尾状态,当媒体资源播放至结尾时,如果用户未设置循环播放(loop = 1),AVPlayer会进入completed状态,此时调用play()会进入playing状态和重播,调用stop()会进入stopped状态。
stopped string 停止状态,在prepared/playing/paused/completed状态调用stop()方法,AVPlayer会进入stopped状态,此时播放引擎只会保留属性,但会释放内存资源,可以调用prepare()重新准备,也可以调用reset()重置,或者调用release()彻底销毁。
released string 销毁状态,销毁与当前AVPlayer关联的播放引擎,无法再进行状态转换,调用release()方法后,会进入released状态,结束流程。
error string 错误状态,当播放引擎发生不可逆的错误,详见错误分类,则会转换至当前状态,可以调用reset()重置,也可以调用release()销毁重建。 注意: 区分error状态和 on(‘error’) : 1、进入error状态时,会触发on(‘error’)监听事件,可以通过on(‘error’)事件获取详细错误信息; 2、处于error状态时,播放服务进入不可播控的状态,要求客户端设计容错机制,使用reset()重置或者release()销毁重建; 3、如果客户端收到on(‘error’),但未进入error状态: 原因1:客户端未按状态机调用API或传入参数错误,被AVPlayer拦截提醒,需要客户端调整代码逻辑; 原因2:播放过程发现码流问题,导致容器、解码短暂异常,不影响连续播放和播控操作的,不需要客户端设计容错机制。

开发步骤及注意事项

1.创建实例createAVPlayer(),AVPlayer初始化idle状态。

2.设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:

事件类型 说明
stateChange 必要事件,监听播放器的state属性改变。
error 必要事件,监听播放器的错误信息。
durationUpdate 用于进度条,监听进度条长度,刷新资源时长。
timeUpdate 用于进度条,监听进度条当前位置,刷新当前时间。
seekDone 响应API调用,监听seek()请求完成情况。 当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。
speedDone 响应API调用,监听setSpeed()请求完成情况。 当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。
volumeChange 响应API调用,监听setVolume()请求完成情况。 当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。
bitrateDone 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。 当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。
availableBitrates 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。
bufferingUpdate 用于网络播放,监听网络播放缓冲信息。
startRenderFrame 用于视频播放,监听视频播放首帧渲染时间。
videoSizeChange 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。
audioInterrupt 监听音频焦点切换信息,搭配属性audioInterruptMode使用。 如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。

3.设置资源:设置属性url,AVPlayer进入initialized状态。

说明:

下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:

  • 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源.
  • 如果使用网络播放路径,需申请相关权限:ohos.permission.INTERNET。
  • 如果使用ResourceManager.getRawFd打开HAP资源文件描述符。
  • 需要使用支持的播放格式与协议。

4.设置窗口:获取并设置属性SurfaceID,用于设置显示画面。 应用需要从XComponent组件获取surfaceID,获取方式请参考XComponent。

5.准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。

6.视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。

7.(可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。

8.退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。

完整代码:

今天我们来看一下视频如何正常播放。

这里面我用的是网络视频,大家记得申请权限之后使用哦。


import { common } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { media } from '@kit.MediaKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { CacheListener } from '@ohos/video-cache';
import GlobalProxyServer from '../model/GlobalProxyServer';

const ORIGIN_URL: string = 'http://192.168.1.15:4000/video';
const TAG: string = 'AVPlayManager';

export default class AvPlayManager {
  private static instance: AvPlayManager | null = null;
  private avPlayer: media.AVPlayer = {} as media.AVPlayer;
  private surfaceID: string = '';

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

  /**
   * Init player.
   * @param context
   * @param surfaceId
   * @param callback
   * @returns
   */
  async initPlayer(context: common.UIAbilityContext, surfaceId: string,
    callback: (avPlayer: media.AVPlayer) => void): Promise<void> {
    hilog.info(0x0000, TAG, `initPlayer==initCamera surfaceId== ${surfaceId}`);
    this.surfaceID = surfaceId;
    try {
      this.avPlayer = await media.createAVPlayer();
      await this.setAVPlayerCallback(callback);
      this.cacheAndPlayVideo(context);
    } catch (err) {
      hilog.error(0x0000, TAG, `initPlayer initPlayer err:${JSON.stringify(err)}`);
    }
  }

  /**
   * Set callback.
   * @param callback
   * @returns
   */
  async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void): Promise<void> {
    hilog.info(0x0000, TAG, `setAVPlayerCallback start`);
    if (this.avPlayer === null) {
      hilog.info(0x0000, TAG, 'avPlayer has not init');
      return;
    }
    this.avPlayer.on('seekDone', (seekDoneTime) => {
      hilog.info(0x0000, TAG, `AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    this.avPlayer.on('error', (err) => {
      hilog.error(0x0000, TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
      this.avPlayer.reset();
    })
    this.avPlayer.on('stateChange', async (state, reason) => {
      switch (state) {
        case 'idle':
          hilog.info(0x0000, TAG, 'AVPlayer state idle called.');
          this.videoRelease();
          break;
        case 'initialized':
          hilog.info(0x0000, TAG, 'AVPlayer state initialized called.');
          if (this.surfaceID) {
            this.avPlayer.surfaceId = this.surfaceID;
            hilog.info(0x0000, TAG, `setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
            this.avPlayer.prepare();
          }
          break;
        case 'prepared':
          hilog.info(0x0000, TAG, 'AVPlayer state prepared called.');
          callback(this.avPlayer);
          hilog.info(0x0000, TAG, 'AVPlayer state prepared duration.' + this.avPlayer.duration);
          this.avPlayer.play();
          break;
        case 'playing':
          hilog.info(0x0000, TAG, 'AVPlayer state playing called.');
          AppStorage.setOrCreate('playStatus', 'playing');
          break;
        case 'paused':
          hilog.info(0x0000, TAG, 'AVPlayer state paused called.');
          break;
        case 'completed':
          hilog.info(0x0000, TAG, 'AVPlayer state completed called.');
          AppStorage.setOrCreate('playStatus', 'completed');
          break;
        case 'stopped':
          hilog.info(0x0000, TAG, 'AVPlayer state stopped called.');
          break;
        case 'released':
          hilog.info(0x0000, TAG, 'AVPlayer state released called.');
          break;
        default:
          hilog.info(0x0000, TAG, 'AVPlayer state unknown called.');
          break;
      }
    })
  }

  /**
   * Cache and play video.
   * @param context
   * @returns
   */
  async cacheAndPlayVideo(context: common.UIAbilityContext): Promise<void> {
    hilog.info(0x0000, TAG, `cacheAndPlayVideo start`);

    class MyCacheListener implements CacheListener {
      onCacheAvailable(cacheFilePath: string, url: string, percentsAvailable: number): void {
        AppStorage.setOrCreate('currentCachePercent', percentsAvailable);
      }
    }

    GlobalProxyServer?.getInstance()?.getServer()?.registerCacheListener(new MyCacheListener(), ORIGIN_URL);
    let proxyUrl: string | undefined = await GlobalProxyServer?.getInstance()?.getServer()?.getProxyUrl(ORIGIN_URL);
    if (proxyUrl?.startsWith(context.cacheDir)) {
      const file = fs.openSync(proxyUrl, fs.OpenMode.READ_ONLY);
      proxyUrl = `fd://${file.fd}`;
    }
    hilog.info(0x0000, TAG, `proxyUrl ${proxyUrl}`);
    this.avPlayer.url = proxyUrl;
  }

  /**
   * Play video.
   */
  videoPlay(): void {
    hilog.info(0x0000, TAG, `videoPlay start`);
    if (this.avPlayer !== null) {
      try {
        this.avPlayer.play();
      } catch (err) {
        hilog.error(0x0000, TAG, `videoPlay = ${JSON.stringify(err)}`);
      }
    }
  }

  /**
   * Pause video.
   */
  videoPause(): void {
    hilog.info(0x0000, TAG, `videoPause start`);
    if (this.avPlayer !== null) {
      try {
        this.avPlayer.pause();
      } catch (err) {
        hilog.info(0x0000, TAG, `videoPause== ${JSON.stringify(err)}`);
      }
    }
  }

  /**
   * Release video.
   */
  videoRelease(): void {
    hilog.info(0x0000, TAG, `videoRelease start`);
    if (this.avPlayer !== null) {
      try {
        this.avPlayer.release();
      } catch (err) {
        hilog.info(0x0000, TAG, `videoRelease== ${JSON.stringify(err)}`);
      }
    }
  }
}


大家可以直接使用这段代码,经过测试,这段代码是ok的。

彩蛋

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

视频容器规格 规格描述 分辨率
mp4 视频格式:H26510+/H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 主流分辨率,如4K/1080P/720P/480P/270P
mkv 视频格式:H26510+/H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 主流分辨率,如4K/1080P/720P/480P/270P
ts 视频格式:H26510+/H264/MPEG2/MPEG4 音频格式:AAC/MP3 主流分辨率,如4K/1080P/720P/480P/270P
webm 视频格式:VP8 音频格式:VORBIS 主流分辨率,如4K/1080P/720P/480P/270P

支持的协议如下:

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

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