鸿蒙中视频播放器ijkPlayer组件分析

173 阅读5分钟

鸿蒙系统下的播放器ijkPlayer是OpenHarmony下的基于FFmpeg的视频播放器,本文对ijkPlayer进行了概况分析并基于ijkPlayer封装一个播放器组件,可以进行视频流的播放。
1,导入播放器模块
import { IjkMediaPlayer } from '@ohos/ijkplayer';
此处为具名导入,因为在ijkplayer代码中的index.ets中有
export { IjkMediaPlayer } from "./src/main/ets/ijkplayer/IjkMediaPlayer";
2,ijkplayer 基本分析
在ijkPlayer代码中的IjkMediaPlayer.ets中,有prepareAsync(),start(),stop(),pause(),reset(),release()等播放处理。

import { IjkPlayerNapi } from "./utils/IjkPlayerNapi";

export class IjkMediaPlayer {
  private static instance: IjkMediaPlayer; // 单例模式设计,确保全局唯一实例
  // 使用私有构造函数防止外部实例化
  private constructor() {
  };

  // 静态 getInstance 方法确保全局唯一实例,懒加载模式,首次调用时创建实例。
  public static getInstance(): IjkMediaPlayer {
    if (!IjkMediaPlayer.instance) {
      IjkMediaPlayer.instance = new IjkMediaPlayer();
    }
    return IjkMediaPlayer.instance;
  }
  
  // 底层原生播放器的 NAPI 接口实例, 通过 setContext 方法注入.
  private ijkplayer_napi: IjkPlayerNapi | null = null;
  // 将 XComponent 的上下文转换为 IjkPlayerNapi 实例, 建立与原生播放器的连接。
  setContext(context: object): void {
    this.ijkplayer_napi = context as IjkPlayerNapi;
  }
  
  // 设置视频数据源 URL
  setDataSource(url: string): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._setDataSource(url);
  }
  
  // 提供完整的播放器生命周期控制,所有方法都先检查 ijkplayer_napi 是否存在。
  prepareAsync(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._prepareAsync();
  }

  start(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._start();
  }

  stop(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._stop();
  }

  pause(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._pause();
  }

  reset(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._reset();
  }

  release(): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._release();
  }

  seekTo(msec: string): void {
    if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._seekTo(msec);
  }
  
  // 设置监听,为各种播放器事件设置监听器, 应用观察者模式,通过监听器机制实现事件通知,解耦播放器和 UI 组件。
  setOnVideoSizeChangedListener(listener: OnVideoSizeChangedListener): void {
    this.mOnVideoSizeChangedListener = listener;
  }

  setOnPreparedListener(listener: OnPreparedListener): void {
    this.mOnPreparedListener = listener;
  }

  setOnCompletionListener(listener: OnCompletionListener): void {
    this.mOnCompletionListener = listener;
  }

  setOnInfoListener(listener: OnInfoListener): void {
    this.mOnInfoListener = listener;
  }

  setOnErrorListener(listener: OnErrorListener): void {
    this.mOnErrorListener = listener;
  }

  setOnBufferingUpdateListener(listener: OnBufferingUpdateListener): void {
    this.mOnBufferingUpdateListener = listener;
  }

  setOnSeekCompleteListener(listener: OnSeekCompleteListener): void {
    this.mOnSeekCompleteListener = listener;
  }

  setOnTimedTextListener(listener: OnTimedTextListener): void {
    this.mOnTimedTextListener = listener;
  }

  // 消息监听机制,
  setMessageListener(): void {
    // 获取相关监听器
    let onPreparedListener = this.mOnPreparedListener;
    let onCompletionListener = this.mOnCompletionListener;
	...
	let messageCallBack = (what: number, arg1: number, arg2: number, obj: string) => {
	  // 核心的事件分发机制,将原生播放器的消息转换为上层回调,支持多种消息类型的处理。
	  if (what == MessageType.MEDIA_PREPARED) {
        if (onPreparedListener !== null)
          onPreparedListener.onPrepared();
      }
	  if (what == MessageType.MEDIA_PLAYBACK_COMPLETE && onCompletionListener != null) {
        onCompletionListener.onCompletion();
      }
	};
	if (!!this.ijkplayer_napi)
      this.ijkplayer_napi._setMessageListener(messageCallBack);
  }
}

3,定义接口

export interface EricMPInterface {
  player: IjkMediaPlayer

  setContext(context: object): void
  play(url: string): void
  pause(): void
  ...
}

4,实现接口

const player: IjkMediaPlayer = IjkMediaPlayer.getInstance()
export class EricMPImpl implements EricMPInterface {
  context?: object
  onPreparedListener?: () => void
  
  setContext(context: object): void {
    this.context = context
  }
  
