当前位置: 代码迷 >> 综合 >> GStreamer官方入门课程3:面向复杂媒体流,教你如何动态构建 GStreamer 复杂管道
  详细解决方案

GStreamer官方入门课程3:面向复杂媒体流,教你如何动态构建 GStreamer 复杂管道

热度:91   发布时间:2023-12-12 16:04:36.0

1. 目标

本教程展示了使用GStreamer所需的其他基本概念,这些概念允许在信息可用时“动态”构建管道,而不是在应用程序的开头定义单片管道。

在本教程之后,您将拥有开始播放教程所必需的知识。这里回顾的要点是:

  • 如何在链接元素时获得更好的控制。
  • 如何得到有趣事件的通知以便及时做出反应。

元素可以处于的各种状态。

2. 介绍

正如您将要看到的,本教程中的管道在设置为播放状态之前并没有完全构建。这没关系。如果我们不采取进一步的措施,数据将到达管道的末端,管道将生成错误消息并停止。但我们会采取进一步的行动…

在这个例子中,我们打开一个被多路复用(或多路复用)的文件,也就是说,音频和视频一起存储在一个容器文件中。负责打开此类容器的元素称为demuxers,容器格式的一些示例包括Matroska(MKV)、Quick Time(QT、MOV)、Ogg或Advanced Systems Format(ASF、WMV、WMA)。

如果一个容器嵌入了多个流(例如,一个视频和两个音频轨),则解复用器会将它们分开,并通过不同的输出端口公开它们。这样,可以在管道中创建不同的分支,处理不同类型的数据。

GStreamer元素相互通信的端口称为pad(GstPad)。存在数据进入元素的汇区和数据退出元素的源区。自然地,源元素只包含源pad,sink元素只包含sink pad,而filter元素包含这两者。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图1. GStreamer 中的元素和它们的 pads。


在这里插入图片描述

图2 具有两个 pad 的解码复用器(demuxer)
...

为了完整起见,这里有一个简化的管道,其中包含一个解复用器和两个分支,一个用于音频,一个用于视频。这不是将在本例中构建的管道:

在这里插入图片描述

图3. 拥有两个分支的管道的例子
...

处理demuxer时的主要复杂性是,它们在接收到一些数据并有机会查看容器以查看其中的内容之前,无法生成任何信息。也就是说,demuxer从没有其他元素可以链接到的源pad开始,因此管道必须在它们处终止。

解决方案是构建从源到解复用器的管道,并将其设置为run(play)。当demuxer接收到足够的信息来了解容器中的流的数量和类型时,它将开始创建源pad。现在是我们完成管道建设并将其连接到新添加的解复用器垫的正确时间。

为了简单起见,在本例中,我们将只链接到音频板而忽略视频。

3. 动态 Hello World

将此代码复制到名为basic-tutorial-3.c的文本文件中(或在GStreamer安装中找到它)。

#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://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_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);
}

4. 从头分析代码

