OpenMAX编程-组件

Posted 嵌入式Max

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenMAX编程-组件相关的知识,希望对你有一定的参考价值。

阅读原文

OpenMAX的重点组成部分就是组件,OpenMAX通过将meida流过程中的各个模块抽象化为组件来进行耦合,在OpenMAX标准下,数据流通过组件来进行传递、处理、显示。在该篇文章里,不需要了解细致的组件内部实现机理,也不需要知道各种方法的代码实现形式(如遇少量代码形式的解析说明可暂时略过,只需要知道该段代码要完成的工作是什么即可),通过阅读该文章,需要知道的是组件是什么?它有什么用处?内部的主要组成结构是怎样的?更加细节性的介绍放到后面的文章里面介绍。

什么是组件

组件是OpenMAX对meida视频流中的模块的抽象,比如视频输入模块、视频编码模块、视频解码模块等均可理解为XXX组件,OpenMAX提供了完整的组件式编程解决方案,包括数据流的交换与同步等。在OpenMAX IL层,组件代表着一个独立的功能性模块,组件可以是源(视频输入),目的(视频输出),编解码,滤波器,分离器(音视频分离),混合器(音视频混合)或者其它任意的数据处理模块。一个组件可以代表硬件设备、软件编解码器、处理器或者它们的组合,这取决于组件的实现方式。

一个典型的组件结构如下图所示:

假想的直播软件组件形式

其中的视频编码、视频解码等都可以作为一个组件的形式存在。

组件的内部构造

下图是OpenMAX IL的spec手册给出的一个抽象化的组件的内部结构图:

组件抽象化构造图

从上图可以看出组件内部大概有这么几个组成部分:

  1. 组件句柄(组件的描述符,类似文件描述符一样)。
  2. 配置相关的结构体方法(set/get parameter/configuration)。
  3. 命令队列。
  4. 端口(port)。
  5. 端口的buffer管理。
  6. 组件事件句柄(用于向IL Client发送事件)。
  7. buffer发送模块。

IL Client对组件的操作流程大概如下:

IL Clent对组件的初始化流程

组件句柄

组件句柄是IL Client操作组件的信物,可以看作是跟Linux或者Windows文件编程当中的文件描述符一样的东西,在文件编程中,文件的一切操作都是根据文件描述符来完成的,组件的一切操作也是有组件句柄来完成的。可以把组件句柄当作整个组件的抽象化实例。

与配置相关的结构体方法

主要的方法有set/get parameter,set/get configuration两类,通过这两类方法IL Client可以对组件的各个属性进行设置以及获取组件的各个属性。对于OpenMAX来说,它们两类方法的作用有所区别。注意:这些回调方法需要自己去实现其具体的动作代码。

  • Parameter

    • Set Parameter
      具体到OpenMAX的相关组件代码中就是OMX_SetParameter这个宏(该宏的详细实现在后面的文章里面会介绍),使用这个宏将会从IL Client向组件发送一个parameter结构体,该结构体内部就包含有需要设置给组件的各种参数信息。当该宏被使用之后,组件内部的相关回调函数就会被调用,然后会根据传入的结构体参数进行相关的组件设置。比如可以设置组件的buffer提供者的端口号(port)索引。
    • Get Parameter
      这个就不必详细解释了,自然是获取组件的参数,具体到OpenMAX的相关组件代码中就是OMX_GetParameter这个宏。
  • Configuration

    • Set Configuration
      具体到OpenMAX的相关代码中就是OMX_SetConfig这个宏(该宏的详细实现在后面的文章里面会介绍),使用该宏将会设置组件的相关配置的值。该宏可以在组件初始化并且状态转为Loaded之后的任意时刻调用。 调用这需要提供配置结构体的内存地址以及已经初始化过的结构体内部相关成员,在该宏被使用完毕后,IL Client可以丢弃相关的配置结构体(组件内部会复制一份相关的配置)。比如可以设置组件的时间戳或者是时间跳转单位以及时间缩放系数等。
    • Get Configuration
      道理同上,只不过该方法是用来获取组件配置的,具体到OpenMAX的相关代码中就是OMX_GetConfig这个宏。
  • 宏定义代码实现(不必过于纠结其进一步的实现细节,留着后面讲解)