  setPreparedListener(onPreparedListener: () => void): void {
    this.onPreparedListener = onPreparedListener
  }
  
  play(url: string): void {
    player.setContext(this.context)
	player.setDataSource(url)
	...
	player.setMessageListener()
    player.prepareAsync()
    player.start()
  }
}
export const ericPlayer = new EricMPImpl()

5,自定义播放器组件

// EricVideoView 是一个 HarmonyOS ArkUI 框架的自定义组件,使用了 TypeScript 语法.
@Component
export struct EricVideoView {
  // @Prop 装饰器表示这是一个父组件传递过来的不可变属性,url属性用于接收视频的播放地址.
  @Prop
  url: string
  // 创建 XComponent 控制器,用于控制底层原生组件.
  controller = new XComponentController()
  // @State 装饰器表示这是组件内部的状态变量,preparing用于控制加载状态的显示,初始值为 true.
  @State
  preparing: boolean = true

  aboutToDisappear(): void {
    ericPlayer.release()
  }

  build() {
    // 使用 Stack 布局实现组件叠加效果,底层是视频播放器,顶层是加载状态显示
    Stack() {
	  // XComponent是 HarmonyOS 中用于集成原生组件的容器;type: 'surface'表示使用 Surface 类型的原生组件;libraryname: 'ijkplayer_napi'指定了使用的原生库名称;controller关联了之前创建的控制器
      XComponent({
        id: 'xcomponentId',
        type: 'surface',
        libraryname: 'ijkplayer_napi',
        controller: this.controller
      })
        .width('100%')
        .height('100%')
        .alignSelf(ItemAlign.Center)
        .onLoad((context) => {
          if (!context) {
            return
          }
		  // 将原生组件上下文绑定到媒体播放器
          ericPlayer.setContext(context)
		  // 设置准备完成监听器,准备完成后隐藏加载状态
          ericPlayer.setPreparedListener(() => {
            this.preparing = false
          })
          setTimeout(() => {
            ericPlayer.play(this.url)
          }, 100)
        })
	  if (this.preparing) {
        Image($r('app.media.preparing'))
          .width('100%')
          .height('100%')
      }
    }
    .alignContent(Alignment.Center)
  }
}

6,使用播放器组件

EricVideoView(
  { url: this.getVideoUrl()}
)

在调用时只需要传入url就可以使用播放器组件,这是因为在播放器组件定义中的 @Prop 装饰器声明了url是一个父组件传递的属性,并且是唯一的必填属性。其他属性如controller和preparing都是组件内部定义的,不需要外部传入。另外播放器组件也没有定义自定义的构造函数,这样也就意味着只需要传递包含url属性的参数对象即可创建组件实例。
7,XComponent 分析
XComponent 是 HarmonyOS ArkUI 框架中的一个重要组件,用于实现 ArkUI 与原生代码(C/C++)的桥接。
基本概念:
XComponent(扩展 Component)是 HarmonyOS 提供的一种特殊组件,它允许开发者在 ArkUI 应用中集成和使用原生代码实现的组件。它充当了 ArkUI 框架与底层原生代码之间的桥梁。
核心作用:
(1) 跨语言集成 连接 ArkUI(TypeScript/ArkTS)与原生代码(C/C++),实现高性能的图形渲染和复杂计算,集成第三方原生库(如媒体播放器、地图 SDK 等)。
(2) 功能扩展 突破 ArkUI 框架的功能限制,访问系统底层能力,实现高性能的自定义组件。
工作原理:
ArkUI应用层 (TypeScript/ArkTS)

XComponent组件

原生能力适配层 (NAPI)

系统底层能力 (C/C++)
XComponent 通过 NAPI(Node-API)实现与原生代码的通信,支持双向数据传递和方法调用。
基本用法:
XComponent({
id: 'uniqueId', // 组件唯一标识
type: 'surface', // 组件类型
libraryname: 'library', // 原生库名称
controller: this.controller // 控制器实例
}) .width('100%') .height('100%')
.onLoad((context) => {
// 组件加载完成回调
})
.onDestroy(() => {
// 组件销毁回调
})
重要属性:
id:组件的唯一标识符,用于在原生代码中识别和操作特定的 XComponent 实例。
type:surface: 用于图形渲染,提供 Surface 画布;component: 用于非图形的原生组件。 libraryname:指定要加载的原生动态库名称,原生库需要提前编译并放置在指定位置。
controller:XComponentController实例,用于在 ArkUI 层控制原生组件。
生命周期回调:
onLoad:当原生组件加载完成时调用,接收原生组件的上下文对象,通常在这里进行初始化和绑定操作。
onDestroy:当原生组件即将销毁时调用,用于释放原生资源,防止内存泄漏。