OpenMax IL层设计分析总结


OpenMax相关设计非常优秀,本文主要从设计角度对OpenMax的IL层进行个人分析总结。
OpenMax的基础概念及IL层相关基础介绍请参考 个人另一篇总结编解码抽象层OpenMax简介:https://blog.csdn.net/runafterhit/article/details/119961868

一、OpenMax的设计理念与特性点

我们先简单梳理一下OpenMax的设计理念和特性,好以此来分析后续设计点。(这部分上一篇博文已总结)

设计理念—媒体框架的抽象/可移植性/异步处理/组件组合

为了提升多媒体的可移植性,同时又能兼顾对接当前设备媒体多样性方案,OpenMAX IL相对这些方案抽象到更高的层次,通常意味着一个多媒体框架。OpenMax IL层的接口来源于媒体框架层次,设计实现上 要更容易集成新的解码器,其他比如文件处理也可以进行方便添加,考虑到各种可扩展性。同时设计上使用高度异步通信方式,者可以是处理通过一个或多个执行线程、或 专用硬件IP(硬件加速),通过多组件链接处理的方式 也给了架构更大的灵活性和效率。

设计特性—组件化API/方便新增解码器/方便扩展/支持动态链接/可配置

基于组件compoment化的核心API接口设计,保持灵活性。
能方便的新增集成新解码器codec的能力。
同时给Khronos Group和供应商提供针对音频、视频、图像领域良好扩展性;
可以被实现为静态库 或者 动态链接库。
提供对父平台(如多媒体frameworks)的关键特性和配置可选项。
保持client和解码器codec、解码器codec之间通信的简易性;

二、OpenMax的设计点分析

梳理openmax真正的设计点,按个人的思路总结如下:

2.1 【兼容性】版本兼容性设计—组件版本号\指针函数\入参void指针

(1)组件版本号:由于Omx的组件Component可以动态加载,每个组件的.so由芯片vender方实现方案,他们可能存在不同的版本,也可能替换方案,因此omx提供了OMX_GetComponentVersion接口,来查询组件的版本号,调用者需要根据版本号判断使用策略(通常framework可能只适配一种组件,就只做校验用,当有多种版本组件就需要参考跑策略)。版本号一般都是静态编译阶段 通过一组8bit字段构成,如下:

// omx的版本号定义数据结构
typedef union OMX_VERSIONTYPE {
    struct {
        OMX_U8 nVersionMajor;   /**< Major version accessor element */
        OMX_U8 nVersionMinor;   /**< Minor version accessor element */
        OMX_U8 nRevision;       /**< Revision version accessor element */
        OMX_U8 nStep;           /**< Step version accessor element */
    } s;
    OMX_U32 nVersion;           /**< 32 bit value to make accessing the version easily done in a single word size copy/compare operation */
} OMX_VERSIONTYPE;
// 典型的omx版本号举例
#define OMX_VERSION_MAJOR 1
#define OMX_VERSION_MINOR 2
#define OMX_VERSION_REVISION 0
#define OMX_VERSION_STEP 0

#define OMX_VERSION ((OMX_VERSION_STEP<<24) | (OMX_VERSION_REVISION<<16) | (OMX_VERSION_MINOR<<8) | OMX_VERSION_MAJOR)

(2)指针函数:为了统一对外接口,封装不同芯片厂商实现差异,omx接口基本都是通过函数指针调用,如下SetParameter,用户通过core的API宏函数OMX_SetParameter,实际调用的是组件的SetParameter函数指针,SetParameter函数指针 通常在组件初始化OMX_ComponentInit时候注册,实现为自己芯片硬解方案对应封装如xxx_SetParameter。

// core接口
#define OMX_SetParameter(                                   \
        hComponent,                                         \
        nParamIndex,                                        \
        pComponentParameterStructure)                        \
    ((OMX_COMPONENTTYPE*)hComponent)->SetParameter(         \
        hComponent,                                         \
        nParamIndex,                                        \
        pComponentParameterStructure)
