gstreamer学习笔记---编码videoencoder

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

  既上一节的《gstreamer学习笔记—v4l2src》之后,我们这一次,学习gstreamer的编码流程。稍微了解gstreamer的小伙伴都知道,gstreamer具备强大的音视频处理功能,相信很多小伙伴也都会使用gstreamer播放或者录制视频等操作,但是了解它的编码框架的,可能又会少一些,在写这篇文章之前,我也不了解,那么,接下来就让我们一起学习gstreamer的编码框架。
  这一次,为了避免平台性的影响,我们使用软件编码插件jpegenc,将v4l2src输出的图像数据编码为JPEG,详细命令如下,通过这样的一条通路来了解gstreamer的编码框架,文章中,具体的函数调用流程不再详细,不懂的可以去看之前的pad分析文章。

gst-launch-1.0 v4l2src ! jpegenc ! image/jpeg ,width=1280,height=720 ! multifilesink location="/tmp/frame%d.jpeg" max-files=1

  而jpegenc的继承关系如下,我们今天的重点是videoEncoder,而jpegenc是继承与它的,看看它是怎么通过videoEncoder完成编码操作。下面,让我们开始编码流程学习吧。

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstVideoEncoder
                         +----GstJpegEnc


  接下来,我们将会通过jpegenc这个element了解gstreamer编码框架,在这之前,我们先大概了解jpegenc的功能。其实它就是通过libjpeg库将raw数据编码为JPEG数据输出,需要使用libjpeg库进行编码,需要创建一个struct jpeg_compress_struct类型的JPEG对象cinfo,用于错误处理的struct jpeg_error_mgr成员jerr以及负责编码的struct jpeg_destination_mgr的jdest。它们三个将是libjpeg的代表。
  jpegenc创建的时候,又进行了什么操作呢?在向gobject系统注册类的时候,将会设置pad template、属性等。而在对象实例初始化的时候,将会初始化libjpeg、绑定用于错误处理的jerr,初始化cinfo结构以及jdest等,至此,实例初始化完毕。
  那么,实例创建完成之后,编码模块又是如何进行协商、内存分配的呢。先别急,同样的,在进行这些操作的之前,先进行element link。
  在link的时候,将会查询pad支持的caps,这个,将会调用到gst_video_encoder_sink_query()。在该函数中,又将会通过encoder_class->sink_query()函数查询pad支持的caps。一般的,子类不会重载该函数,所以将会调用到gst_video_encoder_sink_query_default(),由于我们是查询caps,最终的调用是gst_video_encoder_sink_getcaps()。在gst_video_encoder_sink_getcaps()中,将会检查klass->getcaps()是否赋值,如果有,将会通过赋值函数来获取编码器支持的caps,如果没有,那么,则是通过gst_video_encoder_proxy_getcaps()获取caps。所以,在我们的实际编程中,继承GstVideoEncoder的子类,可以通过重载klass->getcaps()完成pad caps的查询,下面,我们将简单介绍一下gst_video_encoder_proxy_getcaps()流程。
  gst_video_encoder_proxy_getcaps()将会查询下游element的caps,然后再与自身的srcpad caps匹配,得到自身需要的输出交集,再通过__gst_video_element_proxy_caps()将下游element返回的caps按照sinkpad templ caps拷贝相应的视频信息而产生新的filter_caps,最终将通过该filter_caps与sinkpad templ caps取交集而返回查询。
  回到element link,查询之后,jpegenc将会上游element link,caps有交集则link成功。
  到这里,编码element已经与上游element成功link,接下来将是与下游的element link。同样的,srcpad与下游element link时也将会进行查询操作,先调用pad的src_query函数,但是子类并没有重载该函数,所以将会调用到gst_video_encoder_sink_query_default()。在这里,将会通过proxy caps获取src template pad caps。得到caps之后,再与下游element link。
  此时,pipeline已经link,接下来将是element状态切换,从下游element到上游,看看编码又是如何进行的。

NULL—>READY

  每个element的状态切换都不尽一样,在gstreamer中,一般切换到READY状态都将会进行一些硬件设备的操作,所以,在真正介绍该类时,我们先看看GstVideoEncoderClass的接口,详细如下:

struct _GstVideoEncoderClass
{
  /*< private >*/
  GstElementClass  element_class;

