OpenMAX编程-实现一个组件

往期文章索引:

[02 -OpenMAX编程-数据结构]

[01 -OpenMAX编程-组件]

[00 -OpenMAX编程初识]

导读:

本文聚焦于如何编程实现一个真正的组件,主题思想是介绍一个组件在编程sjo的模块组成以及如何编写,也会介绍下组件的初始化过程、组件之间的绑定过程、数据同步过程。在下一篇文章里面会对video、audio、clock等组件进行介绍。

注意:该文章主要介绍组件内部代码的实现,其余部分简略介绍,实际上完整的组件还包括有组件类型的管理,组件绑定模块,组件句柄的分配,然后才是下面的各个组件的实现,但是OpenMAX的标准化使得我们编写组件内部代码的时候不必过于关心更上层的组件管理代码的实现,只需要知道应该按照什么样的规则去编写组件内部代码即可。

编程涉及模块

组件实例结构体:包含组件的各个功能模块

命令控制模块:用于IL Client控制组件的各种行为,包括状态转换,组件退出,部分buffer数据的处理等等。

事件回调模块:组件向IL Client回返通知,用以表明组件已经完成某些工作。比如状态转换完毕时、数据处理完毕时都会产生一个特定的事件回调以通知IL Client已经完成工作。

参数设置/获取:用于IL Client配置组件参数,该部分也可以实现自定义的控制函数。

状态转换:用于实现组件的数据控制以及组件之间的数据同步。

buffer数据管理与同步:用于组件内部输入输出buffer的管理,组件之间的数据同步操作。

组件绑定与解绑:用于连接组件以及去除连接。

组件主体框架的搭建

组件的初始化函数

原型:。所有的组件都是通过该函数来进行初始化创建的,该函数等于说是整个组件内部世界的入口,该函数的参数则是整个组件的句柄,由IL Client分配空间并传入给该函数进行填充与初始化。该函数的参数是一个类型的指针,意味着组件的编写者可以自行定义组件句柄的结构体类型,但通常情况下,组件句柄类型都是一个类型的结构体,其原型如下:

上面代码保留了所有的注释,以便自行了解各个成员的含义以及整个结构体的作用。组件的初始化就是将组件句柄结构体类型内部的成员全部填充好。接下来就是实现类型的组件初始化函数。在该函数里面需要做的事情如下:

初始化该组件的端口,包括端口号、端口类型、端口属性等等

初始化填充内部各种类型的回调函数指针

做一些组件私有属性的设置(比如组件buffer分配,命令队列分配初始化,组件私有变量的初始化等等)

回调函数的实现

该部分详细介绍每一个组件内部回调函数的内部实现(具体的内部代码实现需要开发者自行实现,这里只讲函数里面大概需要做什么事以及各个函数的功能,照着这个指引就可以快速地去实现代码编写了),下面说的该结构体均是指结构体类型。先来看一张图,这张图说明了在各个状态下每一个回调函数是否可执行:

回调函数对应状态可执行表

nSize:表明该结构体类型的大小,可想而知,该字段是由该结构体的分配者来进行填充,对应相关的回调函数是(后面会介绍),不过暂时不必关心它的实现,只需要知道该字段由填充,代表该结构体的大小即可,该字段也可以由组件本身去实现。当然,该字段不是必须的,可以选择不使用。

nVersion:组件的版本,当组件有版本更新时用来表明版本号,通常用于保持组件升级迭代过程中保持各个组件之间的兼容性。该结构体内部包含有组件的主版本号、子版本号等等。

pComponentPrivate:按照OpenMAX标准的说法,该指针是在组件第一次进入状态时才被分配并且初始化的,但是通常情况下,在组件初始化函数中就直接将组件的状态初始化为,因此该指针指向的类型通常由组件初始化函数来分配并进行填充的。该指针指向组件内部的私有结构体类型,这个私有结构体类型由组件内部自定义,只需要将该指针指向自己定义的结构体实例地址即可,通常包含有组件的各个flag变量、端口定义、buffer管理、组件状态描述等等,这些都用于组件自身的管理与功能实现,IL Client不会去访问。

pApplicationPrivate:该指针作为的参数传递给组件,它是IL Client层级的私有数据指针,并且组件所有的回调函数都会将该指针再次返回给IL Client(包含在类型中,作为成员返回)。IL Client可以与组件约定一些公认的变量,以此来在IL Client与组件之间传递某些信息。

