OpenMax IL层设计分析总结

2023-10-19 18:58
文章标签 分析 设计 总结 il openmax

本文主要是介绍OpenMax IL层设计分析总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 一、OpenMax的设计理念与特性点
      • 设计理念---媒体框架的抽象/可移植性/异步处理/组件组合
      • 设计特性---组件化API/方便新增解码器/方便扩展/支持动态链接/可配置
    • 二、OpenMax的设计点分析
      • 2.1 【兼容性】版本兼容性设计---组件版本号\指针函数\入参void指针
      • 2.2 【扩展性】新增组件设计---动态库新增组件
      • 2.3 【扩展性】新增组件新增私有功能 设计---扩展参数设置类型枚举
      • 2.4 【性能】异步处理性能设计---配置异步通知 / buffer轮转回调通知
      • 2.5 【编解码领域】组件间tunnel模式建立buffer自动轮转通路 设计点
      • 2.6 【编解码领域】组件基础抽象设计---handle封装 / port抽象 / 内外buffer /state状态
      • 2.7 【性能】0拷贝的buffer share与buffer透传(待更新)
    • 三、关于多媒体框架思考
      • 为什么编解码这么需要框架支持---编解码需求/硬解私有/用户隔离
      • 如何才能建立一个好的通用多媒体框架(待更新)
      • 私有多媒体通路能从OpenMax吸取哪些好设计(待更新)
    • 参考

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/

这篇关于OpenMax IL层设计分析总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/241724

相关文章

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Windows Docker端口占用错误及解决方案总结

《WindowsDocker端口占用错误及解决方案总结》在Windows环境下使用Docker容器时,端口占用错误是开发和运维中常见且棘手的问题,本文将深入剖析该问题的成因,介绍如何通过查看端口分配... 目录引言Windows docker 端口占用错误及解决方案汇总端口冲突形成原因解析诊断当前端口情况解

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

Python 迭代器和生成器概念及场景分析

《Python迭代器和生成器概念及场景分析》yield是Python中实现惰性计算和协程的核心工具,结合send()、throw()、close()等方法,能够构建高效、灵活的数据流和控制流模型,这... 目录迭代器的介绍自定义迭代器省略的迭代器生产器的介绍yield的普通用法yield的高级用法yidle

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java