// 组件接口
    OMX_ERRORTYPE (*SetParameter)(
            OMX_IN  OMX_HANDLETYPE hComponent, 
            OMX_IN  OMX_INDEXTYPE nIndex,
            OMX_IN  OMX_PTR pComponentParameterStructure);
// 初始化组件时 注册函数指针
OMX_ERRORTYPE OMX_ComponentInit(OMX_HANDLETYPE phandle){
	// 略
	phandle->SetParameter = xxx_SetParameter; // 对接真正的接口实现
	// 略
}

(3)入参void指针 :为了保证各种组件实现的兼容性,omx入参基本都是通过 空指针传递,配合参数类型index 或者 入参size来描述 参数数据结构类型;比如上面的setParameter函数,core接口是一个宏函数,实际调用的component组件的接口,入参通过枚举OMX_INDEXTYPE描述是什么类型的参数,参数数据通过void*类型指针传入。这样可以通过一个setParam接口设置各种类型配置(包括可扩展的私有数据结构)。

typedef enum OMX_INDEXTYPE {
    OMX_IndexComponentStartUnused = 0x01000000,
    OMX_IndexParamPriorityMgmt,             /**< reference: OMX_PRIORITYMGMTTYPE */
    OMX_IndexParamAudioInit,                /**< reference: OMX_PORT_PARAM_TYPE */
    OMX_IndexParamImageInit,                /**< reference: OMX_PORT_PARAM_TYPE */
    OMX_IndexParamVideoInit,                /**< reference: OMX_PORT_PARAM_TYPE */
    OMX_IndexParamOtherInit,                /**< reference: OMX_PORT_PARAM_TYPE */
    OMX_IndexParamNumAvailableStreams,      /**< reference: OMX_PARAM_U32TYPE */
    // 略
 } OMX_INDEXTYPE;
typedef void* OMX_PTR;

2.2 【扩展性】新增组件设计—动态库新增组件

(1)动态库加载设计:omx设计上 每一个解码器组件 都做成一个单独的.so来 通过动态加载实现。通过OMX_GetHandle传入解码器名称,创建解码器实例,在这个过程加载so并获取对应的OMX_ComponentInit符号名称进行初始化,在so中注册好对应的指针函数等,提供后续的访问,关键部分如下:

// OMX_GetHandle创建解码器实例关键部分
OMX_API OMX_ERRORTYPE OMX_APIENTRY OMX_GetHandle(
    OMX_OUT OMX_HANDLETYPE* pHandle, 
    OMX_IN  OMX_STRING cComponentName,
    OMX_IN  OMX_PTR pAppData,
    OMX_IN  OMX_CALLBACKTYPE* pCallBacks) {
    // 略
	dlhandle = dlopen (libname, RTLD_LAZY | RTLD_GLOBAL); // 通过dlopen库称获取到库的handle
		 // libname通常用cComponentName按固定规则生成
	entry = dlsym (dlhandle, "OMX_ComponentInit")); // 通过库的访问handle获取到初始化函数符号地址
	ret = entry(*pHandle); // 通过OMX_ComponentInit的符号,调用到so库实现进行真正的解码器初始化
}
// OMX_ComponentInit(在对应so库)中关键实现
OMX_ERRORTYPE OMX_ComponentInit (OMX_HANDLETYPE handle) {
  OMX_COMPONENTTYPE *phdl = (OMX_COMPONENTTYPE *) handle; // 把handle强转为实例标准上下文
  p_hdl->SendCommand = SendCommand; // 注册对用组件回调
  p_hdl->SetParameter = SetParameter;
  p_hdl->SetConfig = SetConfig;
  p_hdl->EmptyThisBuffer = EmptyThisBuffer;
  // 略
}

通过这种库加载的方式,把解码器的实现封装到一个一个so中隔离开来,需要用到的时候再载入使用;