GetComponentVersion:获取组件的版本信息,该函数的调用是同步的,要求在5ms内返回。该函数需要填充三个成员,如:不必多说,就是该组件的名称(char *);:组件的版本,由组件的厂商提供,完全取决于组件厂商;:与上一个不同的是,该版本指的是组件编写时所参考的OpenMAX spec文档的标准版本号,也就是说厂商发布的第一版组件版本(version 0.1)可能对应的spec版本是openmax_il_spec_1_0(version 1.0),这里的区别需要注意下;:由组件运行时指定,要求每一个组件的UUID都唯一,用于在全局范围内标定组件(因为同一个类型的组件可能会同时创建多个实例化对象,UUID不同就可以区分开它们)。

SendCommand:该函数是非阻塞的,调用生成命令之后立即返回。该函数的主要作用是产生命令,并将命令放入命令队列里面,该函数的实现伪代码如下:

最后,一定记得在组件获取命令执行完毕之后返回给IL Client一个事件,如果命令被成功执行,则返回一个类型的事件,否则就返回类型的事件。

GetParameter:获取组件的参数描述结构体,该函数是同步的,应该在20ms内返回。该函数的参数指定了需要获取的参数索引,通常是一个枚举类型(枚举类型,比如),用户可以根据自身的需要在头文件的枚举类型中扩展自己需要的枚举成员。除了状态不能执行之外,该函数只需要执行拷贝工作即可。也可以在参数里面传入一些信息,比如指定端口号等等,该参数是一个void *类型参数,由索引号来决定其实际的参数类型,由IL Client与组件共同商定。

SetParameter:不必多说,与上一个相反,该回调是用来设置组件的参数的,拷贝方向是从IL Client传入的参数到组件内部,不过只有在与状态下可以执行,其余状态均需要返回状态错误的错误码。

GetConfig:获取组件的配置,该函数与很像。用于从组件里面获取一个配置,该回调函数在组件被初始化之后的任意时刻都可以被调用,调用者需要提前分配好对应的空间并填充大小与版本信息,该回调函数应该是加锁保护的(因为APP与组件会去访问同一个变量,需要互斥)。

SetConfig:与上一个相反,用于向组件发送配置。比如音频相关组件的声音大小、均衡度之类的。另外,可以看到,Config与Parameter相关的设置获取回调函数参数一模一样,它们的参数索引号枚举类型也是一样的,都是类型的枚举,它们的区分并不是很严格,在一些场景下,既可以用Config,也可以用Parameter。

GetExtensionIndex:获取扩展的index(枚举类型),改回调函数是上面四个函数的扩展,用于加入一些厂商自定义的index类型,该函数不是必须的,通常情况下该函数的使用方式如下:

GetState:获取组件的状态,比如在非绑定的状态下APP调用该函数来,获取组件的状态来决定是否向组件发送数据。还有一种应用场景是在设置组件状态时等待状态设置完毕(因为组件状态设置是通过发送命令来完成的,而这个过程是异步的,有时候我们需要确保状态确实被被设置进去了才能进行下一步的动作),示例代码如下:

ComponentTunnelRequest:重点来了,该回调函数用于组件绑定,在上层组件管理代码中,组件绑定函数被调用时需要绑定的两个组件的该回调函数会依次被调用用以实现最终的绑定工作。该函数可以支持buffer协商(由哪个组件的哪个端口提供buffer),当然也可以使用专用的通信方式(proprietary communication),在该模式下buffer的传输方向,buffer提供者均已在组件外部商定好了(厂商自行在组件外部进行自定义,组件内部只需要去获取这个定义即可,无需自行协商)。对于参数是output类型的组件回调来说,它需要做的工作是填充结构体(该结构体描述了该端口的绑定要求与限制);对于参数是input类型的组件回调来说,它需要执行下列步骤:

1.查询传入的参数并确认是否能够兼容(能否对得上眼?),比如端口号、端口的flag等。

2.如果条件满足,可以绑定,则把output端口设置的以及output端口的组件实力句柄等存储起来以备使用。

3.确定哪个端口是数据提供者(根据参数来进行选择),并根据上一步保存的组件实例句柄调用来和与该组件端口绑定的组件进行确认。一个组件可以有多个端口,输入输出类型不一而足,所以该回调函数内部需要根据传入的端口号来确认该组件是充当output端口的提供者还是input端口的提供者,然后选择相应的步骤策略来进行设置。从上述步骤也可推之,组件管理层的组件绑定代码需要先调用output端口提供者的回调,然后再调用input端口提供者的对应回调。