  /*< public >*/
  /* virtual methods for subclasses */
  gboolean      (*open)         (GstVideoEncoder *encoder);
  gboolean      (*close)        (GstVideoEncoder *encoder);
  gboolean      (*start)        (GstVideoEncoder *encoder);
  gboolean      (*stop)         (GstVideoEncoder *encoder);
  gboolean      (*set_format)   (GstVideoEncoder *encoder,
                 GstVideoCodecState *state);
  GstFlowReturn (*handle_frame) (GstVideoEncoder *encoder,
                 GstVideoCodecFrame *frame);
  gboolean      (*reset)        (GstVideoEncoder *encoder,
                 gboolean hard);
  GstFlowReturn (*finish)       (GstVideoEncoder *encoder);
  GstFlowReturn (*pre_push)     (GstVideoEncoder *encoder,
                 GstVideoCodecFrame *frame);
  GstCaps *     (*getcaps)      (GstVideoEncoder *enc,
                                 GstCaps *filter);
  gboolean      (*sink_event)   (GstVideoEncoder *encoder,
                 GstEvent *event);
  gboolean      (*src_event)    (GstVideoEncoder *encoder,
                 GstEvent *event);
  gboolean      (*negotiate)    (GstVideoEncoder *encoder);
  gboolean      (*decide_allocation)  (GstVideoEncoder *encoder, GstQuery *query);
  gboolean      (*propose_allocation) (GstVideoEncoder * encoder,
                                       GstQuery * query);
  gboolean      (*flush)              (GstVideoEncoder *encoder);
  gboolean      (*sink_query)     (GstVideoEncoder *encoder,
                   GstQuery *query);
  gboolean      (*src_query)      (GstVideoEncoder *encoder,
                   GstQuery *query);
  gboolean      (*transform_meta) (GstVideoEncoder *encoder,
                                   GstVideoCodecFrame *frame,
                                   GstMeta * meta);

  /*< private >*/
  gpointer       _gst_reserved[GST_PADDING_LARGE-4];
};

  在这当中设置了很多函数指针操作编码器,但是,并不是所有的函数都需要实现,handle_frame函数是必须要实现的,因为它将是负责RAW数据处理的函数,同时,set_format和getcaps可根据实际情况实现。所以,在切换为READY时,编码子类很少有什么操作,大部分的都将是通过实现GstVideoEncoderClass封装的函数实现,然后再切换的时候,基类GstVideoEncoderClass再调用重载之后的函数。在切换为READY状态时,将会调用open函数,该函数将会打开编码器设备,可根据实际情况填充该函数,而jpegenc在该函数并没有什么操作,状态切换完成。

READY—>PAUSED

  在该状态切换过程中,在子类也并没有完成太多实质操作,而是通过基类GstVideoEncoderClass的change_state函数调用gst_video_encoder_reset()以及encoder_class->start()。gst_video_encoder_reset()将结构体中的变量复位,而start()则是调用子类重载的函数。在start函数中,将会初始化设备或者软件编码库,jpegenc则是初始化GstJpegEnc结构体。简单的,切换完成。

PAUSED—>PLAYING

  到这里,已经是最后一步了,接下来会进行什么操作呢。小伙伴还记不记得之前文章说过的,其实在element的PAUSED状态和PLAYING状态并不相差什么,应该就是clock运转、数据流动吧,所以,该状态切换并没有太多可讲的,但是有没有发现,编码器还有很多设置没有进行呢,那么,这些操作又将会是如何进行的呢,下面我们一起来了解。
  一般的,当上游决定了它使用的caps之后,将会发生caps EVENT到下游,当videoencoder接收到EVENT又会有什么操作呢,在class_init()中我们就设置了gst_video_encoder_sink_event_default()负责处理上游发送的EVENT。当videoencoder接收到caps EVENT,在解析相应信息之后,将会调用gst_video_encoder_setcaps()设置caps,主要流程如下:


static gboolean
gst_video_encoder_setcaps (GstVideoEncoder * encoder, GstCaps * caps)
{
  GstVideoEncoderClass *encoder_class;
  GstVideoCodecState *state;
  gboolean ret;

  encoder_class = GST_VIDEO_ENCODER_GET_CLASS (encoder);

  /* 将会检查set_format函数有没有设置,如果没有,将会直接返回FALSE,所以需要子类需要完成该函数 */
  g_return_val_if_fail (encoder_class->set_format != NULL, FALSE);

  /* 检查编码器的status与将要设置的caps信息是否一致,一致则不用更改,直接返回 */
  if (encoder->priv->input_state) {
    GST_DEBUG_OBJECT (encoder,
        "Checking if caps changed old %" GST_PTR_FORMAT " new %" GST_PTR_FORMAT,
        encoder->priv->input_state->caps, caps);
    if (gst_caps_is_equal (encoder->priv->input_state->caps, caps))
      goto caps_not_changed;
  }

  state = _new_input_state (caps);

  ...

  /* 将通过set_format()把caps信息设置到编码器,所以子类需要实现改函数 */
  ret = encoder_class->set_format (encoder, state);
  if (ret) {
    if (encoder->priv->input_state)
      gst_video_codec_state_unref (encoder->priv->input_state);
    encoder->priv->input_state = state;
  } else {
    gst_video_codec_state_unref (state);
  }

  return ret;
  ...

  从该函数可以看到,我们需要实现set_format()函数,配置编码器格式。在gst codec中,将会通过GstVideoCodecState类型的status变量描述stream的相应信息,这个结构体在编码的整个过程中,都发挥重要的作用,保存着caps相应信息。
  而jpegenc则是调用gst_jpegenc_set_format()完成编码器设置。在该函数中,将会根据传进来的status,解析信息并将调用libjpeg相应函数进行设置。

编码数据处理

  此时,编码器的设置基本完成,接下来的将是处理数据。但其实,在处理数据之前,其实还有一个segment EVENT。该EVENT将会标明接下来发送数据的时间段,需要在该时间段的才是有效的,才会进行处理。因为在gstreamer中,视频流是通过segment 来描述的,无论是编解码还是显示,都会有这样的一个segment 信息。

                complete stream
+------------------------------------------------+
0                                              duration
       segment
   |--------------------------|
 start                       stop

  duration表示的是总的时间,而duration又会分成多个segment,所以,在进行处理的时候,都将会进行数据时间戳是否在segmen的检查。
  当上游push数据到编码器的时候,将会先调用gst_video_encoder_chain()。在该函数中,将会先检查status是否正确,而后检查buffer的时间戳是否在当前的segment内,同时将会通过gst_video_encoder_new_frame()根据传进来的buffer创建frame,同时还会检查是否需要编码为关键帧,最后,将会调用子类的handle_frame()处理RAW数据。
  显然的,handle_frame()将会是对帧数据进行编码处理,自然而然的,每个编码器的该函数不尽相同,但是,都将是填充RAW数据、时间戳等信息到编码器。接下来看看jpegenc是如何完成这个操作的。
  在gst_jpegenc_handle_frame()函数,将会先通过gst_video_frame_map()将frame映射到编码器结构体成员,将会是按照像素分别的取数据,接着,jpegenc将会申请内存保存编码后的数据空间,最后通过jpeg_write_raw_data()将RAW数据填充到编码器,至此,handle_frame()操作完成。
  但实际上,在编码器中,一般的都将会实现类似回调函数的接口,将编码后的数据传送到应用层,这样让应用层拿到编码后的数据。在jpegenc中,有一个jdest的结构体负责这部分操作,在init的时候将会对其赋值,接收到数据,它将会负责编码操作,编码完成之后,将会调用gst_video_encoder_finish_frame()函数告知gstreamer编码完成,同时传进了编码后的数据。其实可以发现,每个编码器在编码完成之后都将会调用gst_video_encoder_finish_frame(),以将数据push到下游element。
  那么gst_video_encoder_finish_frame()又是完成什么操作呢,我们往下看。

GstFlowReturn
gst_video_encoder_finish_frame (GstVideoEncoder * encoder,
    GstVideoCodecFrame * frame)
{
  ...
  /* 将会检查是否需要重新协商,如果需要,将会与下游element重新协商 */
  needs_reconfigure = gst_pad_check_reconfigure (encoder->srcpad);
  if (G_UNLIKELY (priv->output_state_changed || (priv->output_state
              && needs_reconfigure))) {
    ...
  }


  /* 将会检查在编码的时候是否接收到EVENT,但是并没有将EVENT往下游push的,
   * 例如之前说到的segment EVENT,此时将会把之前保存的EVENT push到下游 */
  for (l = priv->frames; l; l = l->next) {
    GstVideoCodecFrame *tmp = l->data;

      for (k = g_list_last (tmp->events); k; k = k->prev)
        gst_video_encoder_push_event (encoder, k->data);
      g_list_free (tmp->events);
      tmp->events = NULL;
    }
  }
    ...
    /* 将会处理关键帧相关信息 */
    if (fevt) {
      ...
    }

  /* 设置时间戳等信息 */
  GST_BUFFER_PTS (frame->output_buffer) = frame->pts;
  GST_BUFFER_DTS (frame->output_buffer) = frame->dts;
  GST_BUFFER_DURATION (frame->output_buffer) = frame->duration;

  /* 处理编码的头部信息等,封装需要用到部分信息 */
  if (G_UNLIKELY (send_headers || priv->new_headers)) {
    ...
  }

  ...
  /* push数据到下游 */
  if (ret == GST_FLOW_OK)
    ret = gst_pad_push (encoder->srcpad, buffer);
  ...
}

  为什么会有部分EVENT需要push呢?因为在sinkpad的EVENT处理函数中,将会根据EVENT差异,有一些需要传递到下游element的,在这个时候将会保存起来在push数据的时候再push EVENT。
  我们再回头看videoencoder的协商函数gst_video_encoder_negotiate_unlocked()。其实就是调用gst_video_encoder_negotiate_default(),在该函数中,将会先根据output_state中的信息填充caps,接下来也将会push之前接收到的EVENT到下游,比较caps是否发生改变。接下来,将会通过PEER pad查询下游element支持的caps,之后就是调用decide_allocation()函数解析查询得到的信息得到allocator以及相应参数,协商完成。这些参数,将是在编码器中,申请output buffer或者frame时会用到(在申请buffer的时候,也都会进行协商的,因为申请buffer也需要用到这些参数)。
  至此,videoencoder关键流程分析完成。

总结

  GstVideoEncoder是所有编码子类的基类,它将封装了各种接口函数,子类继承它之后,只需要完成相应的接口函数,即可完成编码操作,因为重要的调用机制以及相应的状态转换,GstVideoEncoder都将完成。这篇文章是通过jpegenc简单的了解gstreamer的编码框架,按照以前介绍的类重载以及今天介绍的主体流程,分析其他的编码类,也就都差不多了。


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

猜你喜欢

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