2.3 【扩展性】新增组件新增私有功能 设计—扩展参数设置类型枚举

(1)扩展参数设置类型枚举:omx的通用常见调用流程都有定义对应的指针函数如SendCommand、SetConfig配置参数,EmptyThisBuffer等buffer流程调用。我们前面已经了解了SetParameter基本用法,通过cmd类型枚举表示设置什么参数,void*传递参数数据。需要扩展新的设置时,就是扩展新的参数类型,通过组件的GetExtensionIndex函数指针传入函数名称获取扩展的函数类型。

// 标准的cmd类型在定义的时候已经预留了很多中间位,都是按段定义的
typedef enum OMX_INDEXTYPE {
    OMX_IndexComponentStartUnused = 0x01000000,
    OMX_IndexParamPriorityMgmt,             /**< reference: OMX_PRIORITYMGMTTYPE */
    OMX_IndexParamAudioInit,                /**< reference: OMX_PORT_PARAM_TYPE */
    // 略
    OMX_IndexPortStartUnused = 0x02000000,
    OMX_IndexParamPortDefinition,           /**< reference: OMX_PARAM_PORTDEFINITIONTYPE */
    OMX_IndexParamCompBufferSupplier,       /**< reference: OMX_PARAM_BUFFERSUPPLIERTYPE */ 
    OMX_IndexReservedStartUnused = 0x03000000,
	// 略
    /* Audio parameters and configurations */
    OMX_IndexAudioStartUnused = 0x04000000,
    OMX_IndexParamAudioPortFormat,          /**< reference: OMX_AUDIO_PARAM_PORTFORMATTYPE */
    // 略
    OMX_IndexMax = 0x7FFFFFFF
} OMX_INDEXTYPE;
// 通过core的api接口,调用到组件的函数指针GetExtensionIndex查询扩展命令
#define OMX_GetExtensionIndex(                              \
        hComponent,                                         \
        cParameterName,                                     \
        pIndexType)                                         \
    ((OMX_COMPONENTTYPE*)hComponent)->GetExtensionIndex(    \
        hComponent,                                         \
        cParameterName,                                     \
        pIndexType)
// 组件的查询扩展命令函数指针,传入函数方法名称或者 方法枚举,来设置使用
OMX_ERRORTYPE (*GetExtensionIndex)(
        OMX_IN  OMX_HANDLETYPE hComponent,
        OMX_IN  OMX_STRING cParameterName,
        OMX_OUT OMX_INDEXTYPE* pIndexType);

2.4 【性能】异步处理性能设计—配置异步通知 / buffer轮转回调通知

性能方面的设计是多媒体框架重要的一环,这里面主要包含两方面
基于事件和回调的异步处理机制
(1)配置异步通知:是接口是同步的阻塞函数还是异步的非阻塞函数,omx框架中 大量类型的接口 都建议是设计层 异步接口,比如参数设置 port参数调整生效等,通过异步事件回调来通知上层动作何时完成。
(2)buffer轮转回调通知:最原始的就是把输入输出buffer送解码器 然后 轮询 的aquire输出dequeue输入获取,这种方式基于循环遍历,占用调度性能而且不够及时,omx通过EmptyBufferDone和EmptyBufferDone事件来让上层去取帧更及时而且不需要循环调度。

// 在OMX_GetHandle创建解码器实例的时候 设置回调中就包含事件通知 回调。
OMX_API OMX_ERRORTYPE OMX_APIENTRY OMX_GetHandle(
    OMX_OUT OMX_HANDLETYPE* pHandle, 
    OMX_IN  OMX_STRING cComponentName,
    OMX_IN  OMX_PTR pAppData,
    OMX_IN  OMX_CALLBACKTYPE* pCallBacks); // 注册回调类型