UseBuffer:该回调函数用于标记使用一个buffer,该buffer是由IL Client或者tunnel组件的buffer提供者分配,并由数据的分配者调用指定组件的该回调函数将数据传递给指定的组件,指定的组件组要自行分配一个buffer header来存放这些buffer信息(可以看出该函数用于非buffer提供者组件存储buffer提供者分配的buffer),一般情况下,组件会建立一个list链表来存放这些数据以待使用。该回调函数在以下场景下可以被调用:

组件处于状态,并且已经发送了状态转换请求。

组件处于状态,此时资源可用,并且正准备切换到状态。

处于disabled状态的port,此时组件处于,或者状态。

AllocateBuffer:该回调函数与上一个类似,只不过该函数的被调用者组件(由函数的第一个参数指定)既分配实际的buffer空间,又分配buffer header(上一个函数的内部实现只用分配后者),然后由参数返回buffer header信息给调用者,通常情况下,调用者(非绑定情况下就是IL Client,绑定情况下就是buffer提供者组件)需要保存这个buffer header到一个链表里面,以备使用。该函数的调用场景与上一个函数一致。

FreeBuffer:释放一个buffer数据,如果组件只分配了buffer header的话,只需要释放buffer header,如果既分配了buffer header,又分配了buffer的话就需要把两个都释放掉,总之,遵循谁分配、谁释放的原则。显而易见,如果一个组件的角色是非buffer提供者,那么它就需要实现上述三个回调函数,如果一个组件的角色是buffer提供者,那么它只需要实现后面两个回调函数即可。。

EmptyThisBuffer:该函数用于组件之间的正向(数据源头到数据结尾方向)数据传递,在绑定状态下,由output port所在的组件调用,第一个参数就是与之绑定的组件句柄,该回调函数是由input port所在的组件实现的,参数是input port所在的组件句柄与buffer header,该函数被调用之后,后者组件需要把传入的buffer header记录在自己的buffer队列中去,该函数在与状态下均可接收数据。注意,该函数的主语是output port所在的组件,是该组件让input port所在的组件来Empty自己的buffer,这样理解的话这个回调函数的名字就不奇怪了,如果搞错主语,就会奇怪为什么是,而不是。在非绑定状态下就是由app来调用该回调函数,此时主语是app。

FillThisBuffer:该函数用于output port所在的组件向前一个组件(绑定状态,非绑定状态不可用)还回一帧数据。该回调函数是由input port所在的组件实现的,参数就是input port所在的组件以及buffer header,同样的,该函数的主语是output port所在的组件,是说让input port所在的组件来重新填充这一帧buffer(还回去,重新填充)。

SetCallbacks:设置回调函数,其类型的参数就是回调函数的结构体抽象,回调函数成员包括,,。三个的应用场景分别如下:

EventHandler:用于组件向IL CLient发送事件,事件包括但不限于状态转换结束、状态转换错误、具体查看OpenMAX编程-数据结构 OMX_EVENTTYPE一段。

EmptyBufferDone:该回调函数用于向app返回已经接收的buffer,注意此处的的主语,该函数通常用于之后,并且在非绑定的条件下由回调函数的提供者组件调用,该回调函数的作用类似于,可以理解为前者用于非绑定状态下向app还回数据,后者用于绑定状态下向buffer传递者组件还回数据。此时组件的内心活动是:我已经把你给我的buffer存起来了,我通知你一声,至于你要不要释放这一帧数据或者再次填充这帧数据就听开发者怎么说啦。(此为前文还回的意思)

FillBufferDone:该函数用于组件通知app,我已经填充好数据了,你过来拿吧。这里可能会有个疑问,不是app负责产生数据,组件负责处理吗?怎么组件又是接收数据(EmptyBuffer),这里又FillBufferDone呢,FillBuffer不是app干的事吗?其实这两个是不冲突的,举个例子:编码组件,它接受来自app的原始buffer数据(存放在input port的buffer队列),然后进行编码,编码完成之后会产生一个编码后的buffer(存放在output port的buffer队列),然后把该编码后的buffer当作参数调用FillBufferDone来通知app我填充好了编码后的buffer,可以给你用了,是所谓。

ComponentDeInit:该函数就是进行组件去初始化操作了,一般会需要释放由组件初始化函数申请的资源,去初始化一些系统资源等等,做一些清理工作,总之,在组件初始化函数里面是怎么做的,在该函数里面反过来做就行了。

注意:

这些回调函数有很大一部分是加锁互斥访问的(组件实例结构体代码有说明),这个锁是需要自己去实现的。

所有的函数会注明是阻塞的还是非阻塞的(组件实例结构体代码有说明),非阻塞的会有最大时间消耗要求,注意不要超过这个时间。

各个回调函数会有不同的组件状态约束,注意函数实现的时候加上判断,参照”回调函数的实现”开头图片。

buffer的管理是灵活的,可以使用生产者消费者的链表结构实现以及pipe管道模式实现,参照OpenMAX编程-组件,不同的组件还可以灵活选择buffer队列的个数。

基本实现以上各个函数,一个组件的框架代码就成型了,下图说明了buffer传递的几个函数的作用:

buffer传递图

命令控制模块

命令控制模块的用途如下:

app(或者IL Client)控制组件的状态转换。

冲洗一个buffer队列。

禁止/使能某个组件内部端口。

标记一个buffer。

具体的描述在OpenMAX编程-数据结构的OMX_COMMANDTYPE一节可以看到。

命令的生成可以是由app调用组件的回调函数进行生成,也可以由组件内部自行生成,大部分情况下,需要组件实现一个自己的命令描述结构体用来实例化命令,该结构体可能需要包含命令的枚举、命令的数值、命令的字符内容(如果需要传递字符串的话),组件仅在命令产生时申请一个命令描述结构体(通常包含的成员有:命令索引、命令data),然后将这个命令结构体放入一个队列,可以用链表实现,也可以通过创建一个pipe来实现,组件的内部线程在接收到命令之后立即取出执行。创建pipe的方式在OpenMAX的官方文档里面就有相关的代码实现,这里不再赘述。

事件回调

事件回调由那个函数完成?由谁来完成?

事件回调是针对app来说的,事件回调函数结构体(OMX_CALLBACKTYPE类型)由IL Client实现,然后调用组件的回调函数将回调函数存放到组件的自定义结构体里面以供调用。回调函数通常在需要组件通知IL Client的时候由组件进行调用(回调函数实体在IL Client里面实现)。

回调函数的应用场景

事件回调的类型与场景在OpenMAX编程-数据结构的OMX_EVENTTYPE一节中有描述,组件内部实现的时候每当满足某个事件产生的条件时就需要调用事件回调函数来产生相关的事件来通知IL Client,并且注意在调用之前要检查IL Client是否有注册了回调函数,如果未注册就需要立刻返回(是允许IL Client不注册回调函数的,但是不建议)。

参数的设置/获取

IL Client如何与组件进行参数交互

主要是通过,,,四个回调函数来完成,组件内部需要实现相关的参数获取与设置项,这部分是跟组件的具体功能有关的,比如一个编码组件,就需要通过这些回调函数来实现编码器参数的设置与获取(编码帧率、质量、buffer大小等等)。

如何添加自己的index

除了OpenMAX给出的标准的index之外还可以定义厂商自己的index type(组件差异化、产品差异化,有的厂商会有独特的功能参数设置),自定义的这部分index type可以加在枚举类型的相应集合处,比如属于音频的就加载audio处,该枚举结构体内部有为每一个集合预留空间,专门用于厂商自定义相关的参数控制index。

什么时候用得到参数的设置与获取

在一个组件转入Executing状态正式运行之前会用得到,用于预设组件的各项参数,如果参数可以在组件运行途中被改变的话也可以在组件正式运行之后进行获取与设置。

组件的状态转换

组件状态转换有什么作用?

状态转换用于控制组件运行、暂停、恢复、销毁等等。多个组件同时运行的时候状态转换就可以用来进行多组件之间的数据同步,协调组件之间的工作节奏。

由谁来进行状态转换控制,如何去实现控制

状态转换一般是由IL Client调用需要控制的组件的来进行状态转换命令的发送,参数填充枚举成员,参数填充需要转换的命令。当然除了IL Client的控制,组件内部也可能会主动给产生一些状态转换的命令,比如在组件发生严重错误的时候,此时组件内部就需要主动转入Invalid状态。组件内部需要实现命令的获取与解析,通常在组件的内部线程里面完成,在接收到该命令之后,组件内部线程需要根据命令指定的状态进行相关的动作。

有哪些状态,分别对应什么场景

组件内部有6种状态,分别对应如下:

:该状态在组件被调用初始化之后就转入,此时相关的buffer资源还没有被分配,IL Client可以使用函数回调来进行参数的设定,使用函数进行组件之间的绑定操作,最后控制组件从该状态转入或者状态,后者是在组件尝试分配相关的buffer资源并转入状态失败的时候才转入。