/* 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;

到目前为止,我们已经将所需的所有信息(基本上是指向GstElements的指针)保留为局部变量。由于本教程(以及大多数实际应用程序)涉及回调,我们将把所有数据分组在一个结构中,以便于处理。

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

这是一个远期参考,稍后使用。

/* 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,它的源pad最初不可用,我们需要动态链接到它们。

audioconvert对于在不同音频格式之间进行转换非常有用,请确保此示例适用于任何平台,因为音频解码器生成的格式可能与音频接收器预期的格式不同。

音频重采样对于在不同的音频采样率之间进行转换非常有用,类似地,确保此示例在任何平台上都能工作,因为音频解码器生成的音频采样率可能不是音频接收器支持的采样率。

对于音频,autoaudiosink相当于上一个教程中的autovideosink。它会将音频流呈现到声卡。

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

在这里,我们将转换器元素链接到接收器,但不将它们与源链接,因为此时它不包含源 pads。我们只是不链接这个分支(转换器+接收器),直到稍后。

/* 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设置为通过属性播放,就像在上一个教程中一样。

(1) 信号

/* 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生成的信号可以在其文档中找到,也可以使用gst-inspect-1.0工具,如基本教程10:GStreamer工具中所述。

我们现在准备出发了!只需将管道设置为播放状态,并开始侦听总线以获取有趣的消息(如ERROREOS),就像在前面的教程中一样。

(2) 回调

当我们的源元素最终有足够的信息开始生成数据时,它将创建源pad,并触发“pad added”信号。此时,我们将调用回调:

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

src是触发信号的GstElement。在本例中,它只能是uridecodebin,因为它是我们附加到的唯一信号。信号处理程序的第一个参数始终是触发它的对象。

new_pad是刚添加到src元素的GstPad。这通常是我们要链接到的pad

数据是我们在附加到信号时提供的指针。在本例中,我们使用它传递CustomData指针。
·

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

CustomData中提取转换器元素,然后使用gst_element_get_static_pad()检索其sink pad。这是我们要链接到的new_pad。在上一个教程中,我们将元素链接到元素,并让GStreamer选择适当的pad。现在我们要直接把垫子连接起来。

/* 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一旦我们已经链接。

/* 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;
}

现在我们将检查这个新的pad要输出的数据类型,因为我们只对pad产生音频感兴趣。我们之前已经创建了一个处理音频的管道(一个与音频重采样和自动音频接收器链接的音频转换器),例如,我们将无法将其链接到产生视频的pad。

gst_pad_get_current_caps()检索封装在GstCaps结构中的pad的当前功能(即它当前输出的数据类型)。可以使用gst_pad_query_caps()查询一个焊盘可以支持的所有可能的上限。一个pad可以提供许多功能,因此gstcap可以包含许多GstStructure,每个GstStructure代表不同的功能。pad上的当前caps将始终具有单个GstStructure并表示单个媒体格式,或者如果没有当前caps,则返回NULL。

因为,在本例中,我们知道我们想要的pad只有一个功能(音频),所以我们使用gst_caps_get_structure()检索第一个GstStructure

最后,使用gst_structure_get_name()恢复结构的名称,它包含格式的主要描述(实际上是它的媒体类型)。

如果名称不是audio/x-raw,这不是解码的音频板,我们对此不感兴趣。

否则,尝试链接:

/* 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()尝试链接两个焊盘。与gst_element_link()的情况一样,必须从源到汇指定链接,并且两个焊盘都必须由位于同一个bin(或管道)中的元素拥有。

我们完成了!当出现正确类型的pad时,它将链接到音频处理管道的其余部分,并继续执行,直到出现错误或EOS为止。但是,通过引入状态的概念,我们将从本教程中挤出更多的内容。

(3) GStreamer 状态

我们已经讨论了一些状态,当我们说回放不会开始,直到你把管道带到播放状态。我们将在这里介绍其他状态及其含义。GStreamer有4个状态:

状态 描述
NULL 元素的空状态或初始状态。
READY 元素已准备好进入暂停状态。
PAUSED 元素暂停,准备接受和处理数据。但是,接收器元素只接受一个缓冲区,然后阻塞。
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以释放所有资源。

5. 练习

动态pad链接一直是许多程序员的一个难题。通过实例化一个autovideosink(可能前面有一个videoconvert)并在右pad出现时将其链接到demuxer来证明您已经掌握了它。提示:您已经在屏幕上打印了视频板的类型。

现在您应该看到(和听到)与基本教程1:Hello World!中相同的电影!。在那个教程中,您使用了playbin,它是一个方便的元素,可以自动为您处理所有的解组和pad链接。大多数回放教程都是专门介绍playbin的。

6. 小结

在本教程中,您了解到:

  • 如何使用GSignals通知事件
  • 如何直接连接gstpad而不是其父元素。
  • GStreamer元素的各种状态。
  • 您还可以组合这些项来构建一个动态管道,该管道不是在程序启动时定义的,而是在有关媒体的信息可用时创建的。

现在,您可以继续学习基本教程,并了解如何在基本教程4:时间管理中执行查找和与时间相关的查询,或者转到回放教程,进一步了解playbin元素。

请记住,附在本页上的是教程的完整源代码和构建教程所需的任何附件文件。很高兴你能来,很快就见到你了!

  相关解决方案