一、openMAX理解1
gst-omx是基于openMAX开发的插件,所以在介绍gst-omx之前,我们先了解一下openMAX。
openMAX:open media acceleration,开源多媒体加速器,与多媒体相关,为多媒体处理提供统一的接口标准,以此达到跨平台的多媒体软硬件开发目标。
一个标准的多媒体设备包括硬件、设备驱动以及设备应用程序,与之对应的是openMAX的DL、IL和AL层。
- DL:Delelopment Layer 在这一层,定义了众多的API,包含openMAX音频、视频以及图像处理功能,这些功能由芯片开发商完成并优化;
- IL:Integration Layer 使得应用程序与多媒体框架可以以一种统一的方式与多媒体编解码器对接;
- AL:Application LAyer AL层为多媒体中间件与应用层之间提供了一个标准化的API接口,为多媒体接口提供跨平台可移植性;
这些层级,可以互相结合实现,也可以单独实现某个层级,比如有些芯片厂商,根据芯片编解码硬件实现openMAX的IL层,这样使得基于该厂商硬件以及相关中间件开发的应用程序可以快速移植到其他符合openMAX IL的软硬件设备上。
在IL层,有两个关键部件:component(组件)和 IL Client。
- component:音视频、字幕分解组件音频解码组件、音频输出组件、视频解码组件、视频输出组件;
- IL Client:IL的客户端,组件的管理者,Client通过组件内部提供相关的回调函数对组件进行管理,应用程序可以调用client的接口来操作组件,包括设置组件的状态、添加一个组件、销毁一个组件等。
buffer发送模块
tunnel模式(绑定模式)
如果组件A是数据提供者,完整的数据传递如下:
- 组件A调用组件B(通过port实现)的OMX_EmptyThisBuffer(B, pBuffer)宏来实现数据从A传递到B;
- 组件B调用组件A(通过port实现)的OMX_FillThisBuffer(A, pBuffer)宏来完成数据从B到A的还回过程;
如果组件B是数据提供者:
- 组件B调用组件A(通过port实现)的OMX_FillThisBuffer(A, pBuffer)宏来完成buffer数据从B到A的传递过程;
- 组件A填充收到的buffer数据结构,然后调用组件B(通过port实现)的OMX_EmptyThisBuffer(B, pBuffer)宏来实现数据从A到B的还回过程;
Non-tunnel模式(非绑定模式)
在该模式下,数据传递的双方变成了IL Client与组件了,过程如下:
- IL Client通过OMX_FillThisBuffer(pHandle, 1, pBuffer)回调方法向组件A的output端口提供一个空的buffer结构以待填充,该宏是通过IL Core来完成对组件A的FillThisBuffer回调的调用;
- 然后IL Client向组件A传递一个编码数据buffer,然后处理完毕之后生成新的buffer数据填充到上一步接收到的空buffer结构;
- 等待组件A将output的buffer填充完毕,组件A就用OMX_FillBufferDone(pBuffeerOut)方法通知IL Client接收处理后的数据;
- IL Client数据处理完毕之后,就会再次调用OMX_FillThisBuffer(pHandle, 1, pBuffer)来还回buffer到output列表等待再次填充,这样完成一次数据交互;
在gst-omx中,一般情况下应该使用的是Non-tunnel模式,具体情况具体分析。然后,gst-omx源码结构大概如下:
gst-omx-1.14.2
|---omx
|---gstomxvideodec.c /* 类似编解码这个的,相当与是openMAX AL,在他们上面有更详细的编解码插件 */
|---gstomx.c /* 相当于IL Client,对接openMAX IL,内有回调函数,事件、消息处理函数,负责与component交互 */
|---openmax /* openMAX IL层头文件,需要芯片厂商实现其中必要函数 */
openMAX与gst-omx简单介绍完毕,下面将相应了解gst-omx的解码流程。
二、gst-omx流程
以h264dec为例,了解gst-omx流程(以 gst-omx 1.14.2 为例)。
在进入到具体的OMX类element之前,先了解omx相关的element是如何登记到gstreamer。
通过gst-omx中src目录下的Makefile.am可以了解到,该package最终将会编译为一个叫libgstomx.so的库,库是gstreamer element库,将会存放在机器端的/usr/lib/gstreamer-1.0目录下。在加载插件的时候,gstreamer核心将会逐个扫描该目录下的文件,符合相关符号标准的将会加载,识别其中的feature并登记。在这个过程,将会扫描 libgstomx.so,然后在加载插件的时候将会调用到gstomx.c中的plugin_init()注册插件,omx相关的插件,就是在这个函数注册到gstreamer。
gstomx.c的plugin_init()主要操作如下:
- 获取gstomx的配置文件gstomx.conf,该文件一般将会放在机器端的/etc/xdg目录下,该配置文件主要是说明element的名称、rank、port等信息,在加载的时候将会先获取该信息,再加载相应的插件;
- 在前面的文章有说过,向glib核心注册一种新的类型,将会是第一次调用gst_xxx_get_type(),所以接下来又将会通过gstomx.c文件定义的静态数组types,逐个初始化omx相关的类型;
- 最后就是通过解析配置文件,获取相应element的信息,而后调用gst_element_register()注册;
上面提到的获取element信息之后,再注册到gstreamer core,了解该步骤可以先看一下配置文件的内容,以omxh264dec为例:
[omxh264dec]
type-name=GstOMXH264Dec
core-name=/usr/local/lib/libomxil-bellagio.so.0
component-name=OMX.st.video_decoder.avc
rank=257
in-port-index=0
out-port-index=1
hacks=event-port-settings-changed-ndata-parameter-swap;event-port-settings-changed-port-0-to-1
在gstomx.c中,将会读取配置文件gstomx.conf,将每一项类似[omxh264dec]的元素当作一个element,然后进行注册:
- 将会type-name得到之前初始化的omx相关的类型GType;
- 根据core-name提供的库路径,检查element所在的库是否符合要求,检查component-name元素是否存在等;
- 通过g_type_register_static(),从omx相关的类型GType衍生一个新的omx类型,并通过gst_element_register()注册为element;
- 在通过g_type_register_static()衍生新的omx类的时候,有传进type_info参数,该参数赋值了class_init等成员,通过该成员,将会在初始化该类的时候,调用到该成员函数(赋值为gstomx.c的_class_init()),在该函数中将会读取配置项填充该衍生类,在操作该衍生类的时候将会使用到这些参数。
所以,根据上述介绍,我们再看看omxh264dec的继承关系,将会一目了然,其中,GstOMXH264Dec-omxh264dec就是衍生类:
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstVideoDecoder
+----GstOMXVideoDec
+----GstOMXH264Dec
+----GstOMXH264Dec-omxh264dec
创建omxh264dec之后,将会进行link操作,先是omxh264dec与上游element link,这个操作将会查询omxh264dec sink pad支持的caps,而查询caps的操作,又将会查询src pad的PEER pad支持的caps,但是此时并没有PEER pad,所以这个返回为NULL,而查询omxh264dec sink pad支持的caps将会返回sink pad的template caps,与上游element取交集之后,有交集则link成功。
同样的,omxh264dec与下游element link的时候,也都将会查询src pad caps,将会返回template caps,有交集则link成功。
link成功之后,接下来的将会是element状态切换。
2.1 NULL—>READY
在gstomx中,切换到该状态并没有进行什么操作,主要操作是在gstvideodecoder的状态切换函数。当切换到READY时,将会通过decoder_class->open (decoder)
打开设备或者解码库,通过这样的一个方式,将会调用到gst_omx_video_dec_open()。
在该函数中,将会进行以下操作:
- 将会先通过gst_omx_component_new()创建新的component,而component组件的信息,就是在上面提到的,gstomx.c的_class_init()函数中填充,在这里开始起作用的,用于创建组件时填充相应的信息,由于我们这里介绍的是解码,自然就是self->dec组件了;
- 接下来gst_omx_video_dec_open()的操作就是确认dec component的Status为OMX_StateLoaded,获取dec component的port index,然后通过gst_omx_component_get_parameter()获取port的OMX_IndexParamPortDefinition参数信息,并将port添加到dec component,这样就完成了dec open操作;
self->dec =
gst_omx_component_new (GST_OBJECT_CAST (self), klass->cdata.core_name,
klass->cdata.component_name, klass->cdata.component_role,
klass->cdata.hacks);
- gst_omx_component_new()将会通过调用gst_omx_core_acquire()打开配置文件core-name声明的element库,然后通过g_module_symbol()获取相应的OMXCore函数句柄,这样子,gst-omx就与openMAX IL层对接起来了;
- 在gst_omx_core_acquire()之后,将会通过core->get_handle()根据component_name将相应的OMX_CALLBACKTYPE回调函数注册到openMAX IL层component;
- 最后就是根据传进来的一些信息,填充component;
至此,NULL—>READY切换完成。
2.2 READY—>PAUSED
在该过程,GstOMXVideoDec的状态切换只是对dec component的某些状态变量复位,重要的操作,还是通过GstVideoDecoder的状态切换函数gst_video_decoder_change_state()完成。
在这个切换过程,将会对component进行reset,然后再调用start函数。在gst_video_decoder_reset()中,就是对input/output port的时间管理、状态管理、缓冲池管理等进行释放或者失能操作,相应的状态变量复位。而start函数gst_omx_video_dec_start(),也只是简单的进行dec的状态变量复位而已。
是不是感觉gstomx在这个过程并没有进行什么操作?其实并不然,只不过是因为gstreamer都已经帮我们封装好了。还记得之前的文章《gstreamer学习笔记—pad定义、连接、流动》,就有将会,在从READY切换到PAUSED的时候,将会在gst_element_change_state_func()中调用gst_element_pads_activate (element, TRUE)
激活pad,但是看了一遍下来,GstOMXH264Dec-omxh264dec的继承关系都没有重载激活pad相关的操作,所以调用常规的激活函数,选择了push模式,最终完成这个状态切换。
2.3 PAUSED操作
之前的文章有介绍过,pipeline的状态切换,是从sink到src进行,所以在dec完成该状态切换之后,pipeline将会继续设置上游element的状态为PAUSED。有一点需要注意的,gstomx中的大多数都是编码或者解码的element,这里我们以解码来介绍,编码也将会是类似的。decoder的上游将会是parser、demux以及最终的source,根据pipeline的属性,将会逐个设置,当将demux的状态切换为PAUSED时,激活pad的时候,将会与source element协商,然后采用PULL模式,demux从source获取文件数据(这个source可以是本地文件、网络文件以及其他,同时demux与上游element一般是采用pull模式)。
在demux切换到PAUSED状态时,将会激活pad,在这个过程demux将会进行解封装操作,知道音视频的类型等信息之后,将会添加pad到demux,这个操作将会触发pipeline的pad-added信号,一般的在信号的接收函数中将会把demux与下游的parser link起来(这个在之前的文章《gstreamer学习笔记—demux使用》介绍过)。
同时,之前的文章也介绍过,gstreamer是以stream的形式传输数据,所以在数据开始流通之前,将会发送stream-start
事件。
那么,在这里,又将会是谁负责处理EVENT呢?最终将会是gstvideodecoder.c中的gst_video_decoder_sink_event_default()负责处理sinkpad EVENT。
接收到stream-start
EVENT,将会调用gst_video_decoder_drain_out (decoder, FALSE),在该函数中,最终将会调用到gst_omx_video_dec_drain(),而gst_omx_video_dec_drain()函数是负责将dec component中的input buffer全都送往解码器的,但是由于此时解码器还没有工作,所以直接返回了。在dec sink pad的事件处理函数初始化upstream_tags,再将该事件往下游element送。
前面说到,element间已经link,link成功只是说明它们之间caps有交集,但是,相互之间,具体使用哪一种caps并没有协商好,所以在这个时候,将会有多次的pad caps查询操作,但是在omx dec中,都只是返回template caps,知道收到上游的GST_EVENT_CAPS。这个caps中将会是包含什么信息呢,简单理解,播放一个视频,在解封装的时候,就应该知道它的编码格式、视频分辨率以及帧率吧,这个caps EVENT包含的就是这些信息,当然,还有其他信息,但是我们先这样理解便好。
而在上游element push caps EVENT时,真正调用GST_PAD_EVENTFUNC()之前,会先通过pre_eventfunc_check()发送accept caps query,这个查询操作,是根据上游传进来的caps信息,再与自身caps取交集,有交集则将会把caps EVENT传递到下游element。
gstomx接收到caps EVENT,从event解析得到caps之后,将会调用gst_video_decoder_setcaps (decoder, caps)
将caps信息设置到解码器。
static gboolean
gst_video_decoder_sink_event_default (GstVideoDecoder * decoder,
GstEvent * event)
{
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_CAPS:
{
GstCaps *caps;
gst_event_parse_caps (event, &caps);
ret = gst_video_decoder_setcaps (decoder, caps);
gst_event_unref (event);
event = NULL;
break;
}
...
}
}
在dec中,一样的将会有一个GstVideoCodecState类型的input_state变量标示输入port的状态信息,所以,在setcaps()中,将会先检查这个状态信息是否存在,存在的话再确认信息是否和将要设置的caps一致,一致则不设置,退出函数,不一致,将释放input_state,我们第一次setcaps,这个变量为NULL,将进行设置操作。在setcaps()中,主要是调用decoder_class->set_format (decoder, state)设置dec参数,在这里,将会调用到gst_omx_video_dec_set_format(),下面,我们将一起来分析一下,这个函数是如何完成解码器参数设置。
- 将通过gst_omx_port_get_port_definition (self->dec_in_port, &port_def)获取omx IL层的dec input port的OMX_IndexParamPortDefinition参数信息,至于在这里,openMAX IL应该返回什么信息,我们接着往下看,看是怎么处理这个port_def参数的;
- 根据返回的信息,与将要设置的caps比较分辨率、帧率、codec_data(解码或者编码需要用到的信息)等数据是否一致;
- 将通过klass->is_format_change()调用具体的解码element特有的参数,是否一致,比如h264 dec将会调用gst_omx_h264_dec_is_format_change(),判断相应的信息再返回;
- 因为在设置caps的时候,可能这个解码器之前已经工作了,所以将会再次获取dec 组件的状态,如果不是OMX_StateLoaded状态,那么将会需要失能当前dec component;
- 如果dec component已经使能且caps没有改变,那么直接替换GstVideoCodecState返回成功,否则继续往下走;
- 如果需要失能dec component且caps已经改变,那么将失能dec component,再重新通过gst_omx_port_get_port_definition()获取dec component信息;
- 至此,dec component已经失能且caps确认改变,接着,将会根据caps信息,填充port_def,并通过gst_omx_port_update_port_definition()将port_def设置到dec component;
- 调用klass->set_format (self, self->dec_in_port, state)设置视频数据格式到解码器,由于gstomxvideodec.c只是omx解码器的继承类,所以这个函数将会由相应的解码component实现,比如H264的将会是gst_omx_h264_dec_set_format(),这样解码器就知道输出数据的格式、分辨率、帧率等信息;
- 最后通过gst_omx_port_update_port_definition (self->dec_out_port, NULL)更新port的port_def信息,更新self->input_state,setcaps操作完成。
设置了caps,音视频数据在gstreamer中是通过segment表示,所以,上游又将会发送一个GST_FORMAT_TIME类型的segment EVENT,segment中将包含将要播放视频的时间片段范围,dec将会保存该segment信息。
接下来将会有一些tag EVENT,包含编码格式、帧率、language-code等信息,这些我们先忽略。我们设置了解码器格式、分辨率,也都与上游element协商了caps,但是解码器的输出呢,输出格式、分辨率等信息都还没有与下游element协商好,什么时候会最终定下来呢,我们接着往下看。
2.4 PLAYING操作
来到最重要的接收数据环节,GstVideoDecoder的继承类,一般的都将会是gst_video_decoder_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)
负责处理上游element push的数据,除非继承类重写了sink pad chain_function。
gst_video_decoder_chain()很简单,核心在于通过gst_video_decoder_chain_forward (decoder, buf, FALSE)完成解码等操作。在gst_video_decoder_chain_forward()中,将会通过priv->current_frame保存视频buf,由于一般的解码数据都是压缩的,所以将会进入gst_video_decoder_decode_frame (decoder, priv->current_frame)。而在该函数,将会获取buf的pts、dts、duration等信息,最终通过decoder_class->handle_frame()调用到gstomxvideodec.c的gst_omx_video_dec_handle_frame()。
static GstFlowReturn
gst_video_decoder_decode_frame (GstVideoDecoder * decoder,
GstVideoCodecFrame * frame)
{
GstVideoDecoderPrivate *priv = decoder->priv;
GstVideoDecoderClass *decoder_class;
GstFlowReturn ret = GST_FLOW_OK;
decoder_class = GST_VIDEO_DECODER_GET_CLASS (decoder);
frame->distance_from_sync = priv->distance_from_sync;
priv->distance_from_sync++;
frame->pts = GST_BUFFER_PTS (frame->input_buffer);
frame->dts = GST_BUFFER_DTS (frame->input_buffer);
frame->duration = GST_BUFFER_DURATION (frame->input_buffer);
...
/* 将该frame保存到decoder->priv->frames,最终解码完成的时候有用到 */
priv->frames = g_list_append (priv->frames, frame);
...
/* do something with frame */
ret = decoder_class->handle_frame (decoder, frame);
return ret;
}
gst_omx_video_dec_handle_frame()又将会进行什么操作呢,继续看。
static GstFlowReturn
gst_omx_video_dec_handle_frame (GstVideoDecoder * decoder,
GstVideoCodecFrame * frame)
{
/* 在这里,将会检查dec component是否已经开始解码了,如果还没有,
* 还需要检查当前帧是否是关键帧,因为解码需要从关键帧开始 */
if (!self->started) {
if (!GST_VIDEO_CODEC_FRAME_IS_SYNC_POINT (frame)) {
gst_video_decoder_drop_frame (GST_VIDEO_DECODER (self), frame);
return GST_FLOW_OK;
}
/* 还没有开始解码,当前帧已经是关键帧,如果
* output port flushing,enable dec component */
if (gst_omx_port_is_flushing (self->dec_out_port)) {
if (!gst_omx_video_dec_enable (self, frame->input_buffer))
goto enable_error;
}
/* 关键帧、解码component已经使能,运行解码线程 */
gst_pad_start_task (GST_VIDEO_DECODER_SRC_PAD (self),
(GstTaskFunction) gst_omx_video_dec_loop, decoder, NULL);
}
...
}
通过上面我们可以清晰的知道,刚开始进行handle_frame()的操作,先不管dec component是如何进行解码的,我们看看gst_omx_video_dec_enable()干了什么。
static gboolean
gst_omx_video_dec_enable (GstOMXVideoDec * self, GstBuffer * input)
{
GstOMXVideoDecClass *klass = GST_OMX_VIDEO_DEC_GET_CLASS (self);
/* 选择内存申请的方式,是动态还是其它 */
self->input_allocation = gst_omx_video_dec_pick_input_allocation_mode (self,
input);
if (self->disabled) {
...
} else {
/* 进行协商 */
if (!gst_omx_video_dec_negotiate (self))
GST_LOG_OBJECT (self, "Negotiation failed, will get output format later");
if (!(klass->cdata.hacks & GST_OMX_HACK_NO_DISABLE_OUTPORT)) {
...
} else {
/* 使能dec component */
if (gst_omx_component_set_state (self->dec,
OMX_StateIdle) != OMX_ErrorNone)
return FALSE;
/* 申请input port buffer */
if (!gst_omx_video_dec_allocate_in_buffers (self))
return FALSE;
/* 申请output port buffer */
if (gst_omx_port_allocate_buffers (self->dec_out_port) != OMX_ErrorNone)
return FALSE;
}
...
}
/* 设置dec_in_port->flushing和dec_out_port->flushing为FALSE */
gst_omx_port_set_flushing (self->dec_in_port, 5 * GST_SECOND, FALSE);
gst_omx_port_set_flushing (self->dec_out_port, 5 * GST_SECOND, FALSE);
...
self->disabled = FALSE;
return TRUE;
}
在gst_omx_video_dec_enable()中,由于我们是第一次使能dec component,所以将会进行协商,协商之后就进行内存申请。
需要注意的是,这里的协商函数gst_omx_video_dec_negotiate(),是 与下游element进行协商,上游的element之前已经协商完成了。gst_omx_video_dec_negotiate()主要完成以下操作:
- 通过gst_pad_peer_query_caps()获取下游element支持的caps;
- 通过gst_omx_video_get_supported_colorformats()调用gst_omx_component_get_parameter()获取OMX_IndexParamVideoPortFormat参数信息,这样参数将会返回openMAX IL支持的format,这里将会是一个枚举过程;
- 得到双方支持的format之后,取交集,然后通过
gst_omx_component_set_parameter (self->dec, OMX_IndexParamVideoPortFormat, ¶m)
将format等信息设置到解码器,至此,协商完成。
协商完成之后,接下来就是input port和output port的内存申请。
在通过gst_omx_video_dec_allocate_in_buffers()申请input buf的时候,最终通过gst_omx_port_allocate_buffers()调用到gst_omx_port_allocate_buffers_unlocked()。该函数主要是做三件事:
- 通过
gst_omx_port_update_port_definition (port, NULL)
更新port信息; - 通过宏OMX_AllocateBuffer调用openMAX IL的AllocateBuffer函数申请buffer;
- 将申请到的buffer添加到port->pending_buffers。
同样的,在通过gst_omx_port_allocate_buffers (self->dec_out_port)
给output port申请buffer也是一样的操作,输出buffer一般是解码器申请的物理地址连续的内存,GstOMXBuffer类型的变量buf->omx_buf->pBuffer就是指向这块内存(虚拟地址)。
协商之后,申请buffer,接着将会设置dec component状态为OMX_StateExecuting,最后将输入输出port的flushing置为FALSE,使能dec component完成。
回到handle_frame()—gst_omx_video_dec_handle_frame(),在使能dec component之后,将会创建task运行gst_omx_video_dec_loop()负责解码,到这里,我们的解码器硬件已经初始化完成,数据的格式、分辨率、解码输出格式、buffer等参数已经设置,解码task已经运行,接下来就是解码操作了。
在task中,通过gst_omx_port_acquire_buffer()申请buffer之后,由于是第一次运行,所以将会有以下这个操作:
static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
...
/* 这个判断,在刚进这个task的时候会进入
* 因为gst_pad_has_current_caps()检查的是输入参数pad是否有
* GST_EVENT_CAPS EVENT,在这个时候,dec src pad是没有的,
* 所以将会返回空,证明此时caps改变了。此处具体原因我也不是很明白 */
if (!gst_pad_has_current_caps (GST_VIDEO_DECODER_SRC_PAD (self)) ||
acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE) {
/* Reallocate all buffers */
if (acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE
&& gst_omx_port_is_enabled (port)) {
...
}
if (acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE) {
...
} else {
/* Just update caps */
/* 获取port参数信息 */
gst_omx_port_get_port_definition (port, &port_def);
format =
gst_omx_video_get_format_from_omx (port_def.format.video.
eColorFormat);
/* 设置format等信息保存到output port Status */
state = gst_video_decoder_set_output_state (GST_VIDEO_DECODER (self),
format, port_def.format.video.nFrameWidth,
port_def.format.video.nFrameHeight, self->input_state);
/* Take framerate and pixel-aspect-ratio from sinkpad caps */
/* 与下游element协商,并完成pool申请 */
if (!gst_video_decoder_negotiate (GST_VIDEO_DECODER (self))) {
}
}
...
}
接下来,我们将分析一下gst_video_decoder_negotiate(),看看它又是如何协商的,在该函数中,将会通过klass->negotiate()形式调用到gstvideodecoder.c的gst_video_decoder_negotiate_default()。
static gboolean
gst_video_decoder_negotiate_default (GstVideoDecoder * decoder)
{
...
if (state->allocation_caps == NULL)
state->allocation_caps = gst_caps_ref (state->caps);
...
/* 此处设置之后,下次loop task就不会因为gst_pad_has_current_caps()而进入协商 */
ret = gst_pad_set_caps (decoder->srcpad, state->caps);
...
decoder->priv->output_state_changed = FALSE;
/* Negotiate pool */
ret = gst_video_decoder_negotiate_pool (decoder, state->allocation_caps);
}
在gst_video_decoder_negotiate_default()中,我们主要介绍一下gst_video_decoder_negotiate_pool()。
在函数中,将会先与下游element进行GST_QUERY_ALLOCATION查询,一般的将会调用下游propose_allocation(),通过这样的方式,查询下游element buffer pool的配置,由于gstreamer可以两个element共用buffer pool的,所以会有这样的一个查询操作。通过klass->decide_allocation (decoder, query)调用到gst_omx_video_dec_decide_allocation(),最终将会从QUERY中获取相应的信息并创建pool,激活pool。在这个过程中,将会调用到gstomxbufferpool.c中的类函数,完成buffer pool的参数设置,buffer pool方面的不是很了解,这个就暂时不说了。真正的解码buffer我们之前已经申请了,而gstomxbufferpool.c中的buffer pool,更多的是管理我们之前申请的buffer信息,比如物理地址、分辨率、对齐方式等一些信息,而后将会根据解码buffer与这些buffer pool的关系,得到相应的数据信息传递给下游。一般的,buffer传递到下游之后,将buffer unref,buffer内存将会被回收释放,但是有些element是不希望自身产生的buffer传递到下游使用之后被回收释放,而是返回自身buffer管理队列中,这个时候就将使用到bufferpool管理。
可能有些小伙伴看到这里会混淆了,有点晕,一会gstbuffer,一会gstbufferpool,同时他们两个都会有创建相应的buffer,是不是重复申请了呢?其实并没有。比如说这里,解码器输出的RAW数据buffer(gstbuffer),他们申请了物理内存连续的内存块,然后,我们需要通过bufferpool将这些buffer管理起来,自然的就会需要保存相应的信息,这个时候又申请buffer(常用的内存)来保存这些信息,并通过特定的关系与我们之前申请到的物理内存连续的buffer连续起来,这样就可以通过这个关系,达到管理pool即为管理解码器buffer。pool的buffer为常规buffer,常规buffer保存的信息可以找到解码器使用的buffer。
上面我们在介绍gst_omx_video_dec_handle_frame()的时候,就说到在在使能的时候通过gst_omx_port_allocate_buffers (self->dec_out_port)
申请output buffer,但是,实际上比这个还要复杂。因为有和openMAX IL交互的,底层反馈回来的命令也就会影响到相应的buffer。在实际测试,gst_omx_video_dec_loop()前两次循环,第一次会因为gst_pad_has_current_caps (GST_VIDEO_DECODER_SRC_PAD (self))
返回NULL,而重新与下游进行协商,这个我们上面简单的介绍了;第二次,会因为gst_omx_port_acquire_buffer()返回GST_OMX_ACQUIRE_BUFFER_RECONFIGURE,将会调用gst_omx_video_dec_reconfigure_output_port()重新分配output buffer,同时建立bufferpool管理buffer。
编码前数据如何传递到openMAX IL层的解码器,解码后的数据又是如何传回gstomx,接下来继续分析。
我们继续看gst_omx_video_dec_handle_frame()操作:
static GstFlowReturn
gst_omx_video_dec_handle_frame (GstVideoDecoder * decoder,
GstVideoCodecFrame * frame)
{
self = GST_OMX_VIDEO_DEC (decoder);
klass = GST_OMX_VIDEO_DEC_GET_CLASS (self);
port = self->dec_in_port;
...
/* 获取上游element传递的buffer大小 */
size = gst_buffer_get_size (frame->input_buffer);
while (!done) {
/* 向解码器申请输入buf */
acq_ret = gst_omx_port_acquire_buffer (port, &buf);
...
/* buf->omx_buf->nAllocLen代表buffer总长度 */
/* buf->omx_buf->nOffset代表buffer已经使用的长度 */
buf->omx_buf->nFilledLen =
MIN (size - offset, buf->omx_buf->nAllocLen - buf->omx_buf->nOffset);
/* 拷贝编码数据到dec input port buffer */
gst_buffer_extract (frame->input_buffer, offset,
buf->omx_buf->pBuffer + buf->omx_buf->nOffset,
buf->omx_buf->nFilledLen);
offset += buf->omx_buf->nFilledLen;
if (offset == size)
done = TRUE;
...
self->started = TRUE;
/* 将编码数据送往解码器 */
err = gst_omx_port_release_buffer (port, buf);
if (err != OMX_ErrorNone)
goto release_error;
first_ouput_buffer = FALSE;
}
...
}
下面,我们将会介绍,dec component是如何向openMAX IL申请输入buffer,这个过程是gstomx.c中的gst_omx_port_acquire_buffer (GstOMXPort * port, GstOMXBuffer ** buf)
完成:
- 由于openMAX内部消息也都是采用消息的机制,所以,在该函数中,将会先通过gst_omx_component_handle_messages (comp)处理消息事件;
- 接着将会检查port是否是flushing状态,进行解码处理,不能是处于该状态;
- 判断是input port还是output port申请buffer,将会检查是否需要重新配置port;
- 还记得我们上面介绍过的,在申请buffer的时候,buffer是添加到port->pending_buffers链表,所以检查该链表是否为空;
- 最终通过
g_queue_pop_head (&port->pending_buffers)
从port链表中拿到buffer并返回。
在gst_omx_video_dec_handle_frame()中拷贝解码数据到input port buffer的过程,上面代码已经解析得很清楚了,接下来将看看,是如何通过gst_omx_port_release_buffer (port, buf)
将编码数据送往解码器的。在该函数中,一样的,将会先处理消息队列数据,接着检查port状态,最后,如果是input port,将会通过OMX_EmptyThisBuffer (comp->handle, buf->omx_buf)
将input buffer传递到解码器;如果是output port,将通过OMX_FillThisBuffer (comp->handle, buf->omx_buf)
将output buffer返还解码器。宏OMX_EmptyThisBuffer和OMX_FillThisBuffer都是对openMAX IL层函数的封装,通过他们将会完成以上的操作,这些函数,都是需要芯片厂商根据自己平台的编解码接口按照openMAX协议封装提供的。
至此,编码数据已成功送往解码器,gst_omx_video_dec_handle_frame()解析完毕。那么,解码完成后的RAW数据又是什么时候返回呢,我们继续看代码。
还记得我们在handle_frame()中,发现还没有开始解码将会启动task运行gst_omx_video_dec_loop(),解码完成之后的数据会不会是在这里输出呢,我们来看看它的操作。
static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
port = self->dec_out_port;
/* 向dec output port申请解码完成数据 */
acq_return = gst_omx_port_acquire_buffer (port, &buf);
}
同样的,dec output port与input一样,也是通过gst_omx_port_acquire_buffer()向解码器申请buffer,只不过这次是向output port申请buffer,一样是从port->pending_buffers队列中获取buffer的,究竟是谁将编码完成RAW buffer添加到该队列呢 ,同样的,编码前向解码器申请buffer填充编码数据,这个buffer又是谁添加到pending_buffers队列呢?
static OMX_CALLBACKTYPE callbacks =
{ EventHandler, EmptyBufferDone, FillBufferDone };
还记得上面在介绍从NULL切换到READY时通过gst_omx_component_new()创建component,而在该函数中,通过get_handle()获取component handle的时候将回调函数组callbacks传递到openMAX IL中。就是这个回调函数组,在送往解码器解码的数据使用完毕之后,调用callbacks中的EmptyBufferDone()将input port buffer添加到pending_buffers队列,FillBufferDone()将解码完成后output port输出RAW数据buffer添加到pending_buffers队列。
无论是EmptyBufferDone()还是FillBufferDone(),都只是将相应的参数信息封装成GST_OMX_MESSAGE_BUFFER_DONE类型的消息,然后发送到comp->messages,最后当其他函数调用gst_omx_component_handle_messages()处理消息的时候,将会进行相应的处理,最后解析消息,将buffer添加到相应port的pending_buffers队列。这样,应该知道input/output port是如果进行buffer队列管理了吧。
继续回到gst_omx_video_dec_loop (),通过gst_omx_port_acquire_buffer()拿到解码完成的RAW数据之后,接下来应该是送往下游element了吧,但是,视频数据有dts、pts,dts为解码时间戳,pts为显示时间戳,所以,接下来,gst_omx_video_dec_loop ()将会根据buffer的时间戳检查有效性,并丢弃比当前buffer时间戳前的frame,认为这帧已经丢失了,代码如下:
static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
...
/* 首先,通过gst_video_decoder_get_frames()返回的frames链表数据
* 是在gst_video_decoder_decode_frame()中将新来的frame添加到链
* 表中,而gst_omx_video_find_nearest_frame()则是根据传进来的buf
* 时间戳,从参数链表中找到时间戳相差最近的frame并返回 */
frame = gst_omx_video_find_nearest_frame (buf,
gst_video_decoder_get_frames (GST_VIDEO_DECODER (self)));
/* 根据buf时间戳,将链表中比该时间早的frame删除出链表 */
gst_omx_video_dec_clean_older_frames (self, buf,
gst_video_decoder_get_frames (GST_VIDEO_DECODER (self)));
...
}
完成相应的时间戳等信息填充之后,又将会进入关键环节,将数据push到下游,一般的代码流程如下:
static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
...
if(...) {
...
} else if (buf->omx_buf->nFilledLen > 0 || buf->eglimage) {
if (self->out_port_pool) {
gint i, n;
GstBuffer *outbuf;
GstBufferPoolAcquireParams params = { 0, };
n = port->buffers->len;
for (i = 0; i < n; i++) {
GstOMXBuffer *tmp = g_ptr_array_index (port->buffers, i);
/* 在得到output buffer之后,通过这样的一种形式,得到当前buffer的序列 */
if (tmp == buf)
break;
}
g_assert (i != n);
/* 得到序列之后,保存到current_buffer_index变量中,待会
* 通过这个索引从buffer pool得到buffer相应信息保存到outbuf */
GST_OMX_BUFFER_POOL (self->out_port_pool)->current_buffer_index = i;
flow_ret =
gst_buffer_pool_acquire_buffer (self->out_port_pool,
&outbuf, ¶ms);
if (flow_ret != GST_FLOW_OK) {
...
goto invalid_buffer;
}
...
frame->output_buffer = outbuf;
/* 成功完成相应的数据获取了,将会通过gst_video_decoder_finish_frame()
* 调用gst_video_decoder_clip_and_push_buf()将buffer传递到下游*/
flow_ret =
gst_video_decoder_finish_frame (GST_VIDEO_DECODER (self), frame);
frame = NULL;
buf = NULL;
} else {
...
}
} else if (frame != NULL) {
...
}
...
解码前的buffer申请、释放我们知道了,那么解码后的数据buffer又是在哪里释放的呢,这个就是bufferpool的管理功能,当下游使用完buffer之后,将会unref,此时将会通过buffer的释放函数,调用到gstomxbufferpool.c中的gst_omx_buffer_pool_release_buffer(),完成相应的buffer释放,完成一个循环。
由于我是忽略了与gstomx对接的openMAX IL来解析gstomxvideodec,所以有很多地方忽略了,比如上面我们说到的OMX_CALLBACKTYPE类型的对调函数,这个当中还有一个重要的事件回调函数EventHandler()。openMAX IL需要与gstomx交互时,将会发送相应的事件,而该函数就是就是处理相应的事件,而后将事件信息封装为相应的message,在gstomx.c中的gst_omx_component_handle_messages()函数将会处理相应的message,这样,就完成了gstomx与openMAX IL的相应交互处理。除此之外,还有很多的地方理解不到位,希望看到这里,有理解到位的小伙伴可以提点一下我,谢谢。
三、总结
在上面两节,我们介绍了gstomx是如何基于openMAX完成了解码操作,下面我们通过简单的序列图理解这个关系流程,详细如下:
图已经被CSDN吃了,日后再补回来。。。
以上是个人理解,有理解错误的地方,欢迎指出,感谢
openMAX参考-YellowMax-博客 ↩︎