:表明组件所有的资源都已经准备好了,正等待正式运行,该状态下,组件持有buffer,但是不传输也不处理buffer。当组件从或者状态转入该状态时需要归还所有处理完毕的buffer到buffer提供者。

:该状态下,组件持有buffer,传输同时处理buffer。此时组件应该接受其它组件对该组件的与函数回调,同时在非绑定的情况下使用与来归还Empty以及full buffer。绑定情况下就使用与来进行buffer的传递。

:该状态下,组件持有buffer,不传输也不理buffer,但是组件不必归还buffer到buffer的提供者。为了避免丢失数据,此时组件可以选择将接收到的数据存放到自己的buffer队列里面,但是不进一步传输也不去处理,当然也可以选择不去存放,这跟组件的具体类型与功能有关系。

:顾名思义,在该状态下,组件正在等待资源被分配并变得可用。通常情况下该状态由组件自己主动转入,原因前面说过,是因为资源分配失败导致。

:该状态是在组件发生不可修复的错误时转入,此时组件需要产生一个事件,类型为,值为,最后转入状态,当IL Client接收到该消息的时候就需要调用来释放所有组件持有的资源。

组件之间的状态转换图如下:

组件的状态转换

组件内部线程

组件内部线程主要用于数据处理、命令处理、数据传输。它的基本逻辑是这样的:

组件内部线程逻辑

至于每个组件、每种类型的组件的内部线程都是不一样的,因为不同组件的功能不同,有的可能会有多个内部线程,有的可能会有多个buffer管理等等不一而足。基本原则是,以自己组件的功能出发,由上而下去设计自己的组件的内部线程逻辑。

组件管理

组件的初始化与注册

组件的初始化由IL Client调用函数回调来完成,该函数根据传入的组件名来找寻对应的组件,然后调用组件的初始化回调函数实例化一个组件句柄并返回给IL Client。该函数的原型如下:

可以看到该函数有一个输出类型的参数,三个输入类型的参数:

pHandle:该参数由回调函数分配空间,然后传递给的函数实例进行初始化(不同的组件都有自己的类型的初始化函数实体)。组件初始化完毕之后,该参数就指向一个初始化好之后的组件实例了,此时可供IL Client进行使用。

cComponentName:组件的名字(无符号字符类型),一个系统中的所有类型的组件会由一个数组保存,该数组的成员有两个:组件的字符串类型的组件名;组件的类型的初始化函数实体。而该参数就是指定了组件的名字,根据名字可以在整个数据里面找到组件的初始化函数实体,调用之,初始化。

pAppData:需要给组件进行保存的IL Client端的自定以数据结构体,如果组件不需要就置为空(NULL)。

pCallBacks:该参数是必须的,组件会通过IL Clint实现的回调函数实例来通知IL Client各种类型的事件,该回调函数内部成员包括Event、EmptyBufferDone、FillBufferDone等,每个回调成员的作用在前面都有描述。

注意:回调函数实体需要IL Client实现。函数则是做与前者相反的事情。

组件的绑定与解绑

组件的绑定操作由回调函数来完成(该函数实体需要IL Client完成),区别于组件内部的回调方法,该函数内部需要实现分别两次调用需要绑定的两个组件实例的回调方法,只有在状态下才可以实现组件之间的绑定操作。通常情况下,组件只在需要被销毁的时候才需要进行相互之间的解绑,但是此时不需要进行显式的解绑操作,因为绑定的过程中没有分配任何额外的资源,所以直接销毁组件即可,无需关注解绑。如果既不想销毁组件,又想解绑组件,则可以添加一个解绑的函数,函数中通过SetConfig调用直接清除掉组件内部的绑定标志位即可,也可以在函数当中添加一个选择解绑与绑定的参数,具体可灵活实现。

组件的绑定过程

如图,需要将组件A的端口A1与组件B的端口B0进行绑定,整个过程的步骤如下所示:

1.调用传入A1与B0端口号以及A、B组件实例句柄,组件A返回,该参数表明了绑定标志位与buffer提供者。

2.调用传入B0与A1端口号以及B、A组件实例句柄,在组件B的该函数回调内又会去调用组件A的获取相关参数,并调用组件A的设置组件A的buffer supplier。组件B也返回一个。

3.判断返回值是否正确,如不正确就对组件A解绑并返回错误。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180107G0NLJ400?refer=cp_1026

扫码关注云+社区