v4l2src element源码位于gst-plugins-good-xxx/sys/v4l2/gstv4l2src.c,v4l2src主要是从v4l2设备获取视频数据的element,基于v4l2框架采集相应设备的数据。它的继承关系如下:
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstBaseSrc
+----GstPushSrc
+----GstV4l2Src
下面将学习它的结构,了解它是如何与下游element适配、传输数据,只涉及到v4l2src相关的操作。
v4l2src实例创建
在测试过程中,我们使用的是uvc camera,通过gstreamer捕获camera输出的MJPEG格式数据,解码之后再编码保存,详细命令如下:
gst-launch-1.0 v4l2src ! omxmjpegvideodec ! omxmjpegvideoenc ! image/jpeg ,width=1280,height=720 ! multifilesink location="/tmp/frame%d.jpeg" max-files=1
通过这个命令,我们将会在/tmp目录下得到编码为JPEG的图像数据,那么,v4l2src的创建过程是怎样的呢,下面来分析一下。
首先的,应用层将会通过gst_element_factory_make()
函数创建v4l2src,在创建过程,将会先后的调用了gst_v4l2src_class_init (GstV4l2SrcClass * klass)
和gst_v4l2src_init (GstV4l2Src * v4l2src)
函数。在gst_v4l2src_class_init()函数中,很常规的,重载各个需要的父类函数指针,增加pad模板,在gst_v4l2src_init()函数则是创建v4l2_buf_type类型为V4L2_BUF_TYPE_VIDEO_CAPTURE的v4l2src->v4l2object实例对象。这样,v4l2src的实例创建完成,与创建其他的element实例没什么区别。
在上面的那个命令,创建各个element之后,将会创建pipeline,并将各个element添加到pipeline,下一步,将会尝试的进行element link。
之前在gstreamer学习笔记—pad定义、连接、流动 已经分析过element link,这里就不在重述,只会简单的提到,从哪里调用到了v4l2src的相关函数。在link过程中,会通过gst_pad_query_caps()
函数查询v4l2src srcpad的支持的caps,在查询的过程,将会由于父类的关系以及v4l2src的class_init函数重载了get_caps函数basesrc_class->get_caps = GST_DEBUG_FUNCPTR (gst_v4l2src_get_caps)
,所以,最终将会通过gst_v4l2src_get_caps()函数获取v4l2src:src支持的caps。
gst_v4l2src_get_caps()定义如下,从实现可以看到,他将会先检查v4l2设备时候已经打开,如果没有打开,则直接返回class_init函数添加的pad模板caps,如果已经打开,将会通过gst_v4l2_object_get_caps (obj, filter)
函数探测设备实际支持的caps。
static GstCaps *
gst_v4l2src_get_caps (GstBaseSrc * src, GstCaps * filter)
{
GstV4l2Src *v4l2src;
GstV4l2Object *obj;
v4l2src = GST_V4L2SRC (src);
obj = v4l2src->v4l2object;
/* 检查是否已经打开设备,如果没有打开,则直接返回pad template caps */
if (!GST_V4L2_IS_OPEN (obj)) {
return gst_pad_get_pad_template_caps (GST_BASE_SRC_PAD (v4l2src));
}
/* 如果设备已经打开,将会检查设备支持的格式再返回 */
return gst_v4l2_object_get_caps (obj, filter);
}
v4l2src与下游element link,其实就是简单的查询两者的pad caps是否有交集,pad是否还没有连接,符合有交集且都还没有连接,则可以link成功,双方互相保存信息。
v4l2src状态之READY
从前面几篇文章应该会稍微了解到,gstreamer中,创建了相应的element之后,添加到pipeline进行link之后,下一步就是相应element的状态转变了,gstreamer element的状态转变是从sink到src的,下面,我们探究一下,v4l2src从NULL到READY进行了什么操作。
static GstStateChangeReturn
gst_v4l2src_change_state (GstElement * element, GstStateChange transition)
{
switch (transition) {
case GST_STATE_CHANGE_NULL_TO_READY:
/* open the device */
if (!gst_v4l2_object_open (obj))
return GST_STATE_CHANGE_FAILURE;
break;
default:
break;
}
...
}
通过gst_v4l2src_change_state()
函数可以知道,从NULL到READY实际就是调用了gst_v4l2_object_open()
函数,这个函数详细干了什么,继续看。
gboolean
gst_v4l2_object_open (GstV4l2Object * v4l2object)
{
if (gst_v4l2_open (v4l2object))
gst_v4l2_set_defaults (v4l2object);
else
return FALSE;
return TRUE;
}
在gst_v4l2_open()函数中,将会打开相应的video节点,然后会调用到VIDIOC_QUERYCAP查询设备的相应信息,之后,会调用gst_v4l2_fill_lists()函数操作VIDIOC_ENUMINPUT获取input的相关信息并保存到v4l2object->channels,同时还会通过VIDIOC_QUERYCTRL查询设备支持的ctrl操作。
而在gst_v4l2_set_defaults()函数,主要是获取tuner相关的信息,不过好像camera都没有进行相应的操作的,忽略。但是可以看到gst_tuner_set_channel()
是调用S_INPUT接口的,有些平台的csi camera需要进行相应的S_INPUT操作,这个时候就可以在这里进行,感兴趣的可以跟踪一下这里的代码,看看怎么实现,不过好像需要自己实现一个channel-changed
的信号处理函数。
就是以上简单的几步,v4l2src完成了从NULL到READY的状态转变,简单的说,就只是打开video设备而已。
v4l2src状态之PAUSED
在element状态转变为READY之后,下一步,将会转变为PAUSED状态,激活相应的pad,这一步,v4l2src又做了什么呢,继续往下看。
从gst_v4l2src_change_state()
函数可以看到,v4l2src在READY转变到PAUSED状态并没有定义具体的函数,直接调用父类PushSrc的change_state函数,那么,我们来看看,它的父类,又是怎么进行操作的。但是好像PushSrc也没有实现相应的函数,继续看PushSrc的父类BaseSrc。最后发现,都直接调用到GstElement的
change_state函数了,那么,我们看看,在这个阶段,v4l2src究竟干了什么。
static GstStateChangeReturn
gst_element_change_state_func (GstElement * element, GstStateChange transition)
{
switch (transition) {
case GST_STATE_CHANGE_NULL_TO_READY:
break;
case GST_STATE_CHANGE_READY_TO_PAUSED:
if (!gst_element_pads_activate (element, TRUE)) {
result = GST_STATE_CHANGE_FAILURE;
}
}
...
}
从上面可以看到,其实就是通过gst_element_pads_activate()
函数激活pad。之前介绍过,在gst_element_pads_activate()函数中,就是针对element srcpad和sinkpad先后调用activate_pads()函数。我们知道v4l2src
只有src pad,但是在v4l2src这里,是做了什么操作呢,往下看。
在activate_pads()中,其实就是调用gst_pad_set_active()函数,在该函数中将会调用(GST_PAD_ACTIVATEFUNC (pad)) (pad, parent)
函数激活pad。由于v4l2src继承关系,父类没有实现GST_PAD_ACTIVATEFUNC函数,那么,将会调用gstpad.c中,gst_pad_init()初始化的GST_PAD_ACTIVATEFUNC (pad) = gst_pad_activate_default
。那么v4l2src激活pad将会调用gst_pad_activate_default()函数。
static gboolean
gst_pad_activate_default (GstPad * pad, GstObject * parent)
{
g_return_val_if_fail (GST_IS_PAD (pad), FALSE);
return activate_mode_internal (pad, parent, GST_PAD_MODE_PUSH, TRUE);
}
通过上面我们可以知道,默认的activatefunc使用的是PUSH模式,好,继续看,activate_mode_internal进行了什么操作。
由于上面传进来的是PUSH模式,那么我们介绍的,也是以这个流程为主。
static gboolean
activate_mode_internal (GstPad * pad, GstObject * parent, GstPadMode mode,
gboolean active)
{
...
/* Mark pad as needing reconfiguration */
if (active)
GST_OBJECT_FLAG_SET (pad, GST_PAD_FLAG_NEED_RECONFIGURE);
/* pre_activate returns TRUE if we weren't already in the process of
1. switching to the 'new' mode */
if (pre_activate (pad, new)) {
if (GST_PAD_ACTIVATEMODEFUNC (pad)) {
if (G_UNLIKELY (!GST_PAD_ACTIVATEMODEFUNC (pad) (pad, parent, mode,
active)))
goto failure;
}
...
}
...
}
从activate_mode_internal()的实现可以了解到,先设置pad的GST_PAD_FLAG_NEED_RECONFIGURE标志位,而后调用GST_PAD_ACTIVATEMODEFUNC函数,这里将会调用到gst_base_src_activate_mode()
函数。
static gboolean
gst_base_src_activate_mode (GstPad * pad, GstObject * parent,
GstPadMode mode, gboolean active)
{
switch (mode) {
...
case GST_PAD_MODE_PUSH:
src->priv->stream_start_pending = active;
res = gst_base_src_activate_push (pad, parent, active);
break;
...
}
return res;
}
然后在gst_base_src_activate_push()函数再调用gst_base_src_start (basesrc)函数激活pad。而gst_base_src_start()函数也是调用子类的start函数,这里将会调用到gst_v4l2src_start()
,但是好像发现,在gst_v4l2src_start()函数也并没有进行太多的有效操作,更多的是一些参数的复位,那么,激活pad操作的关键究竟是在哪里呢,谁负责启动数据传输呢,看代码。
回到gst_base_src_start()函数,在调用完start函数函数之后,又将会调用gst_base_src_start_complete()
函数完成异步启动操作。在该函数中,将会检查是否需要seek,然后再调用gst_base_src_perform_seek (basesrc, event, FALSE)
函数,注意,在v4l2src这里,传进的event参数为NULL。
在上面我们已经提到gst_base_src_perform_seek()函数的参数,那么具体流程又是怎样的呢,继续看看。
static gboolean
gst_base_src_perform_seek (GstBaseSrc * src, GstEvent * event, gboolean unlock)
{
...
/* and restart the task in case it got paused explicitly or by
2. the FLUSH_START event we pushed out. */
tres = gst_pad_start_task (src->srcpad, (GstTaskFunction) gst_base_src_loop,
src->srcpad, NULL);
...
}
gst_base_src_perform_seek()函数很长,具体是做什么的我也分析不清楚,理解为有点像在播放视频时是否需要快进吧,检查segment的信息并进行设置,但是我们关心的,只是它在哪里启动task,task都运行什么。从上面可以看到,在该函数中已经运行task了,将会运行gst_base_src_loop()
函数。
剩下的pad激活操作也就没什么关键的了,主要都是一些信号、消息的发送处理了,至此,v4l2src PAUSED状态转换完成。
v4l2src状态之PLAYING
在经过PAUSED状态之后,到达了PLAYING,那么,在这个阶段的状态转变,又将会进行什么操作呢,camera相关的操作设置都还没有调用,又将会是在什么时候进行呢,我们继续往下看。
PLAYING与PAUSED状态的差别,简单的说就是时钟是运行的,数据是流动的,所以,在转变为PLAYING状态,将会为pipeline的各个element设置clock。按照上一节介绍,在切换到PLAYING状态时,又将会调用到gst_base_src_change_state()
函数。
static GstStateChangeReturn
gst_base_src_change_state (GstElement * element, GstStateChange transition)
{
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
GST_DEBUG_OBJECT (basesrc, "PAUSED->PLAYING");
if (gst_base_src_is_live (basesrc)) {
/* now we can start playback */
gst_base_src_set_playing (basesrc, TRUE);
}
break;
...
}
}
在gst_v4l2src_init()有调用这样一个函数gst_base_src_set_live (GST_BASE_SRC (v4l2src), TRUE)
,所以将会进入gst_base_src_set_playing()函数。在gst_base_src_set_playing()函数中,将会启动系统时钟,启动task,这样,切换到PLAYING状态完成。
是否记得我们在前一节已经说了v4l2src在进入PAUSED状态的时候会通过一个task运行gst_base_src_loop()函数,刚才切换到PLAYING状态又启动task,这个task运行的gst_base_src_loop()函数究竟干了什么工作,下面我们一起来看看。
v4l2src task之gst_base_src_loop()
我们直入主题,看看该函数究竟都干了什么。
- 首先的,将会通过
gst_base_src_send_stream_start()
函数发送一个GST_EVENT_STREAM_START
事件给下游element,告知上游将开始传输数据; - 可能在stream流动的过程中,caps发生改变呢,这个时候是不是需要重新的协商呢,所以,接下来将会通过
gst_pad_check_reconfigure()
函数检查是否需要重新配置pad。
- 不知道大家有没有留意,在
activate_mode_internal()
函数激活pad的时候,我们就有设置GST_PAD_FLAG_NEED_RECONFIGURE
标志位,同时在gst_base_src_start_complete()
函数中也有通过gst_pad_mark_reconfigure()
函数设置该标志位,所以在task第一次运行该函数的时候,将会通过gst_base_src_negotiate (src)
重新进行协商; - 在gst_base_src_negotiate()函数中,将会先通过bclass->negotiate (basesrc)调用
gst_v4l2src_negotiate()
函数,在该函数中进行v4l2src srcpad caps的查询以及下游element sinkpad caps查询,然后取交集,得到交集caps之后,将会通过gst_v4l2src_fixate()
函数设置v4l2设备数据输出格式,最后将会发送GST_EVENT_CAPS
事件到下游; - 回到gst_base_src_negotiate()函数,协商完成之后,已经设置了v4l2设备的format,接下来,将会调用
gst_base_src_prepare_allocation (basesrc, caps)
函数申请内存资源等。在该函数中,将会先通过caps得到相应的一些信息,而后调用bclass->decide_allocation (basesrc, query),这个时候调用到gst_v4l2src_decide_allocation()
函数; - 在gst_v4l2src_decide_allocation()函数中,先查询、协商得到一系列的参数,比如format、分辨率、帧率、buffer个数等,然后将通过
gst_buffer_pool_set_active (src->v4l2object->pool, TRUE)
函数激活pool; - 在激活pool过程中,就是按照v4l2设备的要求申请buffer,将相应的buffer添加到设备驱动,然后开启流;
- 不知道大家有没有留意,在
- 在调用gst_base_src_prepare_allocation()函数之后,v4l2设备初始化已经完成,需要设置的操作都已经进行了,接下来,回到gst_base_src_loop()函数;
- 上面已经进行了caps是否改变等操作,接下来,将会调用
gst_base_src_get_range()
函数获取buf数据,在该函数中,将会通过bclass->create (src, offset, length, &res_buf)函数调用到gst_v4l2src_create()
函数获取数据; - 获取到数据之后,gst_base_src_loop()又将会调用
gst_pad_push (pad, buf)
函数将buf推送到下游element,至此,gst_base_src_loop()完成一次调用;
通过上面的简单介绍,我们大概了解v4l2src的启动流程,函数调用关系,但是,每一步详细做了什么操作,现在还是不了解的,接下来,我们将着重分析这个流程的关键函数。
v4l2src协商之gst_v4l2src_negotiate()
在v4l2src的task中,会检查是否需要重新配置caps,这个时候将会重新协商,那么协商过程的详细操作就怎样的呢,下面我们一起来学习一下。
协商,自然得知道自己pad支持的caps,所以,将会通过gst_pad_query_caps()调用到gst_v4l2src_get_caps()
函数。在gst_v4l2src_get_caps()函数,将会通过gst_v4l2_object_fill_format_list()函数调用VIDIOC_ENUM_FMT
枚举设备的格式,接着又会通过gst_v4l2_object_probe_caps_for_format()函数调用VIDIOC_ENUM_FRAMESIZES
针对每种格式枚举分辨率,同时会针对每个格式的分辨率通过gst_v4l2_object_probe_caps_for_format_and_size()操作VIDIOC_ENUM_FRAMEINTERVALS
获取支持的分辨率,至此,相对v4l2src查询src pad完成。
回到gst_v4l2src_negotiate(),查询自身 srcpad的caps之后,又将会通过gst_pad_peer_query_caps()函数查询下游element sinkpad caps,然后取交集,得到需要设置的caps。
接下来将会通过gst_v4l2src_fixate()函数将caps的参数设置到设备。在该函数中,将会检查分辨率等参数,同时选择最优的帧率,然后,将会通过gst_v4l2src_set_format()函数最终操作VIDIOC_S_FMT
将格式、分辨参数设置到设备,同时会通过VIDIOC_S_PARM
设置帧率参数。完成设置之后,还会通过gst_v4l2_object_setup_pool (v4l2object, caps)函数初始化pool。
完成以上操作之后,将会发送GST_EVENT_CAPS事件到下游element,至此gst_v4l2src_negotiate()调用完成,但是gst_base_src_negotiate()的调用并没有结束,得到准确的caps之后,接下来将是gst_base_src_prepare_allocation()的调用,通过gst_v4l2src_decide_allocation()配置pool。
v4l2src pool之gst_v4l2src_decide_allocation()
该函数主要将会是根据caps的参数,配置pool,并激活pool,简单代码如下:
static gboolean
gst_v4l2src_decide_allocation (GstBaseSrc * bsrc, GstQuery * query)
{
...
if (ret) {
ret = gst_v4l2_object_decide_allocation (src->v4l2object, query);
if (ret)
ret = GST_BASE_SRC_CLASS (parent_class)->decide_allocation (bsrc, query);
}
if (ret) {
if (!gst_buffer_pool_set_active (src->v4l2object->pool, TRUE))
goto activate_failed;
}
...
}
在gst_v4l2_object_decide_allocation()中,将会将v4l2src设备输出的格式、分辨率、每帧的大小、buf个数等信息设置到pool。其中需要注意一点,如果说,在使用camera的过程中,没法正常出图,然后log中有Video device did not suggest any buffer size.
那么这个是因为在配置pool的每帧大小时,发现这个size为0而设置失败导致的。这个size一般是通过size = obj->info.size
赋值的,而obj->info.size的值,则是在VIDIOC_S_FMT的时候,根据返回的f->fmt.pix.sizeimage
赋值。所以,出现这样的问题,需要去检查你的camera驱动中,有没有返回该值,这个值的大小应该为每帧的数据量大小。
接着,将会通过gst_buffer_pool_set_active()激活pool。在该函数中,将会通过do_start()最终调用到gst_v4l2_buffer_pool_start()。而gst_v4l2_buffer_pool_start()函数,先获取pool的配置参数,然后将通过gst_v4l2_allocator_start()调用VIDIOC_REQBUFS
向内核v4l2申请buffer,同时会在gst_v4l2_allocator_start()中通过gst_v4l2_memory_group_new()操作VIDIOC_QUERYBUF
获取buf的相应信息并保存到groups。
在gst_v4l2_buffer_pool_start()中,得到buf的相应信息之后,又将通过pclass->start()
调用父类的default_start()分配缓冲区内存。
static gboolean
default_start (GstBufferPool * pool)
{
pclass = GST_BUFFER_POOL_GET_CLASS (pool);
/* we need to prealloc buffers */
for (i = 0; i < priv->min_buffers; i++) {
GstBuffer *buffer;
if (do_alloc_buffer (pool, &buffer, NULL) != GST_FLOW_OK)
goto alloc_failed;
/* release to the queue, we call the vmethod directly, we don't need to do
* the other refcount handling right now. */
if (G_LIKELY (pclass->release_buffer))
pclass->release_buffer (pool, buffer);
}
return TRUE;
}
在default_start()函数中,通过do_alloc_buffer()中的pclass->alloc_buffer (pool, buffer, params)将会调用gst_v4l2_buffer_pool_alloc_buffer(),在这里,将会按照申请buf的类型,申请mmap buf或者是dmabuf保存到group->mem。
回到default_start(),alloc buffer之后,又将会通过pclass->release_buffer (pool, buffer)调用到gst_v4l2_buffer_pool_release_buffer()。在该函数中,将会通过gst_v4l2_buffer_pool_qbuf()将之前申请到的各个qbuf到设备驱动,具体是通过gst_v4l2_allocator_qbuf()调用VIDIOC_QBUF
完成的。好的,介绍完default_start(),回到调用default_start()的gst_v4l2_buffer_pool_start()。
接着上面,在gst_buffer_pool_set_active激活pool的时候,通过gst_v4l2_buffer_pool_start()完成pool的配置以及buf申请之后,又将在gst_v4l2_buffer_pool_start()函数中通过gst_v4l2_buffer_pool_streamon (pool)
开启pool的数据流。
在gst_v4l2_buffer_pool_streamon()函数中将会检查buf是否已经qbuf,然后将会调用VIDIOC_STREAMON
开启v4l2设备流传输。
至此,通过gst_buffer_pool_set_active()调用do_start()到pclass->start的gst_v4l2_buffer_pool_start()调用完成,buf已经申请,stream已经开始传输。回到gst_buffer_pool_set_active()函数,在完成以上操作之后,又将通过do_set_flushing (pool, FALSE)设置pool->flushing为0,这样就真正的开始传输了。
以上,介绍完gst_v4l2src_decide_allocation()流程。
通过以上两个小节,我们了解到了在gst_base_src_loop()中,当caps发生改变时的协商流程,其实在刚启动task时就会进行这样的一个协商流程。那么,在task中,又是如何处理数据的呢,下面继续来分析一下。
v4l2src捕获数据之gst_v4l2src_create()
在gst_base_src_loop()中,将会通过gst_base_src_get_range (src, position, blocksize, &buf)获取数据,而在该函数中,将会通过bclass->create (src, offset, length, &res_buf)的方式调用到gst_v4l2src_create(),下面来看看,它是怎么工作的。
static GstFlowReturn
gst_v4l2src_create (GstPushSrc * src, GstBuffer ** buf)
{
do {
/* 调用gst_base_src_default_alloc(),在这里最终是会调用到
* gst_v4l2_buffer_pool_acquire_buffer(),此时进入该函数,params为NULL,
* 所以最终将会通过gst_v4l2_buffer_pool_dqbuf()从设备驱动拿到填满图像数据的buf */
ret = GST_BASE_SRC_CLASS (parent_class)->alloc (GST_BASE_SRC (src), 0,
obj->info.size, buf);
if (G_UNLIKELY (ret != GST_FLOW_OK))
goto alloc_failed;
/* 在这里就是检查buf的大小是否正确,同时检查pool是否还有buf,
* 如果没有,将会不允许上层继续dqbuf,同时也会检查是否需要拷 */
ret = gst_v4l2_buffer_pool_process (pool, buf);
} while (ret == GST_V4L2_FLOW_CORRUPTED_BUFFER);
...
/* 在获得buf之后,都是进行一些buf数据长度、时间戳等的操作 */
}
经过gst_v4l2src_create()之后,得到了buf,在gst_base_src_loop()又将在检查时间戳、segment、事件等,最终通过gst_pad_push()函数将数据push到下游,至此,完成一次数据流动。
可能会好奇,buffer又是什么时候qbuf会底层驱动的,其实在申请buf的时候,就已经设置好释放函数,然后在buf引用计数为0的时候,将会调用释放函数_gst_buffer_dispose()。在该函数中,最终会调用gst_v4l2_buffer_pool_release_buffer()函数,将buf通过VIDIOC_QBUF
填充到设备驱动。
v4l2src总结
v4l2src是gstreamer基于v4l2开发的一个element,最终也都是通过ioctl操作v4l2设备,只不过是为了兼容更多的设备,所以做了很多的操作,但是万变不离其中,通过阅读代码,最终还是可以发现一切的。
简单的来说,v4l2src在一开始的时候,将会枚举设备支持的各种格式,然后在协商的过程将format、size、framerate等操作设置下去,然后在激活pool时,将会根据参数设置pool并申请buffer,而后在循坏中,会每次都检查caps有没有改变,改变就重新协商,每次取buf出来,然后设置时间戳等信息之后,push到下游element。
以上是个人理解,有理解错误的地方,欢迎指出,感谢。