// 注册的回调类型如下,包含事件、输入帧使用完成、输出帧填写完成
typedef struct OMX_CALLBACKTYPE {
   OMX_ERRORTYPE (*EventHandler)( // 事件通知回调
        OMX_IN OMX_HANDLETYPE hComponent,
        OMX_IN OMX_PTR pAppData,
        OMX_IN OMX_EVENTTYPE eEvent,
        OMX_IN OMX_U32 nData1,
        OMX_IN OMX_U32 nData2,
        OMX_IN OMX_PTR pEventData);
    OMX_ERRORTYPE (*EmptyBufferDone)( // 输入帧使用完成事件回调,可以在填写传入组件
        OMX_IN OMX_HANDLETYPE hComponent,
        OMX_IN OMX_PTR pAppData,
        OMX_IN OMX_BUFFERHEADERTYPE* pBuffer);
    OMX_ERRORTYPE (*FillBufferDone)( //输入帧填写完成事件回调,可以拿输出去使用
        OMX_IN OMX_HANDLETYPE hComponent,
        OMX_IN OMX_PTR pAppData,
        OMX_IN OMX_BUFFERHEADERTYPE* pBuffer);
} OMX_CALLBACKTYPE;

备注:omx设计文档中有说明,事件回调的处理函数必须 是 满足线程安全设计,回调可能同时发生(同一个回调同时并发,或者不同回调同时并发),要注意并发执行时的临界区安保护设计,如用信号量等机制;底层不能在非线程中执行回调(比如硬件中断)避免上层回调中发生阻塞 引发系统异常;避免在 回调中执行耗时操作,实际项目实现中,往往 底层的事件通知 是用独立的线程,而 上层当回调可能出现较多耗时也需要 用 单独的线程 收到事件执行 而非在回调直接实现。

2.5 【编解码领域】组件间tunnel模式建立buffer自动轮转通路 设计点

前面buffer轮转回调通知的介绍 说明了buffer轮转的异步通知设计机制,通过这种设计,可以实现底层组件之间的串联调度不需要framework或者用户参与buffer轮转,直接让组件之间通过回调事件管理buffer轮转,也叫组件之间的tunnel通信模式。如下图中,source Component与Host Component是非tunnel的普通调用方式,需要framework根据EmptyBufferDone和EmptyBufferDone事件来触发轮转,而后面几个组件就是tunnel模式。
在这里插入图片描述
tunnel模式举例 :camera采集组件—mjpeg解码组件—render渲染显示组件 三者绑定了tunnel模式,当camera采集完一帧,底层驱动触发了filldone事件把帧EmptyThisBuffer送到 mjpeg解码组件,mjpeg解码组件解码完成把帧EmptyThisBuffer送到render渲染显示模块渲染显示,无论送帧还帧,都不要用户或者framework参与buffer轮转调度。

2.6 【编解码领域】组件基础抽象设计—handle封装 / port抽象 / 内外buffer /state状态

(1)handle封装:用户或者framework对于组件的访问都是通过handle并调用core api传入handle进行访问,只需要包含omx_core.h的头文件即可,这个handle类型OMX_HANDLETYPE实际就是一个void*类型指针,指向组件的上下文。上下文真正的定义是在omx_component.h的头文件,定义真正数据结构OMX_COMPONENTTYPE,用户不可见,omx的core核心实现才可见,以此来实现对组件内部细节的封装。同时OMX_COMPONENTTYPE中还包含pComponentPrivate指针 指向真正的解码器内部私有上下文,类似驱动访问file文件句柄的private,这个上下文只在组件内部访问可见,omx core也不可见,一般在OMX_ComponentInit时候malloc私有上下文挂载到pComponentPrivate,后续组件访问中内部使用;

