gstreamer学习笔记---v4l2src

版权声明:本文为博主原创文章,未经允许,不得转载。 https://blog.csdn.net/weixin_41944449/article/details/81805164

  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()

  我们直入主题,看看该函数究竟都干了什么。

  1. 首先的,将会通过gst_base_src_send_stream_start()函数发送一个GST_EVENT_STREAM_START事件给下游element,告知上游将开始传输数据;
  2. 可能在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添加到设备驱动,然后开启流;
  3. 在调用gst_base_src_prepare_allocation()函数之后,v4l2设备初始化已经完成,需要设置的操作都已经进行了,接下来,回到gst_base_src_loop()函数;
  4. 上面已经进行了caps是否改变等操作,接下来,将会调用gst_base_src_get_range()函数获取buf数据,在该函数中,将会通过bclass->create (src, offset, length, &res_buf)函数调用到gst_v4l2src_create()函数获取数据;
  5. 获取到数据之后,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。


  以上是个人理解,有理解错误的地方,欢迎指出,感谢。

猜你喜欢

转载自blog.csdn.net/weixin_41944449/article/details/81805164