该篇文章对OpenMAX的数据结构进行概要描述,包括OpenMAX的一些官方定义的头文件介绍,以及各种结构体数据介绍:比如OpenMAX组件结构体描述、PORT端口结构体描述。并对组件内部线程的大概结构以及组织方式进行介绍,本文章的目标是可以实现一个有基本功能的组件。
该文章中提到的IL Client可以看作是组件的使用者,负责组件的创建、销毁、命令控制等等。注意本文章需要结合例程代码去看,否则会觉得本文不知所云。代码参考OpenMAX编程-组件一文中的OpenMAX IL sample下载链接
[头文件下载地址]:https://www.khronos.org/files/openmax/headers/omx_il_v1/omx_il_v1.zip
OpenMAX官方提供了一系列的头文件,里面定义了包括组件结构体、音视频相关结构体、命令控制API等等。这些定义可以使得Client控制音频(audio)、视频(video)、图像(image)等类型的多媒体组件。其它(other)域则提供了一些额外的组件功能,比如音视频同步(可以用clock实现)。
头文件列表如下所示:
文件 | 描述 |
---|---|
OMX_Types.h | 数据类型,里面定义了组件类型、输入输出等 |
OMX_Core.h | IL层的核心API,有命令枚举、状态枚举、组件注册/初始化等等 |
OMX_Component.h | 组件相关的API,有端口类型与定义、组件回调函数成员定义等 |
OMX_Audio.h | 与音频相关的结构体定义 |
OMX_IVCommon.h | 图像与音频通用的一些结构体定义 |
OMX_Video.h | 视频相关的结构体定义 |
OMX_Image.h | 图像相关的结构体定义 |
OMX_Other.h | 其它部分的结构体定义 (包含A/V同步) |
OMX_Index.h | OpenMAX定义的数据结构索引 |
值得注意的是,在OMX_Types.h文件里面有这么几个定义:OMX_IN
,OMX_OUT
,OMX_INOUT
,分别代表了仅输入、仅输出、输入输出。这些宏定义并没有对应的值,只是个空的定义,常用于标识函数的参数传递方向,便于函数调用者(一般是IL Client)搞清楚哪些参数是输入的,哪些参数是输出的,避免一些低级的编程错误。这种形式的用法在其它的编程当中也可以借鉴,有了标识的函数参数会使得函数调用者更加地省心,避免在函数使用的时候出现一些不必要的错误。
主要是一些枚举变量结构体原型,其类型主要如下所示:
OMX_Types枚举变量
可以看出来,其主要分为以下类型:
枚举成员 | 意义 | 参数 |
---|---|---|
OMX_CommandStateSet | 改变组件的状态 | OMX_STATETYPE-要转换的状态 |
OMX_CommandFlush | 冲洗组件内部某个端口的buffer队列 | OMX_U32-端口号 |
OMX_CommandPortDisable | 禁止使能组件内部的某个端口 | OMX_U32-端口号 |
OMX_CommandPortEnable | 使能组件内部某个端口 | OMX_U32-端口号 |
OMX_CommandMarkBuffer | 标记一个buffer并指定哪个组件会触发收到的事件标记 | OMX_U32-端口号(还有命令数据OMX_MARKTYPE*-buffer与组件) |
对于Client来说,命令的发送是非阻塞的,当Client把命令发送给组件时,组件会把收到的命令放到一个命令队列(或管道)里面,然后函数就会立刻返回了。当组件内部取出某个命令并执行完毕之后会进行一个事件回调来通知Client某个命令已经执行完毕,并返回命令执行的状态(失败/成功)。
根据OMX标准,定义了如下所示的组件状态集,并且它们之间还有如下的状态转换条件:
组件状态转换
对于这几种状态的说明如下:
举例说明下传输buffer、处理buffer、拥有资源三者的区别:
当然,在实际的编程实现当中在组件内部也不必拘泥于这几种状态,比如可以新增一些状态或者去掉一些状态,比如很大可能OMX_StateWaitForResources状态就用不到,因为资源分配总是可以在Loaded或者Idle状态中分配完成,而这两个状态中间没必要增加OMX_StateWaitForResources这样一个状态。另外对于各个状态下的数据处理也可以灵活运用,比如OMX_StatePause状态下就可以选择传输或者不传输buffer,如果传输的话就需要把buffer缓存起来,等待组件恢复运行时继续处理(可能会发生缓冲区满的情况,需要做相应的措施)。
OMX所有的API返回错误值都在该枚举类型里面定义,该枚举类型里面包含了绝大多数的错误状态类型(实际应用当中基本够用)。当然,如果觉得还是需要自己定义错误值的话也可以,从0x90000000到0x9000FFFF这个范围内就是自定义错误的枚举值范围。
举例部分错误值的意义(错误值定义比较多),其余的在头文件里面可以看到相关错误值的详细意义注释:
组件通过回调机制向IL Client发送事件消息,事件消息的类型如下:
OMX_BUFFERFLAG_EOS
,在返回的参数中,nData1指定输出端口的portindex,nData2则是传递nFlags给Client。如果组件是sink类型的(数据流终点),则nData1传递的是输入端口的portindex。用来指明tunnel(建立隧道链接)的port中哪个是数据提供者(buffer supplier port),buffer提供者port可能有属于自己的独立分配的buffer内存空间,也可能与同一组件内部其它的port共享同一个buffer内存空间,由此可见该枚举类型用于隧道链接的建立设置(tunnel setup)。该枚举类型成员有如下几个:
原型:
typedef struct OMX_COMPONENTREGISTERTYPE
{
const char * pName;
OMX_COMPONENTINITTYPE pInitialize;
} OMX_COMPONENTREGISTERTYPE;
该结构体负责组件的注册工作,pName
是组件的名字,通常是唯一的,pInitialize
是组件的初始化函数,该成员类型的定义是:typedef OMX_ERRORTYPE (* OMX_COMPONENTINITTYPE)(OMX_IN OMX_HANDLETYPE hComponent);
由此可见hComponent
被提前分配,然后被传递给初始化回调函数,在初始化回调函数里面进行整个组件资源分配、环境的初始化工作。
通常情况下,该结构体总是以数组成员的方式被定义在一个OMX_ComponentRegistered[]
类型的数组当中,属于全局、静态的。要创建一个组件时需要根据组件的名字赖在数组里面进行匹配,如果匹配到相关的数组成员,就调用初始化回调函数正式初始化创建组件。
该结构体是buffer的抽象化实例对象。前面的几篇文章当中也提到过,在OpenMAX组件当中,buffer的传递经常是以buffer的描述符(往往包含buffer的地址、长宽、格式等信息)来传递的,而不是实际的buffer本身,当需要处理的时候才真正地去访问实际的buffer内存,这是为了减少大buffer的拷贝,降低CPU的负载。事实上Linux内核里面的V4L2架构也是采用buffer描述符的形式来传递buffer数据,这种设计理念与原则可以延伸到所有涉及到大量的buffer操作的系统当中,参考意义非常大。
OpenMAX中提到的buffer在大部分情况下就是说的这个结构体实例,buffer的传递就指的是该结构体实例的传递,buffer的处理才是真正访问内存的部分。
原型:
typedef struct OMX_BUFFERHEADERTYPE
{
OMX_U32 nSize;
OMX_VERSIONTYPE nVersion;
OMX_U8* pBuffer;
OMX_U32 nAllocLen;
OMX_U32 nFilledLen;
OMX_U32 nOffset;
OMX_PTR pAppPrivate;
OMX_PTR pPlatformPrivate;
OMX_U32 nOutputPortPrivate;
OMX_U32 nInputPortPrivate;
OMX_HANDLETYPE hMarkTargetComponent;
OMX_PTR pMarkData;
OMX_U32 nTickCount;
OMX_TICKS nTimeStamp;
OMX_U32 nFlags;
OMX_U32 nOutputPortIndex;
OMX_U32 nInputPortIndex;
} OMX_BUFFERHEADERTYPE;
OMX_CommandMarkBuffer
命令的时候把标记数据的指针传递给组件,组件会把相关的标记数据拷贝到该成员中,最后传递给hMarkTargetComponent
。#define OMX_BUFFERFLAG_EOS 0x00000001
#define OMX_BUFFERFLAG_STARTTIME 0x00000002 //由数据源组件产生,此时buffer的nTimeStamp就是起始基准值
#define OMX_BUFFERFLAG_DECODEONLY 0x00000004
#define OMX_BUFFERFLAG_DATACORRUPT 0x00000008 //由IL Client产生,表明此buffer是坏的
#define OMX_BUFFERFLAG_ENDOFFRAME 0x00000010
举例来说明下各个成员的作用,比如说编码组件需要对一个1280*720大小的帧进行编码,那么它可能预先需要申请一个1280*720*4/3大小的buffer(原始数据的四分之三),由于编码出来的数据大小不一定,所以nFilledLen
与nAllocLen
大小就不一致,如果编码出来的数据需要加上一个前缀(比如表示其是H264还是其它的编码之类的),那么nOffset
就不会为0了:
编码的Buffer结构
在OpenMAX中,有一个很重要的点就是回调机制,回调机制提供了组件到IL Client的单向数据传递,组件里面的回调常用于通知IL Client某些命令已经执行完毕(比如状态设置),下面几种情况下会触发组件的回调机制:
OMX_StateInvalid
状态时就会触发回调机制。该结构体的原型如下:
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;
上面三个回调函数成员需要IL Client实现,并在OMX_GetHandle
函数里面设置填充到组件的自定义结构体成员里面然后就可以在组件内部去调用这些回调函数了。
OMX_EVENTTYPE
枚举定义了组件可以产生的事件种类。nData1
参数一般会放置OMX_COMMANDTYPE
枚举类型(该命令被组件处理完毕)或者OMX_ERRORTYPE
枚举类型(组件处理命令时发生错误或其它错误);nData2
可以放置一些额外的参数(比如状态转换命令时的目的状态OMX_STATETYPE
枚举)。pEventData
是与事件相关的特定类型的数据,类型由IL Client与组件进行协商规定,该回调是加锁调用的,所以IL Client实现该回调的时候需要在5ms之内返回。下表给出了几种事件和与之对应的各个参数组合:eEvent | nData1 | nData2 | pEventData |
---|---|---|---|
OMX_EventCmdComplete | OMX_CommandStateSet | 要到达的状态 | 空 |
OMX_CommandFlush | 端口号 | 空 | |
OMX_CommandPortDisable | 端口号 | 空 | |
OMX_CommandPortEnable | 端口号 | 空 | |
OMX_CommandMarkBuffer | 端口号 | 空 | |
OMX_EventError | 错误码 | 0 | 空 |
OMX_EventMark | 0 | 0 | 与mark相关的数据 |
OMX_EventPortSettingsChanged | 端口号 | 0 | 空 |
OMX_EventBufferFlag | 端口号 | nFlags | 空 |
OMX_EventResourcesAcquired | 0 | 0 | 空 |
EmptyBufferDone
该回调函数用于组件从输入端口(Input Port)拿出一帧数据还回给IL Client(可以看出这种状态下默认IL Client送给组件的数据已经被拷贝到组件私有内存空间了,所以可以直接还回),但是很多时候组件并不会自己再次开辟一个内存空间,而是与IL Client公用同一个内存空间,这时IL Client需要对buffer进行引用计数,并且在组件处理完毕数据之前是不应该还回数据的,所以这个回调函数很多时候并不用实现。
FillBufferDone
组件调用该回调从组件的输出端口(Output Port)拿出一帧数据还回给IL Client,道理同上。需要注意的是,这三个回调函数都是在加锁的情况下被调用,所以IL Client在实现这些函数的时候要保证能够在5ms之内返回。
OMX_PORT_PARAM_TYPE
typedef struct OMX_PORT_PARAM_TYPE {
OMX_U32 nSize;
OMX_VERSIONTYPE nVersion;
OMX_U32 nPorts;
OMX_U32 nStartPortNumber;
} OMX_PORT_PARAM_TYPE;
用于指定组件端口的数量(nPorts
)与起始标号(nStartPortNumber
)。
OMX_PARAM_PORTDEFINITIONTYPE
typedef struct OMX_PARAM_PORTDEFINITIONTYPE {
OMX_U32 nSize;
OMX_VERSIONTYPE nVersion;
OMX_U32 nPortIndex;
OMX_DIRTYPE eDir;
OMX_U32 nBufferCountActual;
OMX_U32 nBufferCountMin;
OMX_U32 nBufferSize;
OMX_BOOL bEnabled;
OMX_BOOL bPopulated;
OMX_PORTDOMAINTYPE eDomain;
union {
OMX_AUDIO_PORTDEFINITIONTYPE audio;
OMX_VIDEO_PORTDEFINITIONTYPE video;
OMX_IMAGE_PORTDEFINITIONTYPE image;
OMX_OTHER_PORTDEFINITIONTYPE other;
} format;
} OMX_PARAM_PORTDEFINITIONTYPE;
端口的实例化对象,一个该结构体类型的变量就用于代表组件的一个端口。
1. nPortIndex
:只读,表明该端口的索引号,对于同一个组件来说,其端口号是各不相同的,而不同的组件的端口号可以相同。
2. eDir
:指定端口的数据流向,(OMX_DirInput
-输入,OMX_DirOutput
-输出)。
3. bEnabled
:表明端口是否被使能。OMX_CommandPortEnable
或者OMX_CommandPortDisable
。
4. eDomain
:表示该端口所在的域,有OMX_PortDomainAudio
,OMX_PortDomainVideo
,OMX_PortDomainImage
,OMX_PortDomainOther
四种枚举。
5. format
:共用体类型,根据eDomain
指定的域来选择相应的结构体来填充。
下面列举部分OpenMAX官方提供的宏定义,专门用来帮助IL Client来完成对组件的各种操作,具体宏定义的原型就不再详述了,可以根据名字参照OpenMAX提供的头文件代码查看并使用。
OMX_SendCommand
:向指定的组件发送命令。OMX_CommandStateSet
:设置指定组件的状态。OMX_Get/SetParameter
:获取/设置指定组件的参数。OMX_Get/SetConfig
:获取/设置指定组件的配置。OMX_EmptyThisBuffer
:像指定组件传递buffer数据,传递buffer数据到组件的输入端口。OMX_FillThisBuffer
:像指定组件还回buffer数据,传递buffer数据到组件的输出端口。OMX_Init/OMX_Deinit
:组件的初始化/销毁。OMX_GetHandle
:获取组件的实例化对象(句柄)。OMX_SetupTunnel
:在两个组件之间建立连接(tunnel)。本文主要介绍了OpenMAX官方头文件里面定义的一些数据类型,其中大部分的数据类型是需要我们自己去实现的,就是说OpenMAX只是提供了一个框架与一套API接口,而接口的具体实现则要靠我们自行编写代码去实现,并且也不是完全不可变的,而是根据实际开发的需要来适当改变一些实现方法来更好地满足开发设计需求。
可以看出来组件的设计方法与linux内核里面的media framework非常相似,都是将一个具体的功能模块抽象为一个(元件),模块之间的连接抽象为(走线),而端口就是(管脚),这个跟电路板上面的电路组成十分相似,可以看出来有时候软件设计有时候可以从硬件设计上面获取灵感,以实现更好的效果,或许除了硬件,还可以从更多的地方来获取到软件设计的方法与思想。