OpenMAX编程 数据结构

导读

该篇文章对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_Core.h)

主要是一些枚举变量结构体原型,其类型主要如下所示: 

 

OMX_Types枚举变量

 

可以看出来,其主要分为以下类型:

  • OMX_BUFFERSUPPLIERTYPE:用于组件内部端口数据流标识。
  • OMX_COMMANDTYPE:命令相关的枚举,用于Client对组件的命令控制。
  • OMX_STATETYPE:组件状态相关的枚举,用于组件状态标识。
  • OMX_ERRORTYPE:错误相关的枚举,类似于C库的错误状态集,用于标识组件内部或者Client的出错状态。
  • OMX_EVENTTYPE:事件相关的枚举,用于组件内部向Client发送事件通知。

OMX_COMMANDTYPE

枚举成员 意义 参数
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标准,定义了如下所示的组件状态集,并且它们之间还有如下的状态转换条件: 

 

组件状态转换

 

对于这几种状态的说明如下:

  • OMX_StateLoaded:组件已经被加载,但是还没有分配相关的资源
  • OMX_StateIdle:组件已经分配了相关的全部资源,但是没有传输buffer,也不处理buffer
  • OMX_StateExecuting:组件开始传输buffer,并且处理buffer(如果buffer可用的话)。
  • OMX_StatePause:组件暂停buffer处理(有可能从暂停的地方恢复数据的处理),该状态没有定义数据是否暂停传输,所以可以依照具体的组件实现来定,可以传输也可以不传输。
  • OMX_StateWaitForResources:组件正等待资源可用(此时资源还未被分配)。
  • OMX_StateInvalid:组件发生了不可修复的错误,此时需要转入该状态。

举例说明下传输buffer、处理buffer、拥有资源三者的区别:

  • 拥有资源:buffer的缓存(内存空间),比如视频数据,在进行数据传输处理之前需要分配相应数量的buffer内存空间用来存放传输的数据,当相应的内存被分配之后,就说该组件拥有资源(has resources)。
  • 传输buffer:组件之间开始传递buffer,比如将buffer从解码组件传输到显示组件,完成数据的组件间转移,称之为传输buffer。
  • 处理buffer:这是对单一组件内部来说的,比如编码组件,它需要接收buffer,然后对接收到的buffer进行编码,这个编码的过程称之为处理buffer,解码组件可同理推之。

当然,在实际的编程实现当中在组件内部也不必拘泥于这几种状态,比如可以新增一些状态或者去掉一些状态,比如很大可能OMX_StateWaitForResources状态就用不到,因为资源分配总是可以在Loaded或者Idle状态中分配完成,而这两个状态中间没必要增加OMX_StateWaitForResources这样一个状态。另外对于各个状态下的数据处理也可以灵活运用,比如OMX_StatePause状态下就可以选择传输或者不传输buffer,如果传输的话就需要把buffer缓存起来,等待组件恢复运行时继续处理(可能会发生缓冲区满的情况,需要做相应的措施)。

OMX_ERRORTYPE

OMX所有的API返回错误值都在该枚举类型里面定义,该枚举类型里面包含了绝大多数的错误状态类型(实际应用当中基本够用)。当然,如果觉得还是需要自己定义错误值的话也可以,从0x90000000到0x9000FFFF这个范围内就是自定义错误的枚举值范围。

举例部分错误值的意义(错误值定义比较多),其余的在头文件里面可以看到相关错误值的详细意义注释:

  • OMX_ErrorNone:没有错误,正确返回。
  • OMX_ErrorInvalidComponent:组件没有初始化函数或者初始化函数调用返回出错。
  • OMX_ErrorUnderflow:buffer为空。

OMX_EVENTTYPE

组件通过回调机制向IL Client发送事件消息,事件消息的类型如下:

  • OMX_EventCmdComplete:组件命令执行完毕,每一个来自Client的命令被执行完毕之后,组件都会向Client返回一个事件,以此表明命令的执行结果。注意:OMX_StateInvalid状态不会产生此事件消息。
  • OMX_EventError:组件发生了一个错误。错误值会作为一个参数返回给Client,错误值就是错误枚举类型里面的成员。
  • OMX_EventMark:一个buffer标记以及到达指定的组件,此时组件会返回给Client一个此事件消息,并且Client还会通过私有的结构体指针成员获取到相关的数据信息。
  • OMX_EventPortSettingsChanged:组件改变端口的设置。比如编码组件在运行的过程中发现要编码的数据流宽高发生了变化,此时就会改变端口的宽高设置。
  • OMX_EventBufferFlag:检测到bit流结束时由组件内部产生。当输出端口检测到buffer的nFlags为被设置为OMX_BUFFERFLAG_EOS,在返回的参数中,nData1指定输出端口的portindex,nData2则是传递nFlags给Client。如果组件是sink类型的(数据流终点),则nData1传递的是输入端口的portindex。
  • OMX_EventResourcesAcquired:组件已经接收到了相关资源(资源分配),此时状态正从OMX_StateWaitForResources转换为OMX_StateIdle state。

