使用gstreamer播放视频,在gst-launch-1.0添加参数video-sink=“xxx”,即可指定显示的element,那么显示的element一般操作又是怎样的呢,它是如何知道它将要显示的数据格式、分辨率、帧率等参数呢,下面我们一起来学习一下。
为了减少平台硬件相关性,下面将通过fbdevsink了解videosink,而videosink又是继承与basesink的,所以也将会了解到basesink。先来看看fbdevsink的继承关系:
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstBaseSink
+----GstVideoSink
+----GstFBDEVSink
创建element的过程没什么好介绍的,就是加载相应的动态库,加载plugin,创建element,下面,我们将介绍videosink在各个状态切换的操作。
之前的文章有介绍过,element的link操作,一般的是从src到sink,而videosink是sink element,将会是最后一个element。在link的时候,都将会通过gst_pad_query_caps (pad, NULL)查询pad支持的caps,由于videosink都没有重载查询函数,所以将会调用到basesink的这里将会调用到gst_base_sink_sink_query(),最终调用到gst_base_sink_default_query (GstBaseSink * basesink, GstQuery * query)。由于是查询caps,将会调用gst_base_sink_query_caps(),在该函数中最终通过bclass->get_caps (bsink, filter)调用到fbdevsink的gst_fbdevsink_getcaps()获取template caps。当获取到caps,与上游element caps有交集,即可link成功。
之前就介绍过,element从NULL切换到READY,一般都是打开设备,初始化相应的硬件,同样的,在fbdevsink这里也是,fbdevsink、videosink在该状态都没有什么实际性的操作,将会在basesink的gst_base_sink_change_state()调用bclass->start (basesink)。这个调用将会调用到fbdevsink的gst_fbdevsink_start (GstBaseSink * bsink)。刚才已经说了,从NULL切换到READY,将会设置初始化设备,而在fbdevsink这里,将会打开/dev/fb0节点,通过ioctl获取机器framebuffer的信息,并将framebuffer映射保存到fbdevsink->framebuffer,start完成操作。
在该状态下,将会进行设备初始化、相应资源申请等操作,而显示的videosink,又将会进行什么操作呢,往下看。
由于我们是减少平台相关行而采用fbdevsink介绍videosink,在fbdevsink、videosink在这个状态切换都没有相应的操作,只是在basesink中进行一些element的成员变量初始化而已。需要注意的,basesink是异步的将状态切换到PAUSED,会发送async_start消息,只是bin接收到该消息在这里没有进一步操作而已。在该切换状态下,将会进行相应的pad激活操作,这个是在gstelement.c的gst_element_change_state_func()实现的。
在激活pad的过程中将会通过GST_PAD_ACTIVATEFUNC (pad)调用到gstbasesink.c的gst_base_sink_pad_activate()。由于basesink默认是不能激活为pull模式,所以在继承类没有修改的情况下,将会通过gst_pad_activate_mode (pad, GST_PAD_MODE_PUSH, TRUE)
激活为push模式。
static gboolean
gst_base_sink_pad_activate (GstPad * pad, GstObject * parent)
{
gboolean result = FALSE;
GstBaseSink *basesink;
GstQuery *query;
gboolean pull_mode;
basesink = GST_BASE_SINK (parent);
gst_base_sink_set_flushing (basesink, pad, FALSE);
/* we need to have the pull mode enabled */
if (!basesink->can_activate_pull) {
GST_DEBUG_OBJECT (basesink, "pull mode disabled");
goto fallback;
}
...
/* push mode fallback */
fallback:
GST_DEBUG_OBJECT (basesink, "Falling back to push mode");
if ((result = gst_pad_activate_mode (pad, GST_PAD_MODE_PUSH, TRUE))) {
GST_DEBUG_OBJECT (basesink, "Success activating push mode");
}
...
return result;
}
在gst_pad_activate_mode()中,最后又将会通过GST_PAD_ACTIVATEMODEFUNC (pad) (pad, parent, mode, active)调用到gstbasesink.c的gst_base_sink_pad_activate_mode(),模式为push,所以又将调用gst_base_sink_pad_activate_push(),而最终该函数也只是简单的填充basesink->pad_mode而已,状态切换完成。
前几篇文章也都介绍过,虽然已经link、caps有交集,但是,使用那个caps还是没有协商清楚的,所以接下来将会有多次的caps query,直至caps协商完成,在这个过程是多次调用到bclass->get_caps(),具体的操作可以看videosink继承类等的实现,在link的时候,也有进行这个查询,只是由于当时硬件设备还没有初始化,不知道设备具体支持的caps,所以,虽然多次查询,调用的是同一个函数,但是在该函数中,将会在不同的状态返回不同的信息,具体的得根据element的实现得知。
caps确定下来之后,将会接收到stream-start
事件,最终在gst_base_sink_default_event()处理该事件。在该函数中,并没有对该事件进行太多的处理,相应的信息也没有保存。但是,videosink,是sink,gstreamer中,部分事件是从头到尾贯穿pipeline的,而stream-start就是其中一个,所以,videosink接收到该事件,最终将会把该事件转换为message,发送到消息总线,这样消息总线的监听者将会接收并处理该消息。
虽然caps已经确定了,但是最后还有accept-caps
query,这里上游将会发送最后确定的caps到videosink,videosink检查自身是否支持该caps,支持将会在query中设置result为1。
确定下来之后,上游又将会发送caps事件到videosink。在gst_base_sink_default_event()中处理该事件,将会先从EVENT中解析得到caps,然后检查当前pad是否已经设置了caps,对比是否有改变,改变将会调用bclass->set_caps (basesink, caps)设置caps。这里将会调用到fbdevsink的gst_fbdevsink_setcaps()。在video显示中,大概应该会了解显示主要关注的是什么,主要也是这么几个,数据格式、分辨率、帧率、显示位置与大小等信息。明白这些,大概就知道videosink的set_caps将会设置什么参数。在fbdevsink的set_caps()中,将会从caps中获取相应的格式、分辨率、帧率这些信息,然后设置相应的成员变量。我想videosink型的element应该都是类似的吧。这样,caps EVENT处理完毕。
在设置caps之后,部分element需要设置buffer缓冲区的,当然有些element也会不设置,这个得具体element分析。但是,上游会发送GST_QUERY_ALLOCATION查询缓冲区的属性。在basesink这里将会通过gst_base_sink_default_query()调用bclass->propose_allocation (basesink, query)查询element的buffer缓冲区属性,协商buffer pool等。这个propose_allocation()函数主要是从query中获取caps信息,同时获取是否需要创建pool,设置buffer pool参数,创建pool等。而fbdevsink不需要进行该操作,上游将自己申请buffer pool。
最后,应该就是GST_EVENT_SEGMENT事件了。音视频数据在gstreamer中都是通过segment描述其时间范围,所以,在都准备好的时候,播放之前,将会发送segment事件,该事件中带有描述接下来播放数据的时间范围,element接收到数据的时候,将会检查该帧数据的时间戳是否在segment内,一般情况下是需要在这个范围内才会正常使用。在gst_base_sink_default_event()处理,将会从EVENT提取segment信息保存在basesink->segment,将会在接收到数据的时候使用待segment信息。
有时候我们播放歌曲获取视频,都会有一些附带信息,比如歌手是谁、歌名叫什么、视频格式又是什么、波特率、language-code等信息,这些信息,都会多次的通过GST_EVENT_TAG通过pipeline贯穿管道的element。但是basesink只是简单的将tag EVENT转换为message并发送到消息总线。
在切换到PLAYING状态之前,也会有一帧数据先贯穿pipeline,所以下面我们来分析一下,当buffer数据达到的时候,又将会进行什么操作。
数据预滚达到basesink类element的时候,最先将会是调用gst_base_sink_chain(),最终将调用gst_base_sink_chain_unlocked (basesink, pad, buf, FALSE)。视频播放,播放器会按照每帧视频的时间戳排序,播放各帧视频,所以,gst_base_sink_chain_unlocked()的主要操作概括如下:
在pipeline接收到PAUSED消息之后,又将会设置element进入PLAYING状态,那么。videosink在该状态又将会进行什么操作呢,接着看。
切换江湖调用到gst_base_sink_change_state(),由于我们之前使能了预滚,所以将会把预滚标志位置为FALSE,同时发送preroll信号,这样,上面的预滚显示将会接收到该信号,接着运行gst_base_sink_chain_unlocked()。剩余的操作就是设置clock以及时钟运行了,这些就不介绍了。下面我们看看,在PLAYING状态下,数据的处理过程。
回到数据处理函数gst_base_sink_chain_unlocked(),接着上面介绍的跟踪下去:
下面我们先来看看,gst_base_sink_do_sync()是如何同步的?代码注释已经讲清楚了。
static GstFlowReturn
gst_base_sink_do_sync (GstBaseSink * basesink,
GstMiniObject * obj, gboolean * late, gboolean * step_end)
{
GstClockTime rstart, rstop, rnext, sstart, sstop, stime;
/* 通过该函数获取buf的pts、duration等信息,知道该帧该什么时候显示,显示多长时间 */
syncable = gst_base_sink_get_sync_times (basesink, obj,
&sstart, &sstop, &rstart, &rstop, &rnext, &do_sync, &stepped, current,
step_end);
/* 得到开始显示时间之后,还需要考虑basesink->priv->latency
* 以及basesink->priv->render_delay时间 */
stime = gst_base_sink_adjust_time (basesink, rstart);
/* 完了之后,stime就是显示时间了,然后通过该函数,
* 确定当前时间就是显示时间,不是将会等待 */
status = gst_base_sink_wait_clock (basesink, stime, &jitter);
/* 保存该帧的时间差,在qos使用到 */
priv->current_jitter = jitter;
}
同步完成之后,将会通过bclass->render()调用具体的子类渲染函数。videosink都将会调用到gstvideosink.c的gst_video_sink_show_frame(),而在该函数将会调用videosink子类的klass->show_frame(),在fbdevsink将会调用到gst_fbdevsink_show_frame()。fbdevsink的show_frame()就是从buffer拷贝数据到之前映射的framebuffer,即完成渲染操作。
gstreamer是通过qos事件说明当前播放速度,所以在播放完一帧之后,都会反馈当先的播放情况到上游element,在videosink这里,是通过gstbasesink.c的gst_base_sink_perform_qos()完成这个操作的。在该函数中,将会根据刚才显示的这帧数据时间戳,得到本身时间戳是多少,真正显示时间又是多少,在该element处理,又花了多少时间,同时还会将之前各帧的处理时间去平均值保存在priv->avg_pt,最终根据处理时间以及到达该element的时间延迟计算一个比例priv->avg_rate,最终将这个比例以及显示时间、时间差距等信息通过QOS EVENT发送到上游。上游element接收到该事件,将会保存相应的信息,这些信息将会在下一次它自身处理数据的时候,影响相应的时间戳等,这样达到一个反馈。
至此,videosink介绍完成。
videosink和之前介绍的element主体流程也是非常类似的,只是由于videosink是pipeline的最后一个,很多时候需要转发message到消息总线上。同时,videosink主要是用于显示视频的,所以还会有一个时间戳的问题,需要等到相应的时间再进行render。
以上是个人理解,有理解错误的地方,欢迎指出,感谢