基础教程3:Dynamic pipelines( 动态管道 )
目标
本教程显示了使用GStreamer所需的其他基本概念,它允许“动态”构建pipeline(管道),信息变得可用,而不是在应用程序开始时定义一条单一的管道。
完成本教程后,您将具备启动 Playback tutorials 。这里回顾的要点是:
-
如何在链接elements时实现更精细的控制。
-
如何获得有趣事件的通知以便及时做出反应。
-
元素可以处于的各种状态。
介绍
正如您将要看到的,本教程中的管道在设置为播放状态之前尚未完全构建。这没关系。如果我们不采取进一步行动,数据将到达管道末端,管道将生成错误消息并停止。但我们将采取进一步行动.....
在这个例子中我们将打开一个多路复用的文件,音频和视频被存储在同一个容器文件中。负责打开此类容器的element称为 解复用器,容器格式的一些示例有Matroska(MKV)、Quick Time(QT、MOV)、Ogg或Advanced Systems Format(ASF、WMV、WMA)。
如果一个容器嵌入了多个流(例如,一个视频和两个音频曲目),解复用器会将它们分开,并通过不同的输出端口将它们公开。这样,可以在管道中创建不同的分支,处理不同类型的数据。
GStreamer elements相互通信的端口称为pads (GstPad
)。存在sink pad(数据通过它进入元素)和src pad(数据通过它退出元素),根据定义很明显可以知道 source element 只有 src pad,sink element只有sink pad,而 filter element 两者都有。
图 1. GStreamer elements with their pads.
解复用器包含一sink pad,复用数据经过它进入element;含有多个source pads,复用数据中的每路数据各一个。
图2. A demuxer with two source pads.
为了完整起见,这里是一个包含一个解复用器和两个分支的简单pipeline,一个分支处理音频数据,一个分支处理视频数据。这不是此示例中构建的管道:
图3. 具有两个分支的示例管道。
处理解复用器的难点在于,直到收到一些数据以及有机会查看容器文件中的内容之前解复用器无法生成任何信息,即解复用器的source pad是动态生成的,在生成之前其他element无法与它连接,于是pipeline只能在这终止。
解决方案是构建从源代码到emuxer的管道,并将其设置为运行(play)。当解复用器接收到足够的信息以了解容器中流的数量和类型时,它将开始创建source pad。现在是我们完成构建pipeline 并将其连接到新添加的demuxer pads的正确时机。
为了简单起见,在本例中,只连接音频pad而忽略视频pad。
动态Hello World
将此代码复制到名为basic-tutorial-3.c
(或在GStreamer安装中找到)。
basic-tutorial
-3.c
#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 *resample;
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.resample = gst_element_factory_make ("audioresample", "resample");
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.resample || !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.resample, data.sink, NULL);
if (!gst_element_link_many (data.convert, data.resample, data.sink, NULL)) {
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://gstreamer.freedesktop.org/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_get_current_caps (new_pad);
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);
}
需要帮助吗?
如果您需要帮助来编译此代码,请参阅构建教程平台的部分:Linux操作系统 ,Mac OS X或Windows,或在Linux上使用此特定命令:
gcc basic-tutorial-3.c -o basic-tutorial-3 `pkg-config --cflags --libs gstreamer-1.0`
如果您需要帮助来运行此代码,请参阅运行教程平台的部分:Linux操作系统 ,Mac OS X或Windows .
本教程仅播放音频。媒体是从Internet获取的,因此可能需要几秒钟才能启动,具体取决于您的连接速度。
所需库:
gstreamer-1.0
工作流
CustomData
/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *convert;
GstElement *resample;
GstElement *sink;
} CustomData;
到目前为止,我们已经保存了所有需要的信息(指向GstElement
指针)作为局部变量。但由于本教程(以及大多数实际应用程序)需要使用回调函数,为了便于处理我们将所有数据组织成一个结构体。
/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);
这是一个前向 reference,将在稍后使用。
Build Pipeline
/* Create the elements */
data.source = gst_element_factory_make ("uridecodebin", "source");
data.convert = gst_element_factory_make ("audioconvert", "convert");
data.resample = gst_element_factory_make ("audioresample", "resample");
data.sink = gst_element_factory_make ("autoaudiosink", "sink");
我们像往常一样创建元素。uridecodebin
将在内部实例化所有必要的元素(源、解复用器和解码器),以将URI转换为原始音频和/或视频流。它完成了playbin所做的一半工作。因为它包含了demuxer,它的 source pads 在初始时不可用,我们需要在运行时动态地将它们链接起来。
audioconvert
是一个非常有用的插件,它能转换不同的音频格式,确保教程中的例子能够在任何平台上运行(各个平台上的音频解码器解码出的格式不一定符合audio sink的要求)。
audioresample
是一个非常有用的插件,它能够转换不同的音频采样率,同样是为了确保教程中的例子能够在任何平台上运行(aduio sink不一定支持各个平台上的音频解码器解码出的音频采样率)。
autoaudiosink
和前文中用到的autovideosink
一样,它将把音频流渲染到声卡上。请参阅上一教程。
if (!gst_element_link_many (data.convert, data.resample, data.sink, NULL)) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (data.pipeline);
return -1;
}
在这里,我们链接了elements converter, resample and sink,但我们并不将它们与 source 连接起来,因为此时它不包含任何source pads。我们只是暂时保留这个分支(converter + sink)未链接状态,等待后续处理。
/* Set the URI to play */
g_object_set (data.source, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm", NULL);
我们将文件的URI设置为通过属性播放,就像我们在上一个教程中所做的那样。
Signals(信号)
/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);
GSignals
是GStreamer中一个重点。它们将在某些事件发生的时候以回调的方式通知你。这些信号以名字属性区分,每个GObject都有它自己的信号。
在这一行中,我们正在将“pad-added”信号附加到我们的源(一个uridecodebin元素)上。为此,我们使用g_signal_connect()并提供要使用的回调函数(pad_added_handler)和一个数据指针。GStreamer不会对这个数据指针做任何处理,它只是将其转发给回调函数,以便我们可以与它共享信息。在这种情况下,我们传递了一个指向为此目的专门构建的CustomData结构的指针。
GstElement生成的信号可以在其文档中找到,或者如基础教程10:GStreamer工具所述,使用gst-inspect-1.0工具查找。
我们现在可以开始了!只需将管道设置为播放状态,并开始侦听总线以获取感兴趣的消息(如错误或结束),就像在以前的教程中一样。
回调函数
当source element最终获取到足够的信息从而开始生成数据的时候,它将创建source pads并且出发pad-added
信号,这时将调用信号连接的回调函数 :
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
-
src
是出发信号的element,在本教程中是uridecodebin
,信号处理程序的第一个参数总是触发它的对象。
-
new_pad
是src
element刚刚添加的GstPad
,这正是我们想要连接的pad。
-
data
是我们连接信号时传递的指针,在本教程中是一个CustomData
指针。
GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");
我们从CustomData
中获取autoaudioconvert
element,并使用gst_element_get_static_pad()
获取它的sink pad
,要与new_pad
连接的pad。在之前教程中我们连接elements,由GStreamer自行选择正确的pads,现在我们手动完成这部分连接。
/* 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
将生成尽可能多的pads,这取决于它能够出处理的数据类型,并且没生成一个pad,回调都会被调用一次,上述代码将避免我们尝试将new_pad
与一个已经连接了的element连接。
/* Check the new pad's type */
new_pad_caps = gst_pad_get_current_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;
}
现在我们将要检查这个 new pad 将要输出的数据类型,因为我们只对产生音频的 pads 感兴趣。我们之前创建了一段处理音频的管道(一个 audioconvert 链接到audioresample和autoaudiosink ),我们将无法将其链接到一个产生视频的pad 上。
gst_pad_get_current_caps()检索pad 的当前功能(即它当前输出的数据类型),这些功能被包装在一个GstCaps结构中。可以使用gst_pad_query_caps()查询pad 可以支持的所有可能的功能。一个pad 可以提供许多功能,因此GstCaps可以包含许多GstStructure,每个代表不同的功能。pad 上的当前功能将始终具有一个单一的GstStructure,并代表一个单一的媒体格式,或者如果当前还没有功能,则返回NULL。
在这种情况下,我们知道我们想要的pad 只有一个功能(音频),我们用gst_caps_get_structure()检索第一个GstStructure。
最后,使用gst_structure_get_name()我们恢复结构的名称,其中包含格式的主要描述(实际上是其媒体类型)。
如果名称不是audio/x-raw,这不是一个解码的音频pad ,我们对其不感兴趣。
否则,尝试链接
/* 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()
尝试连接两个pads。就像gst_element_link()
实例一样,必须指定从source到sink的链路,并且两个pads必须属于同一个bin/pipeline中的elements。
我们完成了!当出现正确类型的pad时,它将被链接到音频处理管道的其余部分,执行将继续直到出现错误或结束。然而,我们将通过引入状态的概念来从本教程中获取更多内容。
GStreamer States
我们已经谈到了一些状态,当我们说回放只有在管道到达PLAYING
状态。我们将在这里介绍其余的状态及其含义。GStreamer中有4种状态:
州 | 描述 |
---|---|
NULL | element 的NULL状态或初始状态。 |
READY | element 已准备好进入PAUSED。 |
PAUSED | element 被暂停,它准备接受和处理数据。然而,接收器元素只接受一个缓冲区,然后阻塞。 |
PLAYING | element 正在播放,时钟正在运行,数据正在流动。 |
你只能在相邻的之间移动,这是,你不能从NULL
到PLAYING,你必须通过中级考试READY
和PAUSED
状态。如果将pipeline 设置为PLAYING,
不过,GStreamer将为您进行中间转换。
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;
我们添加了这段代码,它侦听有关状态更改的总线消息,并将其打印在屏幕上,以帮助您理解转换。每个元素都会将有关其当前状态的消息放在总线上,因此我们将其过滤掉,只侦听来自管道的消息。
大多数应用程序只需要关心PLAYING
状态能否正常播放,PAUSED
状态能否正常暂停以及NULL
状态能否退出和回收资源。
练习
对大多数程序员而言,动态连接一直是一个困难的话题。通过实例化一个autovideosink(可能在前方有一个videoconvert),并在正确的 pad 出现时将其链接到解复用器,证明你已经掌握了这个技能。提示:你已经在屏幕上打印出了视频 pad 的类型。
你现在应该看到(和听到)和在基础教程1:Hell world!。在您使用的教程中 playbin
,这是一个方便的元素,可以自动为您处理所有的解复用和pad链接。大多数playback致力于playbin
。
总结
在本教程中,您学习了:
- 如何使用
GSignals
通知事件 - 如何连接
GstPads
而不是连接它们的父elements - GStreamer element的各种状态
你结合以上内容构建了一条动态pipeline,这条pipeline不是在程序开始就被指定好的,而是在获取到所有媒体信息的情况下创建的。
现在,您可以继续学习基本教程,并在中了解执行搜索与时间相关的查询 基础教程4:时间管理或移动到playback教程,并进一步了解playbin
元素。
请记住,在本页的附件中,您应该可以找到教程的完整源代码以及构建它所需的任何附件文件。很高兴在这里与您相遇,期待很快再见!