typedef void* OMX_HANDLETYPE; // 用户和framework看到的handle
typedef struct OMX_COMPONENTTYPE { // omx core实现中把handle转换为真的上下文实现
    OMX_U32 nSize;
    OMX_VERSIONTYPE nVersion;
    OMX_PTR pComponentPrivate; // omx component组件私有数据结构上下文访问
    OMX_PTR pApplicationPrivate;
    OMX_ERRORTYPE (*GetComponentVersion)(
            OMX_IN  OMX_HANDLETYPE hComponent,
            OMX_OUT OMX_STRING pComponentName,
            OMX_OUT OMX_VERSIONTYPE* pComponentVersion,
            OMX_OUT OMX_VERSIONTYPE* pSpecVersion,
            OMX_OUT OMX_UUIDTYPE* pComponentUUID);
    OMX_ERRORTYPE (*SendCommand)(
            OMX_IN  OMX_HANDLETYPE hComponent,
            OMX_IN  OMX_COMMANDTYPE Cmd,
            OMX_IN  OMX_U32 nParam1,
            OMX_IN  OMX_PTR pCmdData);
	// 略     
} OMX_COMPONENTTYPE;

(2)port抽象:port是对于数据端口的抽象,分输入in port和输出out port,一般常见组件都是一个输入一个输出,如果是source组件可能只有输出,最后一级如render渲染显示组件可能只有输入。有些组件可能存在多个port,比如支持一进多出的处理组件输出存在多个port;
在这里插入图片描述
(3)内外buffer模式:buffer模式表示某个port的内存是组件内部申请还是外部送入的,这是常见的解码器用法,使用外部buffer能更方便上层使用管理。
(4)state状态机 :每个组件都有一个状态机管理内部的状态切换,每个组件首先被认为是卸载的,组件被CoreAPI进行加载,然后在通信过程中与其他状态转换,当遇到无效数据时组件可能进入Invalid无效状态。不同的状态下组件的行为操作存在差异和限制。状态的切换通过SendCommand调用OMX_CommandStateSet类型命令进行设置。
在这里插入图片描述

2.7 【性能】0拷贝的buffer share与buffer透传(待更新)

三、关于多媒体框架思考

为什么编解码这么需要框架支持—编解码需求/硬解私有/用户隔离

(1)随着音视频领域技术发展,对编解码的性能要求日益加剧,fhd到4k到8k,帧率从24hz到60hz到120hz,还有hdr等提升显示还原效果等技术出现,特别是手机等可移动设备发展,以往的软解码的方式难以保证性能稳定,并且软解对于cpu性能要求非常高
(2)于是各种设备厂商都在通过硬解码进行加速,硬解的方案往往非常私有化,解码协议实现方案多种多样,很多协议模糊处依靠大量问题处理迭代累计,属于关键技术资产,形成各家技术壁垒,通常都通过闭源发布 ,比如跑在mcu上 而非 放到kernel设备中 来规避内核Licence传染,玩法自然差异非常大。并且可能同一个设备上 不同解码器 来着不同团队,甚至经常是不同公司
(3)而用户自然不能看到这些差异,因此用户通常面对的是多媒体framework甚至是更高层次的封装,framework往下来对接 各种软解码、硬解码器方案,并且这些解码器之间并不是独立的,他们经常需要组合使用,并且还对性能有很高要求(软件调度、异步处理),这里面如果没有一套标准来对接,将会是极其复杂的。
(4)再进一步,不同平台有自己的framework,如果每个芯片厂商都去适配一次,将会极其困难,严格意义上讲,openmax应该是算 对接framework和硬解码方案 之间的封装层,并不是一个framework,比如ffmpeg、Gstreamer、Android的mediaCodec都可以和openmax对接,而硬件厂商只需要对接openmax即可。

如何才能建立一个好的通用多媒体框架(待更新)

私有多媒体通路能从OpenMax吸取哪些好设计(待更新)

参考

官网IL层详细文档:https://www.khronos.org/files/openmax_il_spec_1_0.pdf
编解码抽象层OpenMax简介:https://blog.csdn.net/runafterhit/article/details/119961868
omx头文件:http://androidxref.com/9.0.0_r3/xref/frameworks/native/headers/media_plugin/media/openmax/

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值