既上一节的《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
编码实例创建、link
接下来,我们将会通过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的编码框架,按照以前介绍的类重载以及今天介绍的主体流程,分析其他的编码类,也就都差不多了。
以上是个人理解,有理解错误的地方,欢迎指出,感谢。