GStreamer官方教程系列
Pipeline manipulation
本节展示了诸多可以用来操纵你的应用中的管道(pipeline)的方法。主要有以下主题:
- 怎么将一个应用中的数据插入到管道中。
- 怎么从一个管道中读取数据。
- 怎么控制管道的速度、长度以及起始点。
- 怎么监听一个管道的数据处理过程。
本节部分内容十分底层,你需要一些编程经验并且对GStreamer有较好的理解能力来阅读这些内容。
使用探针(probe)
*最好将探测想象为有一个pad监听器。从技术上来讲,一个探针就是一个回调,可以用gst_pad_add_probe()来将回调插入一个pad中。相反,你可以使用gst_pad_remove()来移除回调。当添加了回调,探针会提醒你pad发生的各种活动。你可以在添加探针的时候定义你感兴趣活动的消息。
探针种类:
- 推出或拉取缓冲区。可以在注册探针时指定为GST_PAD_PROBE_TYPE_BUFFER。因为pad可以被多种方式安排。也可以用GST_PAD_PROBE_TYPE_PUSH和GST_PAD_PROBE_TYPE_PULL选项来制定你需要的安排方式。你可以使用这个探针来检查、修改或删除缓冲区。
- 推缓冲区列表。在注册探针时使用GST_PAD_PROBE_TYPE_BUFFER_LIST。
- 一个事件(event)经过pad。使用GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM或GST_PAD_PROBE_TYPE_EVENT_UPSTREAM标志(flag)来选择顺流或逆流事件。亦或使用GST_PAD_PROBE_TYPE_EVENT_BOTH来得知双向的事件消息。默认的,刷新时间不会通知。但也可以通过显式地加入GST_PAD_PROBE_TYPE_EVENT_FLUSH来获取刷新事件的回调。事件仅通过推送模式来通知。你可以使用这种探针来检查、修改或删除事件。
- 经过pad的请求。使用GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM或GST_PAD_PROBE_TYPE_QUERY_UPSTREAM标志(flag)来选择顺流或逆流请求。可以使用GST_PAD_PROBE_TYPE_QUERY_BOTH来方便地获取双向的请求。请求探针会提醒两次:当请求顺流/逆流经过和请求结果返回。你可以分别使用GST_PAD_PROBE_TYPE_PUSH或GST_PAD_PROBE_TYPE_PULL来选择在什么情况下调用回调。你可以使用请求探针检查或修改请求,甚至在探针的回调中回复请求。要回复请求,你可以将结果填入请求中并从回调中返回GST_PAD_PROBE_DROP。
- 除了提醒你数据流,你也可以在回调返回的时候令探针阻塞数据流。这叫做阻塞探针,可以指定GST_PAD_PROBE_TYPE_BLOCK标志来激活。你可以同时使用其他标志来选定某种活动发生时阻塞数据流。移除探针或回调返回GST_PAD_PROBE_REMOVE时,阻塞的pad会重新疏通。你可以通过令回调返回GST_PAD_PROBE_PASS仅通过现在阻塞的项目,这会在下一个项目上再次阻塞。阻塞探针用于临时阻塞你将要解除链接或没有链接的探针。如果数据流没有阻塞,管道会由于数据推入一个没有链接的帕的而进入错误状态。我们会探究如何使用阻塞探针来部分地预滚管道。
- 在pad中没有活动发生时提醒。使用GST_PAD_PROBE_TYPE_IDLE标志注册探针。你可以指定GST_PAD_PROBE_TYPE_PUSH和/或GST_PAD_PROBE_TYPE_PULL来选择pad处于哪种安排模式下提醒。这种探针(IDLE probe)也是一种阻塞探针,只要安装了这种探针,它就不会允许数据经过pad。你可以使用闲置探针来动态重链接一个pad。我们会探究如何使用闲置探针来替换管道中的某一个组件。
数据探针
数据探针会在数据经过pad时提示你。将GST_PAD_PROBE_TYPE_BUFFER和/或
GST_PAD_PROBE_TYPE_BUFFER_LIST传递给gst_pad_add_probe()来创建这种探针。大多数寻常的缓冲操作组件在_chain()函数中能够做到的,同样可以在探针回调中做到。
数据探针运行在管道流线程上下文中,因此回调应该尽量避免阻塞和做一些奇怪的操作。这么做的话可能会对管道的性能有负面影响,万一有bug,会导致死锁或崩溃。更确切地说,在探针回调中应该尽量避免调用GUI相关函数,不要尝试改变管道状态。一个应用可以在管道总线中发送自定义消息来与应用主线程通信并使主线程执行诸如停止管道之类的操作。
下面是一个使用数据探针的例子。比较这个程序与gst-launch-1.0 videotestsrc ! xvimagesink的输出,如果你不清楚程序的目的:
#include <gst/gst.h>
static GstPadProbeReturn
cb_have_data (GstPad *pad,
GstPadProbeInfo *info,
gpointer user_data)
{
gint x, y;
GstMapInfo map;
guint16 *ptr, t;
GstBuffer *buffer;
buffer = GST_PAD_PROBE_INFO_BUFFER (info);
buffer = gst_buffer_make_writable (buffer);
/* Making a buffer writable can fail (for example if it
* cannot be copied and is used more than once)
*/
if (buffer == NULL)
return GST_PAD_PROBE_OK;
/* Mapping a buffer can fail (non-writable) */
if (gst_buffer_map (buffer, &map, GST_MAP_WRITE)) {
ptr = (guint16 *) map.data;
/* invert data */
for (y = 0; y < 288; y++) {
for (x = 0; x < 384 / 2; x++) {
t = ptr[384 - 1 - x];
ptr[384 - 1 - x] = ptr[x];
ptr[x] = t;
}
ptr += 384;
}
gst_buffer_unmap (buffer, &map);
}
GST_PAD_PROBE_INFO_DATA (info) = buffer;
return GST_PAD_PROBE_OK;
}
gint
main (gint argc,
gchar *argv[])
{
GMainLoop *loop;
GstElement *pipeline, *src, *sink, *filter, *csp;
GstCaps *filtercaps;
GstPad *pad;
/* init GStreamer */
gst_init (&argc, &argv);
loop = g_main_loop_new (NULL, FALSE);
/* build */
pipeline = gst_pipeline_new ("my-pipeline");
src = gst_element_factory_make ("videotestsrc", "src");
if (src == NULL)
g_error ("Could not create 'videotestsrc' element");
filter = gst_element_factory_make ("capsfilter", "filter");
g_assert (filter != NULL); /* should always exist */
csp = gst_element_factory_make ("videoconvert", "csp");
if (csp == NULL)
g_error ("Could not create 'videoconvert' element");
sink = gst_element_factory_make ("xvimagesink", "sink");
if (sink == NULL) {
sink = gst_element_factory_make ("ximagesink", "sink");
if (sink == NULL)
g_error ("Could not create neither 'xvimagesink' nor 'ximagesink' element");
}
gst_bin_add_many (GST_BIN (pipeline), src, filter, csp, sink, NULL);
gst_element_link_many (src, filter, csp, sink, NULL);
filtercaps = gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, "RGB16",
"width", G_TYPE_INT, 384,
"height", G_TYPE_INT, 288,
"framerate", GST_TYPE_FRACTION, 25, 1,
NULL);
g_object_set (G_OBJECT (filter), "caps", filtercaps, NULL);
gst_caps_unref (filtercaps);
pad = gst_element_get_static_pad (src, "src");
gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BUFFER,
(GstPadProbeCallback) cb_have_data, NULL, NULL);
gst_object_unref (pad);
/* run */
gst_element_set_state (pipeline, GST_STATE_PLAYING);
/* wait until it's up and running or failed */
if (gst_element_get_state (pipeline, NULL, NULL, -1) == GST_STATE_CHANGE_FAILURE) {
g_error ("Failed to go into PLAYING state");
}
g_print ("Running ...\n");
g_main_loop_run (loop);
/* exit */
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);
return 0;
}
严格来说一个pad探针回调仅允许修改缓冲内容,如果缓冲允许写入的话。情况是否如此很大程度上取决于管道及其中的组件。相当多的时候,是这种情况,然而有些时候又不是,并且,如果不是的话那么意外的数据或元数据修改会导致出现很难调试和追踪的bug。你可以通过gst_buffer_is_writable()函数来确认缓冲区是否允许写入。由于你可以传递回一个不同于传入的缓冲区,最好在回调中使用gst_buffer_make_writable()函数来使缓冲区可写。
Pad探针非常适合查看通过管道的数据。如果你需要修改数据,你应该编写你自己的GStreamer组件。诸如GstAudioFilter,GstVideoFilter或GstBaseTransform的基类使得这相当容易。
如果你只是想当缓冲经过管道时检查它,你甚至不需要创建一个pad探针。你可以向管道插入一个验证组件并连接到“handoff”信号。验证组件提供了一些有用的调试工具,比如dump和last-message属性,后者可以通过向gst-launch添加-v来激活,并将验证的silent属性设置为FALSE。
播放媒体文件的某一段
在本案例中我们会展示如何播放媒体文件中的某一段。目标是仅播放2-5s的部分然后退出。
第一步中,我们将uridecodebin组件设置为PAUSED的状态并确保阻塞了所有已经创建了的源pad。当所有源pad被阻塞,他们都有了数据,我们称之为预滚。
完成预滚的管道中,我们可以获取媒体时长并且执行跳转。通过在管道中执行跳转来选择2-5s部分。
在设置好我们想要的部分后,我们可以链接槽组件,打开阻塞的源pad并设置管道为PLAYING状态。你会在结束前看到设置的部分在槽中播放。
#include <gst/gst.h>
static GMainLoop *loop;
static gint counter;
static GstBus *bus;
static gboolean prerolled = FALSE;
static GstPad *sinkpad;
static void
dec_counter (GstElement * pipeline)
{
if (prerolled)
return;
if (g_atomic_int_dec_and_test (&counter)) {
/* all probes blocked and no-more-pads signaled, post
* message on the bus. */
prerolled = TRUE;
gst_bus_post (bus, gst_message_new_application (
GST_OBJECT_CAST (pipeline),
gst_structure_new_empty ("ExPrerolled")));
}
}
/* called when a source pad of uridecodebin is blocked */
static GstPadProbeReturn
cb_blocked (GstPad *pad,
GstPadProbeInfo *info,
gpointer user_data)
{
GstElement *pipeline = GST_ELEMENT (user_data);
if (prerolled)
return GST_PAD_PROBE_REMOVE;
dec_counter (pipeline);
return GST_PAD_PROBE_OK;
}
/* called when uridecodebin has a new pad */
static void
cb_pad_added (GstElement *element,
GstPad *pad,
gpointer user_data)
{
GstElement *pipeline = GST_ELEMENT (user_data);
if (prerolled)
return;
g_atomic_int_inc (&counter);
gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM,
(GstPadProbeCallback) cb_blocked, pipeline, NULL);
/* try to link to the video pad */
gst_pad_link (pad, sinkpad);
}
/* called when uridecodebin has created all pads */
static void
cb_no_more_pads (GstElement *element,
gpointer user_data)
{
GstElement *pipeline = GST_ELEMENT (user_data);
if (prerolled)
return;
dec_counter (pipeline);
}
/* called when a new message is posted on the bus */
static void
cb_message (GstBus *bus,
GstMessage *message,
gpointer user_data)
{
GstElement *pipeline = GST_ELEMENT (user_data);
switch (GST_MESSAGE_TYPE (message)) {
case GST_MESSAGE_ERROR:
g_print ("we received an error!\n");
g_main_loop_quit (loop);
break;
case GST_MESSAGE_EOS:
g_print ("we reached EOS\n");
g_main_loop_quit (loop);
break;
case GST_MESSAGE_APPLICATION:
{
if (gst_message_has_name (message, "ExPrerolled")) {
/* it's our message */
g_print ("we are all prerolled, do seek\n");
gst_element_seek (pipeline,
1.0, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE,
GST_SEEK_TYPE_SET, 2 * GST_SECOND,
GST_SEEK_TYPE_SET, 5 * GST_SECOND);
gst_element_set_state (pipeline, GST_STATE_PLAYING);
}
break;
}
default:
break;
}
}
gint
main (gint argc,
gchar *argv[])
{
GstElement *pipeline, *src, *csp, *vs, *sink;
/* init GStreamer */
gst_init (&argc, &argv);
loop = g_main_loop_new (NULL, FALSE);
if (argc < 2) {
g_print ("usage: %s <uri>", argv[0]);
return -1;
}
/* build */
pipeline = gst_pipeline_new ("my-pipeline");
bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
gst_bus_add_signal_watch (bus);
g_signal_connect (bus, "message", (GCallback) cb_message,
pipeline);
src = gst_element_factory_make ("uridecodebin", "src");
if (src == NULL)
g_error ("Could not create 'uridecodebin' element");
g_object_set (src, "uri", argv[1], NULL);
csp = gst_element_factory_make ("videoconvert", "csp");
if (csp == NULL)
g_error ("Could not create 'videoconvert' element");
vs = gst_element_factory_make ("videoscale", "vs");
if (csp == NULL)
g_error ("Could not create 'videoscale' element");
sink = gst_element_factory_make ("autovideosink", "sink");
if (sink == NULL)
g_error ("Could not create 'autovideosink' element");
gst_bin_add_many (GST_BIN (pipeline), src, csp, vs, sink, NULL);
/* can't link src yet, it has no pads */
gst_element_link_many (csp, vs, sink, NULL);
sinkpad = gst_element_get_static_pad (csp, "sink");
/* for each pad block that is installed, we will increment
* the counter. for each pad block that is signaled, we
* decrement the counter. When the counter is 0 we post
* an app message to tell the app that all pads are
* blocked. Start with 1 that is decremented when no-more-pads
* is signaled to make sure that we only post the message
* after no-more-pads */
g_atomic_int_set (&counter, 1);
g_signal_connect (src, "pad-added",
(GCallback) cb_pad_added, pipeline);
g_signal_connect (src, "no-more-pads",
(GCallback) cb_no_more_pads, pipeline);
gst_element_set_state (pipeline, GST_STATE_PAUSED);
g_main_loop_run (loop);
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (sinkpad);
gst_object_unref (bus);
gst_object_unref (pipeline);
g_main_loop_unref (loop);
return 0;
}
注意我们使用了一个自定义的应用消息通知主线程uridecodebin已经预滚。主线程会发出一个刷新寻道。刷新会暂时解除pad的阻塞,并在新数据到达时再次阻塞pad。我们检测第二次阻塞并移除探针。然后我们将管道设置为PLAYING,管道将会播放2-5s的部分,应用会等待EOS消息然后退出。
手动向管道添加或移除数据
很多人想使用自己的源来向管道注入数据,还有的想在他们的应用中抓取管道的输出并处理。尽管强烈反对使用这些方法,但GStreamer提供了这些操作。注意!你需要指导自己在干什么。由于没有基类的支持,你需要彻底理解状态改变和同步。如果没用的话,你很可能搬起石头砸自己的脚。最好简单地编写一个插件并且令基类来管理它。参考Plugin Writer’s Guide来获取更多。另外,回顾下一节,其讲述了如何在你的应用中静态嵌入插件。
有两个组件——appsrc(假想的源)和appsink(假想的槽)你可以用来实现上面提到的目的。同样的方法将在这些组件上应用。我们将讨论如何使用这些组件在管道中插入(appsrc)或抓取(appsink)数据,以及如何协调。
appsrc和appsink都提供了两套API。一套使用标准GObject(action)信号和属性。同样的API也可以作为常规C API使用。C API性能更强但是要想使用这些组件需要链接到app库。
使用appsrc插入数据
我们来看appsrc如何将应用数据插入管道。
appsrc有一些配置选项控制它的操作方式。你应该决定以下几点:
- appsrc操作在推送模式还是拉取模式。stream-type属性是用来控制这方面的。random-access stream-type会使得appsrc启动拉取模式调度,同时其他stream-type激活推送模式。
- appsrc推出的缓冲区caps。这需要用caps属性来配置。这个属性必须设置为一个确定的caps,并且会用来协调顺流格式。
- appsrc是否操作在直播模式。这在is-live属性中设定。当操作在live-mode下,设置min-latency和max-latency属性同样是很重要的。min-latency应该被设置为获取一个缓冲区到缓冲区推送入appsrc的时间。在直播模式下,当缓冲的第一个比特被捕获而未提供给appsrc时,你需要用管道的running-time为缓冲添加时间戳。你可以让appsrc使用do-timestamp来添加时间戳,但是min-latency就必须设定为0,因为appsrc的时间戳基于获得一个给定缓冲时的所谓running-time。
- appsrc推出的SEGMENT事件的格式。这种格式对如何计算缓冲的running-time有影响,因此你必须保证你了解它。对于直播源你可能想将格式属性设置为GST_FORMAT_TIME。对于非直播源,格式取决于你正处理的媒体的种类。如果你打算为缓冲添加时间戳,你应该使用GST_FORMAT_TIME作为格式,如果你不使用,那么GST_FORMAT_BYTES可能也是合适的。
- 如果appsrc操作在random-access模式下,将size属性设置为流中的字节数是很重要的。这会使得顺流组件可以知晓媒体的大小并且在需要时寻找流的尾端。
处理送入appsrc的主要方法是gst_app_src_push_buffer()函数或者发送push-buffer动作信号。这会将缓冲区放入队列中,appsrc将在其流线程中读取队列。需要注意的是,数据传输不会发生在执行push-buffer调用的线程中。
max-bytes属性控制在appsrc判定队列满了之前队列能够允许多大数据进入。一个已经填满的队列总是会发出enough-data信号,这会提示应用应该停止将数据推入appsrc中。block属性会使得appsrc阻塞push-buffer方法直到空闲数据再次可用。
当内部队列数据用尽,会发送need-data信号,这提示应用应该开始推送更多数据进入appsrc。
除了need-data和enough-data信号,appsrc可以在stream-mode属性被设置为seekable或random-access时发送seek-data信号。该信号参数会包含新的需要的流中的位置,这个位置以format属性设置的单元集表示。在接收到seek-data信号后,应用会从新的位置开始推送缓存。
当最后的字节被推送入appsrc,你必须调用gst_app_src_end_of_stream()令其顺流发送EOS信号。
这些信号使得应用可以在推送或拉取模式亦或接下来介绍的模式中操作appsrc。
推送模式使用appsrc
当appsrc被设置为推送模式(stream-type是stream或seekable),应用会用新缓冲区反复调用push-buffer方法。appsrc中队列的大小可以通过enough-data和need-data信号来控制,其分别停止或启动push-buffer调用。min-percent属性的值决定了在need-data信号发送时appsrc中队列有多少数据。你可以将其设置为某个正数来防止队列彻底为空。
不要忘记在stream-type设置为GST_APP_STREAM_TYPE_SEEKABLE时使用seek-data回调。
当实施于各种网络协议与硬件设备时使用这种模式。
拉取模式使用appsrc
拉取模式下,need-data信号处理程序将数据送入appsrc。你需要准确地推入need-data信号所请求的字节数的数据量。只有当EOS时你才被允许推入更少的字节。
对文件访问或其他随即获取的源使用这种模式。
Appsrc案例
这个案例应用会通过使用appsrc作为源并用caps强制设置一种格式来生成黑色/白色(每秒切换)视频给一个Xv-window输出,我们使用色域转换组件来确保给X服务器提供正确的格式。我们设置一个帧率可变(0/1)的视频流并且为流出的缓冲区添加时间戳,这样我们每秒播放两帧。
注意如何使用推送模式下推送新缓冲区的方法为运行在拉取模式下的appsrc推送数据。
#include <gst/gst.h>
static GMainLoop *loop;
static void
cb_need_data (GstElement *appsrc,
guint unused_size,
gpointer user_data)
{
static gboolean white = FALSE;
static GstClockTime timestamp = 0;
GstBuffer *buffer;
guint size;
GstFlowReturn ret;
size = 385 * 288 * 2;
buffer = gst_buffer_new_allocate (NULL, size, NULL);
/* this makes the image black/white */
gst_buffer_memset (buffer, 0, white ? 0xff : 0x0, size);
white = !white;
GST_BUFFER_PTS (buffer) = timestamp;
GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale_int (1, GST_SECOND, 2);
timestamp += GST_BUFFER_DURATION (buffer);
g_signal_emit_by_name (appsrc, "push-buffer", buffer, &ret);
gst_buffer_unref (buffer);
if (ret != GST_FLOW_OK) {
/* something wrong, stop pushing */
g_main_loop_quit (loop);
}
}
gint
main (gint argc,
gchar *argv[])
{
GstElement *pipeline, *appsrc, *conv, *videosink;
/* init GStreamer */
gst_init (&argc, &argv);
loop = g_main_loop_new (NULL, FALSE);
/* setup pipeline */
pipeline = gst_pipeline_new ("pipeline");
appsrc = gst_element_factory_make ("appsrc", "source");
conv = gst_element_factory_make ("videoconvert", "conv");
videosink = gst_element_factory_make ("xvimagesink", "videosink");
/* setup */
g_object_set (G_OBJECT (appsrc), "caps",
gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, "RGB16",
"width", G_TYPE_INT, 384,
"height", G_TYPE_INT, 288,
"framerate", GST_TYPE_FRACTION, 0, 1,
NULL), NULL);
gst_bin_add_many (GST_BIN (pipeline), appsrc, conv, videosink, NULL);
gst_element_link_many (appsrc, conv, videosink, NULL);
/* setup appsrc */
g_object_set (G_OBJECT (appsrc),
"stream-type", 0,
"format", GST_FORMAT_TIME, NULL);
g_signal_connect (appsrc, "need-data", G_CALLBACK (cb_need_data), NULL);
/* play */
gst_element_set_state (pipeline, GST_STATE_PLAYING);
g_main_loop_run (loop);
/* clean up */
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (GST_OBJECT (pipeline));
g_main_loop_unref (loop);
return 0;
}
使用appsink抓取数据
不像appsrc,appsink更容易使用。它同样支持拉取和推送模式来从管道获取数据。
正常的从appsink中获取采样的方式是使用gst_app_sink_pull_sample()和gst_app_sink_pull_preroll()方法或者使用pull-sample和pull-preroll信号。这些方法阻塞直到槽中一个采样可用或者槽被关闭又或者到达了EOS。
appsink会在内部使用一个队列收集来自流线程中的缓冲。如果应用拉取采样的速度不够快,那么这个队列会随着时间大量消耗内存。max-buffers属性可以用来限制队列的大小。drop属性控制流线程是否阻塞或当队列已满旧缓冲是否删除。注意阻塞流线程会影响实时性能,应该避免这么做。
如果阻塞行为不是你想要的,设置emit-signals属性为TRUE会令appsink在不阻塞就能拉取一个采样时发送new-sample和new-preroll信号。
appsink的caps属性可以用来控制后面能够接受的格式。这个属性可以包含非固定的caps,拉取采样的格式可以通过采样的caps来获取。
如果pull-preroll或pull-sample方法其中之一返回NULL,appsink就是停止或者在EOS状态。你可以通过eos属性或者gst_app_sink_is_eos()方法来确认EOS状态。
eos信号可以在EOS状态时发出提示,防止轮询。
考虑在appsink以下属性中进行配置:
- sync属性,如果你想在传递给你采样之前让槽基类根据管道时钟同步缓冲区。
- 使用qos激活Quality-of-Service。如果你在处理原始视频帧并让基类根据时钟进行同步。让基类逆流发送QOS事件同样是个好主意。
- caps属性,包含接收的caps。上游的组件会转换格式以使其适配appsink上的配置好了的caps。你仍然必须确认GstSample以获取缓冲区实际的caps。
Appsink案例
以下案例展示如何使用appsink截取一个视频流的快照。
#include <gst/gst.h>
#ifdef HAVE_GTK
#include <gtk/gtk.h>
#endif
#include <stdlib.h>
#define CAPS "video/x-raw,format=RGB,width=160,pixel-aspect-ratio=1/1"
int
main (int argc, char *argv[])
{
GstElement *pipeline, *sink;
gint width, height;
GstSample *sample;
gchar *descr;
GError *error = NULL;
gint64 duration, position;
GstStateChangeReturn ret;
gboolean res;
GstMapInfo map;
gst_init (&argc, &argv);
if (argc != 2) {
g_print ("usage: %s <uri>\n Writes snapshot.png in the current directory\n",
argv[0]);
exit (-1);
}
/* create a new pipeline */
descr =
g_strdup_printf ("uridecodebin uri=%s ! videoconvert ! videoscale ! "
" appsink name=sink caps=\"" CAPS "\"", argv[1]);
pipeline = gst_parse_launch (descr, &error);
if (error != NULL) {
g_print ("could not construct pipeline: %s\n", error->message);
g_clear_error (&error);
exit (-1);
}
/* get sink */
sink = gst_bin_get_by_name (GST_BIN (pipeline), "sink");
/* set to PAUSED to make the first frame arrive in the sink */
ret = gst_element_set_state (pipeline, GST_STATE_PAUSED);
switch (ret) {
case GST_STATE_CHANGE_FAILURE:
g_print ("failed to play the file\n");
exit (-1);
case GST_STATE_CHANGE_NO_PREROLL:
/* for live sources, we need to set the pipeline to PLAYING before we can
* receive a buffer. We don't do that yet */
g_print ("live sources not supported yet\n");
exit (-1);
default:
break;
}
/* This can block for up to 5 seconds. If your machine is really overloaded,
* it might time out before the pipeline prerolled and we generate an error. A
* better way is to run a mainloop and catch errors there. */
ret = gst_element_get_state (pipeline, NULL, NULL, 5 * GST_SECOND);
if (ret == GST_STATE_CHANGE_FAILURE) {
g_print ("failed to play the file\n");
exit (-1);
}
/* get the duration */
gst_element_query_duration (pipeline, GST_FORMAT_TIME, &duration);
if (duration != -1)
/* we have a duration, seek to 5% */
position = duration * 5 / 100;
else
/* no duration, seek to 1 second, this could EOS */
position = 1 * GST_SECOND;
/* seek to the a position in the file. Most files have a black first frame so
* by seeking to somewhere else we have a bigger chance of getting something
* more interesting. An optimisation would be to detect black images and then
* seek a little more */
gst_element_seek_simple (pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_FLUSH, position);
/* get the preroll buffer from appsink, this block untils appsink really
* prerolls */
g_signal_emit_by_name (sink, "pull-preroll", &sample, NULL);
/* if we have a buffer now, convert it to a pixbuf. It's possible that we
* don't have a buffer because we went EOS right away or had an error. */
if (sample) {
GstBuffer *buffer;
GstCaps *caps;
GstStructure *s;
/* get the snapshot buffer format now. We set the caps on the appsink so
* that it can only be an rgb buffer. The only thing we have not specified
* on the caps is the height, which is dependant on the pixel-aspect-ratio
* of the source material */
caps = gst_sample_get_caps (sample);
if (!caps) {
g_print ("could not get snapshot format\n");
exit (-1);
}
s = gst_caps_get_structure (caps, 0);
/* we need to get the final caps on the buffer to get the size */
res = gst_structure_get_int (s, "width", &width);
res |= gst_structure_get_int (s, "height", &height);
if (!res) {
g_print ("could not get snapshot dimension\n");
exit (-1);
}
/* create pixmap from buffer and save, gstreamer video buffers have a stride
* that is rounded up to the nearest multiple of 4 */
buffer = gst_sample_get_buffer (sample);
/* Mapping a buffer can fail (non-readable) */
if (gst_buffer_map (buffer, &map, GST_MAP_READ)) {
#ifdef HAVE_GTK
pixbuf = gdk_pixbuf_new_from_data (map.data,
GDK_COLORSPACE_RGB, FALSE, 8, width, height,
GST_ROUND_UP_4 (width * 3), NULL, NULL);
/* save the pixbuf */
gdk_pixbuf_save (pixbuf, "snapshot.png", "png", &error, NULL);
#endif
gst_buffer_unmap (buffer, &map);
}
gst_sample_unref (sample);
} else {
g_print ("could not make snapshot\n");
}
/* cleanup and exit */
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (sink);
gst_object_unref (pipeline);
exit (0);
}
强制设定格式
有时你会想要设置一个特定的格式。你可以使用capsfilter组件来做到。
假如说你想要一个特定的视频画面大小和颜色格式或者音频bitsize和通道数量;你可以使用filtered caps来强制设定管道上一个特定的GstCaps。通过将capsfilter放置在两个组件之间并在其caps属性中指定你想要的GstCaps来在链接中设置filtered caps。capsfilter仅允许和这些功能兼容的种类们协调。
改变运行中管道的格式
在运行状态中也是允许动态地改变管道中的格式。这可以简单地通过改变capsfilter的caps属性来做到。capsfilter会向上游发送RECONFIGURE事件,这会使得上游组件尝试重新协调新的格式和分配器。这只在上游组件的源pad没有使用固定的caps时才有用。
下面的案例展示了如何在PLAYING状态下改变管道中的caps。
#include <stdlib.h>
#include <gst/gst.h>
#define MAX_ROUND 100
int
main (int argc, char **argv)
{
GstElement *pipe, *filter;
GstCaps *caps;
gint width, height;
gint xdir, ydir;
gint round;
GstMessage *message;
gst_init (&argc, &argv);
pipe = gst_parse_launch_full ("videotestsrc ! capsfilter name=filter ! "
"ximagesink", NULL, GST_PARSE_FLAG_NONE, NULL);
g_assert (pipe != NULL);
filter = gst_bin_get_by_name (GST_BIN (pipe), "filter");
g_assert (filter);
width = 320;
height = 240;
xdir = ydir = -10;
for (round = 0; round < MAX_ROUND; round++) {
gchar *capsstr;
g_print ("resize to %dx%d (%d/%d) \r", width, height, round, MAX_ROUND);
/* we prefer our fixed width and height but allow other dimensions to pass
* as well */
capsstr = g_strdup_printf ("video/x-raw, width=(int)%d, height=(int)%d",
width, height);
caps = gst_caps_from_string (capsstr);
g_free (capsstr);
g_object_set (filter, "caps", caps, NULL);
gst_caps_unref (caps);
if (round == 0)
gst_element_set_state (pipe, GST_STATE_PLAYING);
width += xdir;
if (width >= 320)
xdir = -10;
else if (width < 200)
xdir = 10;
height += ydir;
if (height >= 240)
ydir = -10;
else if (height < 150)
ydir = 10;
message =
gst_bus_poll (GST_ELEMENT_BUS (pipe), GST_MESSAGE_ERROR,
50 * GST_MSECOND);
if (message) {
g_print ("got error \n");
gst_message_unref (message);
}
}
g_print ("done \n");
gst_object_unref (filter);
gst_element_set_state (pipe, GST_STATE_NULL);
gst_object_unref (pipe);
return 0;
}
注意如何使用gst_bus_poll(),用一个较小的超时来获取消息并且引入了一个较短的sleep。
可以使用;分隔capsfilter的caps来设置多个caps。capsfilter可以尝试重新协调列表中第一个可能的格式。
动态改变管道
本节我们讨论动态改动管道的一些技术。我们讨论了改变特别是在PLAYING状态中的管道并且不中断数据流。
构建动态管道时有一些重要的事需要考虑:
- 当从管道中移除组件时,确保未链接的pad上没有数据流,否则会造成严重的管道错误。总是在解除pad链接前阻塞源pad(推送模式)或槽pad(拉取模式)。
- 当为管道添加组件,在允许数据流动强,确保将组件设置为正确的状态,通常是和父组件同样的状态。当一个组件是刚创建的,它在NULL状态中,当它接收到数据时会返回错误。
- 当为管道添加组件,GStreamer会默认设置组件的时钟和基准时间为当前管道的值。这意味着组件能够构建与管道中其他组件相同的管道running-time。就是说槽会像管道中其他槽一样同步缓冲,并且源会用与其他源匹配的tunning-time来制造缓冲。
- 当解除上游链组件的链接时,通过向组件的槽pad(s)发送EOS事件和等待EOS离开组件(事件探针)确保刷新组件所有队列中的数据。
如果你不实施刷新,你会失去未链接组件中缓冲的数据。这会导致简单帧的丢失(一些视频帧,音频的几个毫秒,等等)但是如果你移除一个muxer————某些情况一个解码器或类似的组件,你会冒着得到一个损坏的文件的风险,这个文件可能因为一些相关的源数据(header, seek/index tables, internal sync tags)没有正确地存储或更新而不能正确地播放。 - 直播源会用与管道当前running-time一样的running-time制造缓冲。
没有直播源的管道使用从0开始的running-time来制造缓冲。类似的,在一个刷新寻找之后,管道会将running-time重置为0。
running-time可以用gst_pad_set_offset()来改变。了解管道中的组件的running-time对保持同步来说十分重要。 - 添加组件可能会改变管道的状态。例如添加一个没有预滚的槽将使管道倒退回预滚状态。移除一个没有预滚的组件可能会将管道从PAUSED状态转变为PLAYING状态。
添加一个直播源会取消预滚阶段并将管道置为PLAYING状态。添加任何直播源都可能会改变管道的延迟。
添加或移除管道的组件可能会改变管道时钟的选择。如果新添加的组件提供了一个时钟,管道最好使用这个新的时钟。另一方面,如果为管道提供了时钟的组件被移除了,那么必须选择一个新的时钟。 - 添加或移除组件可能会造成上游或下游组件协调caps和/或allocators。你不需要真的在应用中做什么,插件自己就能适应新的管道拓扑,为了优化格式和分配策略。
重要的是当你添加、移除或改变管道中的组件,可能管道需要协调新的格式,而这可能失败。通常,你可以通过在需要的地方插入正确的converter组件来修复这个问题。
GStreamer支持几乎任何动态管道修改,但你需要在动态修改管道而不造成任何问题之前了解一些细节。下面的小节将会展示一些典型的修改用例。
改动管道中的组件
在这个例子中组件链如下所示:
- ----. .----------. .---- -
element1 | | element2 | | element3
src -> sink src -> sink
- ----’ ‘----------’ '---- -
我们想要在管道处于PLAYING状态时将组件2替换为组件4。我们假设组件2时一个可视化而你想在管道中切换可视化。
我们不能仅仅只是解除组件1的源pad和组件2的槽pad之间的链接,因为这会使得组件1的源pad处于未链接的状态,当数据推向源pad时会在管道中造成流错误。技巧就是在我们用组件4替换组件2之前阻塞组件1源pad的数据流,然后通过以下步骤恢复数据流:
- 使用阻塞pad探针阻塞组件1的源pad。当pad阻塞时,探针的回调将被调用。
- 阻塞回调中组件1与组件2之间在解除阻塞前没有数据流动。
- 解除组件1与组件2的链接。
- 确保组件2的数据被清除。一些组件可能会在内部保留一些数据,你需要通过强制清除组件2的数据以确保没有遗漏。你可以通过推送EOS给组件2来做到,就像这样:
在组件2的源pad中放置一个事件探针。
向组件2的槽pad发送EOS。这会确保组件2中所有的数据被清除。
等待EOS事件在组件2中的源pad中出现。当EOS被接收,丢弃事件并移除事件探针。 - 解除组件2和组件3的链接。现在你可以从管道中移除组件2并且将状态设置为NULL。
- 将组件4添加到管道中,如果还没有添加的话。链接组件4与组件3。链接组件1与组件4。
- 确保组件4与管道中其他组件处于同一状态。在它能够接收缓冲和事件前需要至少设置为PAUSED状态。
- 解除组件1源pad探针的阻塞。这会使得新的数据进入组件4并继续流。
上述算法在源pad阻塞时才能管用,也就是管道中存在数据流的场合。如果没有数据流,那么也就没有必要改变组件(目前),所以这个算法也可以在PAUSED状态下使用。
这个例子将每一秒改变一次简单管道中的视频效果:
#include <gst/gst.h>
static gchar *opt_effects = NULL;
#define DEFAULT_EFFECTS "identity,exclusion,navigationtest," \
"agingtv,videoflip,vertigotv,gaussianblur,shagadelictv,edgetv"
static GstPad *blockpad;
static GstElement *conv_before;
static GstElement *conv_after;
static GstElement *cur_effect;
static GstElement *pipeline;
static GQueue effects = G_QUEUE_INIT;
static GstPadProbeReturn
event_probe_cb (GstPad * pad, GstPadProbeInfo * info, gpointer user_data)
{
GMainLoop *loop = user_data;
GstElement *next;
if (GST_EVENT_TYPE (GST_PAD_PROBE_INFO_DATA (info)) != GST_EVENT_EOS)
return GST_PAD_PROBE_PASS;
gst_pad_remove_probe (pad, GST_PAD_PROBE_INFO_ID (info));
/* push current effect back into the queue */
g_queue_push_tail (&effects, gst_object_ref (cur_effect));
/* take next effect from the queue */
next = g_queue_pop_head (&effects);
if (next == NULL) {
GST_DEBUG_OBJECT (pad, "no more effects");
g_main_loop_quit (loop);
return GST_PAD_PROBE_DROP;
}
g_print ("Switching from '%s' to '%s'..\n", GST_OBJECT_NAME (cur_effect),
GST_OBJECT_NAME (next));
gst_element_set_state (cur_effect, GST_STATE_NULL);
/* remove unlinks automatically */
GST_DEBUG_OBJECT (pipeline, "removing %" GST_PTR_FORMAT, cur_effect);
gst_bin_remove (GST_BIN (pipeline), cur_effect);
GST_DEBUG_OBJECT (pipeline, "adding %" GST_PTR_FORMAT, next);
gst_bin_add (GST_BIN (pipeline), next);
GST_DEBUG_OBJECT (pipeline, "linking..");
gst_element_link_many (conv_before, next, conv_after, NULL);
gst_element_set_state (next, GST_STATE_PLAYING);
cur_effect = next;
GST_DEBUG_OBJECT (pipeline, "done");
return GST_PAD_PROBE_DROP;
}
static GstPadProbeReturn
pad_probe_cb (GstPad * pad, GstPadProbeInfo * info, gpointer user_data)
{
GstPad *srcpad, *sinkpad;
GST_DEBUG_OBJECT (pad, "pad is blocked now");
/* remove the probe first */
gst_pad_remove_probe (pad, GST_PAD_PROBE_INFO_ID (info));
/* install new probe for EOS */
srcpad = gst_element_get_static_pad (cur_effect, "src");
gst_pad_add_probe (srcpad, GST_PAD_PROBE_TYPE_BLOCK |
GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, event_probe_cb, user_data, NULL);
gst_object_unref (srcpad);
/* push EOS into the element, the probe will be fired when the
* EOS leaves the effect and it has thus drained all of its data */
sinkpad = gst_element_get_static_pad (cur_effect, "sink");
gst_pad_send_event (sinkpad, gst_event_new_eos ());
gst_object_unref (sinkpad);
return GST_PAD_PROBE_OK;
}
static gboolean
timeout_cb (gpointer user_data)
{
gst_pad_add_probe (blockpad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM,
pad_probe_cb, user_data, NULL);
return TRUE;
}
static gboolean
bus_cb (GstBus * bus, GstMessage * msg, gpointer user_data)
{
GMainLoop *loop = user_data;
switch (GST_MESSAGE_TYPE (msg)) {
case GST_MESSAGE_ERROR:{
GError *err = NULL;
gchar *dbg;
gst_message_parse_error (msg, &err, &dbg);
gst_object_default_error (msg->src, err, dbg);
g_clear_error (&err);
g_free (dbg);
g_main_loop_quit (loop);
break;
}
default:
break;
}
return TRUE;
}
int
main (int argc, char **argv)
{
GOptionEntry options[] = {
{"effects", 'e', 0, G_OPTION_ARG_STRING, &opt_effects,
"Effects to use (comma-separated list of element names)", NULL},
{NULL}
};
GOptionContext *ctx;
GError *err = NULL;
GMainLoop *loop;
GstElement *src, *q1, *q2, *effect, *filter1, *filter2, *sink;
gchar **effect_names, **e;
ctx = g_option_context_new ("");
g_option_context_add_main_entries (ctx, options, NULL);
g_option_context_add_group (ctx, gst_init_get_option_group ());
if (!g_option_context_parse (ctx, &argc, &argv, &err)) {
g_print ("Error initializing: %s\n", err->message);
g_clear_error (&err);
g_option_context_free (ctx);
return 1;
}
g_option_context_free (ctx);
if (opt_effects != NULL)
effect_names = g_strsplit (opt_effects, ",", -1);
else
effect_names = g_strsplit (DEFAULT_EFFECTS, ",", -1);
for (e = effect_names; e != NULL && *e != NULL; ++e) {
GstElement *el;
el = gst_element_factory_make (*e, NULL);
if (el) {
g_print ("Adding effect '%s'\n", *e);
g_queue_push_tail (&effects, el);
}
}
pipeline = gst_pipeline_new ("pipeline");
src = gst_element_factory_make ("videotestsrc", NULL);
g_object_set (src, "is-live", TRUE, NULL);
filter1 = gst_element_factory_make ("capsfilter", NULL);
gst_util_set_object_arg (G_OBJECT (filter1), "caps",
"video/x-raw, width=320, height=240, "
"format={ I420, YV12, YUY2, UYVY, AYUV, Y41B, Y42B, "
"YVYU, Y444, v210, v216, NV12, NV21, UYVP, A420, YUV9, YVU9, IYU1 }");
q1 = gst_element_factory_make ("queue", NULL);
blockpad = gst_element_get_static_pad (q1, "src");
conv_before = gst_element_factory_make ("videoconvert", NULL);
effect = g_queue_pop_head (&effects);
cur_effect = effect;
conv_after = gst_element_factory_make ("videoconvert", NULL);
q2 = gst_element_factory_make ("queue", NULL);
filter2 = gst_element_factory_make ("capsfilter", NULL);
gst_util_set_object_arg (G_OBJECT (filter2), "caps",
"video/x-raw, width=320, height=240, "
"format={ RGBx, BGRx, xRGB, xBGR, RGBA, BGRA, ARGB, ABGR, RGB, BGR }");
sink = gst_element_factory_make ("ximagesink", NULL);
gst_bin_add_many (GST_BIN (pipeline), src, filter1, q1, conv_before, effect,
conv_after, q2, sink, NULL);
gst_element_link_many (src, filter1, q1, conv_before, effect, conv_after,
q2, sink, NULL);
gst_element_set_state (pipeline, GST_STATE_PLAYING);
loop = g_main_loop_new (NULL, FALSE);
gst_bus_add_watch (GST_ELEMENT_BUS (pipeline), bus_cb, loop);
g_timeout_add_seconds (1, timeout_cb, loop);
g_main_loop_run (loop);
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);
return 0;
}
注意,我们在效果之前和之后添加了videoconvert组件。这是必要的,因为一些组件可能在不同的色域空间中操作;通过插入转换组件,我们可以保证一个可以被协调的正确的格式。