GStreamer笔记二: Dynamic Pipeline

主要也是关于建立Pipeline的,不过主要目的是建立动态的Pipeline,即在信息可用时随时创建Pipeline,而不是在应用程序开始时候定义单一Pipeline。

一些基本概念的重申

本次尝试: 将Pipeline在其未完全建立起来时设置为Playing状态。虽然这并没有什么问题,如果不做任何动作,当数据到达Pipeline末端时将会由Pipeline产生一个error并停止,所以尝试会采取进一步的操作。尝试打开一个多路复用(muxed)的文件,即视频和音频存在一个容器文件中。负责打开该容器的元素称之为分离器(demuxers)。容器格式例如: MKV(Matroska), QT/MOV(Quick Time), Ogg或高级系统格式如ASF, WMV, WMA等。

pad

如前所述,pad就是Gstreamer元素间互相通信的一个接口,也有人翻译为衬垫。数据通过sink pad流入,通过source pad流出。只包含source pad的称之为source元素,只包含sink pad的元素称为sink元素,两者兼有则称之为filter元素。如图所示:



分离器

如果一个容器嵌入多个流(例如一个视频和两个音频轨道),则分离器将分离它们并将其展示于不同的输出端口。通过这种方式,可以在流水线中创建不同的分支,处理不同类型的数据。
一个含有两个source pad和一个sink pad的分离器的例子如图所示:




使用分离器的一个Pipeline例子如下:




该例是一个基本的Ogg播放器的Gstreamer Pipeline。
处理分离器的主要复杂性在于,只有在接收到一些数据且有机会查看容器并看到其内部信息之后,才能产生信息。即分离器开始时,没有任何其他元素能连接的source pad,因此Pipeline必须终止它们。
解决方法是建立一个从source向下到分离器的一个Pipeline,并将其设置为运行(Play)。当分离器了解了关于容器中数据流的数目和种类的足够信息之后,其会开始创建source pads。此时即是完成Pipeline创建并将其添加到新的分离器pads上的最佳时机。
简单起见,所用例子仅连接到audio pad,忽略video pad。

动态建立示例

一个动态的HelloWorld示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <gst/gst.h>
/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *convert;
GstElement *sink;
} CustomData;
/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);
int main(int argc, char *argv[]) {
CustomData data;
GstBus *bus;
GstMessage *msg;
GstStateChangeReturn ret;
gboolean terminate = FALSE;
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Create the elements */
data.source = gst_element_factory_make ("uridecodebin", "source");
data.convert = gst_element_factory_make ("audioconvert", "convert");
data.sink = gst_element_factory_make ("autoaudiosink", "sink");
/* Create the empty pipeline */
data.pipeline = gst_pipeline_new ("test-pipeline");
if (!data.pipeline || !data.source || !data.convert || !data.sink) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Build the pipeline. Note that we are NOT linking the source at this
* point. We will do it later. */
gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert , data.sink, NULL);
if (!gst_element_link (data.convert, data.sink)) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (data.pipeline);
return -1;
}
/* Set the URI to play */
g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);
/* Start playing */
ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
g_printerr ("Unable to set the pipeline to the playing state.\n");
gst_object_unref (data.pipeline);
return -1;
}
/* Listen to the bus */
bus = gst_element_get_bus (data.pipeline);
do {
msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
/* Parse message */
if (msg != NULL) {
GError *err;
gchar *debug_info;
switch (GST_MESSAGE_TYPE (msg)) {
case GST_MESSAGE_ERROR:
gst_message_parse_error (msg, &err, &debug_info);
g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
g_clear_error (&err);
g_free (debug_info);
terminate = TRUE;
break;
case GST_MESSAGE_EOS:
g_print ("End-Of-Stream reached.\n");
terminate = TRUE;
break;
case GST_MESSAGE_STATE_CHANGED:
/* We are only interested in state-changed messages from the pipeline */
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
}
break;
default:
/* We should not reach here */
g_printerr ("Unexpected message received.\n");
break;
}
gst_message_unref (msg);
}
} while (!terminate);
/* Free resources */
gst_object_unref (bus);
gst_element_set_state (data.pipeline, GST_STATE_NULL);
gst_object_unref (data.pipeline);
return 0;
}
/* This function will be called by the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");
GstPadLinkReturn ret;
GstCaps *new_pad_caps = NULL;
GstStructure *new_pad_struct = NULL;
const gchar *new_pad_type = NULL;
g_print ("Received new pad '%s' from '%s':\n", GST_PAD_NAME (new_pad), GST_ELEMENT_NAME (src));
/* If our converter is already linked, we have nothing to do here */
if (gst_pad_is_linked (sink_pad)) {
g_print (" We are already linked. Ignoring.\n");
goto exit;
}
/* Check the new pad's type */
new_pad_caps = gst_pad_query_caps (new_pad, NULL);
new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
new_pad_type = gst_structure_get_name (new_pad_struct);
if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) {
g_print (" It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
goto exit;
}
/* Attempt the link */
ret = gst_pad_link (new_pad, sink_pad);
if (GST_PAD_LINK_FAILED (ret)) {
g_print (" Type is '%s' but link failed.\n", new_pad_type);
} else {
g_print (" Link succeeded (type '%s').\n", new_pad_type);
}
exit:
/* Unreference the new pad's caps, if we got them */
if (new_pad_caps != NULL)
gst_caps_unref (new_pad_caps);
/* Unreference the sink pad */
gst_object_unref (sink_pad);
}