/* get/set parameter宏定义的实现 */
#define OMX_GetParameter (
    hComponent,
    nParamIndex,
    ComponentParameterStructure)
    ((OMX_COMPONENTTYPE*)hComponent)->GetParameter( \\
        hComponent, \\
        nParamIndex, \\
        ComponentParameterStructure)
#define OMX_SetParameter (
    hComponent,
    nParamIndex,
    ComponentParameterStructure)
    ((OMX_COMPONENTTYPE*)hComponent)->SetParameter( \\
        hComponent, \\
        nParamIndex, \\
        ComponentParameterStructure)

/* get/set configuration宏定义的实现 */
#define OMX_GetConfig (
    hComponent,
    nConfigIndex,
    ComponentConfigStructure)
    ((OMX_COMPONENTTYPE*)hComponent)->GetConfig( \\
        hComponent, \\
        nConfigIndex, \\
        ComponentConfigStructure)
#define OMX_SetConfig (
    hComponent,
    nConfigIndex,
    ComponentConfigStructure)
    ((OMX_COMPONENTTYPE*)hComponent)->SetConfig( \\
        hComponent, \\
        nConfigIndex, \\
        ComponentConfigStructure)

命令队列

命令队列用与IL Client与组件的各种控制,比如下面这么几种命令控制命令对应的动作代码需要自行实现

commandfunction
OMX_CommandStateSet设置组件的状态
OMX_CommandFlush冲洗相关端口的buffer
OMX_CommandPortDisable停止指定的端口
OMX_CommandPortEnable使能指定的端口
OMX_CommandMarkBuffer标记一个buffer,指定哪个组件将响应这个事件
组件部分命令

在OpenMAX的sample里面,命令队列是采用pipe的形式实现的,具体分为:
1. 在组件的初始化代码里面新建一个pipe(使用pipe函数),此时就可以获取到一个类似于文件句柄一样的pipe实例。
2. 在组件内部线程里面等待pipe可读(有命令到达),使用select函数等待,完全可以将上一步生成的pipe实例当作文件描述符(文件句柄)来用。
3. 在相关的回调函数里面(对于命令的发送来说,宏定义为OMX_SendCommand)往pipe里面写命令数据,使用write函数,参数就是第一步获取的pipe实例以及命令结构体。
4. 组件内部线程的select检测到pipe可读,则立马读出一个命令进行执行,执行完毕之后再次转到步骤2进行循环。

暂时不需要知道组件命令队列的代码实现细节,也不需要知道命令的产生过程,只需要了解命令的大致作用即可

除了上面的用pipe结合read,write的形式进行命令队列的管理之外,还可以自行构建一个生产者-消费者的数据队列模型进行命令队列的管理。其实上面那种方法就可以看作是一个生产者-消费者模型,其工作流程如下图所示:

命令pipe

上图中的组件内部线程即可当作消费者,而IL Client则可以当作生产者,生产者不断地往命令pipe数据库里面写入命令(命令的生产),消费者不断地从命令pipe里面读取命令(消费命令),两者异步进行,互不干扰,并且命令也不会由于系统繁忙而丢失。

还有一种自行实现的生产者-消费者模型,是通过链表来实现的,其基本流程如下图所示:

命令队列

组件维护两个双向链表,通过list_head结构体来实现,一个链表存储空的命令结构体,另一个链表存储新生成的命令结构体,IL Client从空的命令结构体链表里面不断地取出一个命令结构体,然后将要执行的命令填充进去(如果链表为空的话需要重新申请新的命令结构体),最后放到填充好的命令结构体链表里面,而组件内部线程则不断地从准备好的命令结构体链表里面取出一个命令,然后进行分发执行,最后再将执行完毕的命令结构体重新送回空的命令结构体链表等待下一次填充。