OMX_BUFFERSUPPLIERTYPE

用来指明tunnel(建立隧道链接)的port中哪个是数据提供者(buffer supplier port),buffer提供者port可能有属于自己的独立分配的buffer内存空间,也可能与同一组件内部其它的port共享同一个buffer内存空间,由此可见该枚举类型用于隧道链接的建立设置(tunnel setup)。该枚举类型成员有如下几个:

  • OMX_BufferSupplyUnspecified:未指定buffer提供者端口,或者根本就没有(不需要)buffer提供者。
  • OMX_BufferSupplyInput:输入端口充当buffer提供者。
  • OMX_BufferSupplyOutput:输出端口充当buffer提供者。

组件实例相关结构体

OMX_COMPONENTREGISTERTYPE

原型:

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[]类型的数组当中,属于全局、静态的。要创建一个组件时需要根据组件的名字赖在数组里面进行匹配,如果匹配到相关的数组成员,就调用初始化回调函数正式初始化创建组件。

OMX_BUFFERHEADERTYPE

该结构体是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;
  • nSize:buffer的总大小。
  • pBuffer:buffer的数据存储地址。
  • nAllocLen:分配的buffer的总大小,包括有效数据空间与未使用的内存空间。
  • nFilledLen:有效的数据长度,其起始地址由pBuffer与nOffset共同决定。
  • nOffset:有效数据的起始偏移值,相对于pBuffer来说的。
  • pAppPrivate:指向IL Client的私有数据。
  • hMarkTargetComponent:在接收到带有标记的buffer之后,处理完毕时需要发送OMX_EventMark 事件的组件句柄。
  • pMarkData:指向IL Client特定的数据,等于说是一个特定的标记,不同的buffer可以由该成员来打上不同的标记数据,进而产生不同的处理结果。IL Client在发送OMX_CommandMarkBuffer命令的时候把标记数据的指针传递给组件,组件会把相关的标记数据拷贝到该成员中,最后传递给hMarkTargetComponent
  • nFlags:该标志位包含了buffer的特定标志,比如EOS(流结束)标志等,以下是不同标志位的定义:
#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(原始数据的四分之三),由于编码出来的数据大小不一定,所以nFilledLennAllocLen大小就不一致,如果编码出来的数据需要加上一个前缀(比如表示其是H264还是其它的编码之类的),那么nOffset就不会为0了: 

 

编码的Buffer结构

 

OMX_CALLBACKTYPE

在OpenMAX中,有一个很重要的点就是回调机制,回调机制提供了组件到IL Client的单向数据传递,组件里面的回调常用于通知IL Client某些命令已经执行完毕(比如状态设置),下面几种情况下会触发组件的回调机制:

  • 当一个异步的命令被执行完毕(成功、失败或错误)时就需要产生一个回调事件以通知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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面三个回调函数成员需要IL Client实现,并在OMX_GetHandle函数里面设置填充到组件的自定义结构体成员里面然后就可以在组件内部去调用这些回调函数了。

  • EventHandler: 
    组件通过该回调方法向IL Client发送各种事件,在IL Client端的该方法实现中可以对感兴趣的消息进行分发处理(可以用switch语句实现),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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

用于指定组件端口的数量(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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

端口的实例化对象,一个该结构体类型的变量就用于代表组件的一个端口。 
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非常相似,都是将一个具体的功能模块抽象为一个(元件),模块之间的连接抽象为(走线),而端口就是(管脚),这个跟电路板上面的电路组成十分相似,可以看出来有时候软件设计有时候可以从硬件设计上面获取灵感,以实现更好的效果,或许除了硬件,还可以从更多的地方来获取到软件设计的方法与思想。

你可能感兴趣的:(OpenMAX编程 数据结构)