该示例代码仅播放音频,由于是在线媒体,所以连接速度会与网速有关。

代码分析

首先定义了一个结构体:

1
2
3
4
5
6
7
/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *convert;
GstElement *sink;
} CustomData;

简单情况下,通常可以使用一个局部变量(一个GstElement类型的基本指针)来表示所需要的信息。但大多数情况下(包括该例)是涉及到回调的,所以将其放在一个结构体中以便处理。

1
2
/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);

此处是一个添加pad的前置函数声明。

1
2
3
4
/* Create the elements */
data.source = gst_element_factory_make ("uridecodebin", "source");
data.convert = gst_element_factory_make ("audioconvert", "convert");
data.sink = gst_element_factory_make ("autoaudiosink", "sink");

该段是创建Element的代码。其中:
uridecodebin通过将uri转化为原始音频或视频流来实例化所有的必要Element(source, 分离器,解码器),其所做的是playbin的一半。由于含有分离器,其source pads最初并不可用,而且我们需要随时将其连接起来。
audioconvert对于转换不同格式的音频很实用,由于解码器生成的格式可能和audio sink期望的不一样,所以为了确保其可以在任何平台上工作,使用audioconvert进行转换。
autoaudiosink在音频中类似于视频中的autovideosink,其将会把音频流送给声卡。

1
2
3
4
5
if (!gst_element_link (data.convert, data.sink)) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (data.pipeline);
return -1;
}

此处主要作用是将转换元素连接到sink,但是由于其不含source pads,所以没有将其连接到source上,仅仅是保持该分支(转换器+sink)为未连接状态,待后续处理。

1
2
/* Set the URI to play */
g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

这里通过设置文件uri属性方法来播放它们。

信号

GSignals是GStreamer中的一个关键部分。当一些你感兴趣的事发生时,它允许你通过回调的方式获得通知。信号(Signals)由名称标识,且每个GObject都有自己的signals。

1
2
/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

该行代码将一个”pad added”信号附加到我们的source(一个uridecoderbin元素)上。为此使用了g_signal_connect函数,并提供要使用的回调函数pad_add_handler和一个数据指针。GStreamer并未对数据指针做任何事,仅仅将它转发给回调函数,因此可以与其共享信息。在这种情况下,我们传递一个指向我们专门为此建立的一个结构体CustomData的指针。
GStreamer产生的信号可以通过其文档或者使用gst-inspect-1.0工具查询。
至此已经准备好了,只需要将Pipeline设置为PLAYING状态并开始监听总线(bus)上感兴趣的Message(如ERROR或EOS)。

回调函数

当source element有了足够的信息开始产生数据时,将会创建source pads,并触发“pad-added”信号。此时,回调函数就会被调用:

1
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {

其中,
信号处理的第一个参数始终是触发它的对象。src是触发这个信号的GstElement。此例中,它只能是uridecodebin,因为它是我们唯一附加的信号。
new_pad是刚刚添加到src element的GstPad, 通常是我们想要连接的pad。
data是我们附加到信号时提供的指针,例中用其传递CustomData指针。

1
GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");

从CustomData中提取转换元素,然后使用gst_element_get_static_pad函数取回其sink pad。这是我们希望连接到new_pad的pad。之前涉及的简单例子中直接将元素连接到元素,并由GStreamer选择合适的pad。现在我们直接将这些pad连接起来。

1
2
3
4
5
/* If our converter is already linked, we have nothing to do here */
if (gst_pad_is_linked (sink_pad)) {
g_print (" We are already linked. Ignoring.\n");
goto exit;
}

uridecodebin可以创建尽可能多的pad,而且每个pad都会调用这个回调函数。该行代码主要作用是阻止我们尝试连接到已经连接了的pad上。

1
2
3
4
5
6
7
8
/* Check the new pad's type */
new_pad_caps = gst_pad_query_caps (new_pad, NULL);
new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
new_pad_type = gst_structure_get_name (new_pad_struct);
if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) {
g_print (" It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
goto exit;
}

现在将会检查这个新pad要输出的数据类型,因为我们仅对产生音频的pad有兴趣。之前已经创建了一个处理音频的Pipeline(一个autoaudioconver连接到一个autoaudiosink),例中我们将不能将其连接到产生视频的pad上。
gst_pad_query_caps函数查询或检索pad的功能(这是一种它所支持的数据,封装在GstCaps结构体中),一个pad可以提供许多功能(cap),因此GstCap可能包含多个GstStructure,且每个表示不同的功能。
由于此例中我们知道我们想要的pad只有一个能力(audio),所以使用gst_caps_get_structure函数获取第一个GstStructure。
最后使用gst_structure_get_name函数获取包含格式(实际是媒体类型)的主要描述的结构体名称。如果名称不是audio/x-raw,这就不是解码的音频pad,也不是我们所感兴趣的。否则,尝试连接:

1
2
3
4
5
6
7
/* Attempt the link */
ret = gst_pad_link (new_pad, sink_pad);
if (GST_PAD_LINK_FAILED (ret)) {
g_print (" Type is '%s' but link failed.\n", new_pad_type);
} else {
g_print (" Link succeeded (type '%s').\n", new_pad_type);
}

其中,gst_pad_link函数尝试连接这两个pad。和gst_element_link函数一样,连接必须指定有source到sink,且这两个pad必须属于同一个bin(或Pipeline)中的元素。
至此已经基本完成,当出现一个正确类型的pad时,它将会被连接到音频处理Pipeline的其余部分,并执行且继续直到遇到ERROR或者EOS。但关于GStreamer的状态还是需要重申一下。

关于GStreamer States

之前,已经解释过GStreamer的状态了,一共四种,如下所示:

StateDescription
NULLthe NULL state or initial state of an element.
READYthe element is ready to go to PAUSED.
PAUSEDthe element is PAUSED, it is ready to accept and process data. Sink elements however only accept one buffer and then block.
PLAYINGthe element is PLAYING, the clock is running and the data is flowing.

四种状态只能在相邻状态之间移动,不能直接从NULL跳到PLAYING,必须经过中间的READYPAUSED状态。但是如果将管道设置为PLAYIING状态,GStreamer将会为你进行中间转换。

1
2
3
4
5
6
7
8
9
case GST_MESSAGE_STATE_CHANGED:
/* We are only interested in state-changed messages from the pipeline */
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
}
break;

此段代码的添加主要用于监听关于状态更改的总线消息(bus message),并将其打印在屏幕上以帮助了解这个转换。每个元素都将关于其当前状态的信息放在总线上,因此我们将其过滤出来,并只收听来自Pipeline的信息。
大多数应用程序只需要关心去PLAYING来开始播放,然后PAUSED来暂停,然后在程序退出时返回NULL来释放所有资源。

孤芳自赏,不必捧场。
分享