端口

  1. PORT(端口)是用来干什么的?
    PORT充当了组件之间数据交流的代理人(驻外大使馆),通过PORT端口,两个组件之间可以互相传递数据,同时也可以进行其它的一些控制。
  2. 如何通过PORT(端口)来交换数据?
    组件之间是可以进行绑定的,在组件绑定完成之后,绑定组件的双方PORT口里面就存放了绑定双方组件的实例化组件句柄以及绑定端口信息等数据,然后通过PORT来调用绑定组件的buffer相关的回调函数,这样就可以实现组件之间互相调用对方的内部方法来实现数据交换(包括数据发送与数据回收)。
  3. 如何定义一个PORT?
    一个PORT的定义需要三个结构体来完成,在OpenMAX标准下,这三个结构体分别是:OMX_PARAM_PORTDEFINITIONTYPE,OMX_VIDEO_PARAM_PORTFORMATTYPE,OMX_PARAM_BUFFERSUPPLIERTYPE,其中第二个结构体是跟组件的PORT端口类型有关的,分别有video、image、audio等类型,这里给出的就是video类型。这三个结构体描述囊括了了PORT类型,端口号,COMP句柄等信息。
  4. 如何通过PORT建立连接?
    调用组件的内部方法进行协商连接(协商建立外交),具体到OpenMAX里面就是OMX_SetupTunnel这个宏定义,在绑定组件的时候需要调用双方组件的这个宏定义,最终可以回调到双方组件内部的一个ComponentTunnelRequest方法来完成组件的绑定(该方法的具体代码内容需要自行实现)。
端口绑定示意图

端口的buffer管理

端口的buffer管理是类似于命令队列的,在OpenMAX的sample里面,buffer的管理有一部分也是类似于命令队列的管理方式,data的生成与读取过程如下:

data的生成与读取

OpenMAX的sample中data在组件内部线程里面的管理就是通过两个链表(实际使用中不一定是两个,可以自定义链表的维护方式)来完成的,一个是InputList,另一个是OutputList,分别存放输入的与将要输出的buffer数据。

注意:这里所说的buffer并不是指的实际的video数据或者image,audio数据,而是一个buffer头部描述结构体,在这个结构体里面包含了实际的buffer数据的起始地址、大小、buffer类型、格式等等信息,组件内部buffer的管理指的就是这个buffer描述结构体的管理。试想一下,如果是video数据,一帧就有几M的大小,如果每次都把这几M的数据拷贝来拷贝去,那CPU的资源将会被耗费殆尽,所以采用只传递buffer的信息描述的形式进行buffer的管理,只有真正需要去处理buffer时才去根据buffer的信息描述去实际的buffer存放地址读取buffer数据

组件的事件句柄

组件在初始化的时候会向IL Client注册一个OMX_CALLBACKTYPE类型的回调函数集,其结构体原型如下所示:

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实现,在组件内部可以通过某种事件触发这三种回调成员的回调,比如EventHandler回调成员就会在组件状态转换完成的时候被回调,其余两个则会在EmptyBuffer,FillThisBuffer操作完成的时候被调用,利用这几个回调函数,IL Client可以接收来自于组件内部的若干消息事件,从而达到某种同步以及接收组件的反馈。

buffer发送模块

tunnel模式(绑定模式)

该模块的实现是组件内部比较重要的一部分,通常位于组件内部线程的命令分发之后(tunnel模式-也即绑定模式),可以参考下OpenMAX sample的线程实现部分。实际的buffer发送是通过PORT端口来完成的,通过PORT端口来调用与之绑定的组件的EmptyThisBuffer方法来完成数据的传递。我们来看一张图:

tunnel模式下的数据传递

由图中可以看出,如果组件A是数据的提供者,那么完整的一帧数据传递就是:

  1. 组件A调用组件B(通过PORT端口实现)的OMX_EmptyThisBuffer(B, pBuffer)宏来实现数据从A传递到B。
  2. 组件B调用组件A(通过PORT端口实现)的OMX_FillThisBuffer(A, pBuffer)宏来完成数据从B到A的还回过程。

如果组件B是数据的提供者:

  1. 组件B调用组件A(通过PORT端口实现)的OMX_FillThisBuffer(A, pBuffer)宏来完成buffer数据从B到A的传递过程。
  2. 组件A填充收到的buffer数据结构体,然后调用组件B(通过PORT端口实现)的OMX_EmptyThisBuffer(B, pBuffer)宏来实现数据从A到B的还回过程。

上述过程在双方组件的内部线程里面完成,至于在线程内部的具体哪个位置则取决于组件的功能以及其具体的实现方式,套路不是固定死的,可以根据自己的需求来做出一些改变。

Non-tunnel模式(非绑定模式)

Non-tunnel模式下的数据传递

在该模式下,数据传递的双方变为IL Client与组件了,整个过程变成了:

  1. IL Client通过OMX_FillThisBuffer(pHandle,1,pBufferOut)回调方法向组件A的Output端口提供一个空的buffer结构体以待填充个,该宏是通过IL Core来最终完成对A组件的FillThisBuffer回调方法的调用的。
  2. 然后IL Client向组件A传递一个buffer数据,这时组件A可以对buffer进行处理,然后处理完毕之后生成的新的buffer数据填充到上一步接收到的空buffer结构体里。
  3. 等待组件A将Output里的buffer填充完毕,组件A就会调用OMX_FillBufferDone(pBufferOut)方法来通知IL Client接收处理后的数据。
  4. IL Client数据处理完毕之后就会再次调用OMX_FillThisBuffer(pHandle,1,pBufferOut)来还回buffer到Output列表等待再次填充。
  5. 组件A处理完接收到的buffer数据之后就会调用OMX_EmptyBufferDone(pBufferIn)方法来通知IL Client,说明组件A已完成接收buffer的数据处理。

上面的过程描述可能看起来有点懵,带入实例来说明下,比如组件A是一个解码组件,那么整个过程就变成了:
1. IL Client向组件A(解码组件)的Output buffer列表提供一个空的buffer结构体来存放解码后的数据。注意,此时传递给A的只是buffer的头部描述(参见上面的buffer头部描述内容介绍-端口的buffer管理),真正的buffer数据存放地址是在IL Client那里的。
2. IL Client向组件A的Input端口发送一帧待解码的buffer数据。
3. 组件A解码完毕,解码后的数据被拷贝到了Output端口buffer队列里面(根据buffer描述直接将解码后数据拷贝到IL Client的实际buffer存放地址),同时通过回调通知IL Client,buffer可用了(解码数据可用)。
4. IL Client对解码后的数据进行处理,比如保存为文件等等,然后把buffer重新还给组件A以待填充下一帧解码数据。
5. 组件A通过回调通知IL Client该帧数据已经解码完毕。可以进行下一帧数据的传递。

组件的状态转换

  1. 为什么要有状态转换?
    通过组件的状态转换可以控制组件之间数据传递的暂定、开始、恢复等,可用于组件间的数据同步以及数据交换开关。
  2. 如何控制组件的状态?
    通过组件内部提供的回调函数进行控制,主要是通过SendCommand回调来发送OMX_CommandStateSet命令来完成。
  3. 都有哪些状态?
    一共6种状态,类型以及状态特征和它们之间的转换关系见下图:
组件的状态转换

可以看到,组件一开始初始化的时候,状态就被初始化为OMX_StateLoaded,然后等到资源准备完毕,IL Client会将组件的状态设置为OMX_StateIdle,在需要传递数据的时候需要将状态设置为OMX_StateExecuting,组件停止到销毁的过程正好与上面的相反。

组件状态与对应的动作
  1. 组件初始化,最终状态转为Executing,数据正常流转。
  2. 组件之间停止数据传输,状态转为Idle。
  3. 组件的状态转为Loaded,同时后面的组件需要调用前面组件的FillThisBuffer回调方法来还回数据。
  4. 组件正式销毁,所有资源释放。

本篇文章大致勾勒了一个组件的整体框架,后面介绍组件内部的相关结构体以及回调函数成员等。

文章参考:
C语言之list_head双向链表
OpenMAX编程初识


如果觉得本文章不错,请关注微信公众号-YellowMax多多支持,查看更多文章
欢迎转发、关注、点赞一波

以上是关于OpenMAX编程-组件的主要内容,如果未能解决你的问题,请参考以下文章

OpenMAX IL介绍与其体系

OpenMAX (OMX)框架

Android音视频——OpenMAX (OMX)框架

转:android中多媒体解码openmax的实现

Android Multimedia框架总结(十三)CodeC部分之OpenMAX框架初识及接口与适配层实现

如何使用 ARM 的 OpenMAX 开发层 (DL) 构建和解码