GStreamer Basic Tutorial 3 Dynamic Pipelines

"GStreamer 基本教程3: 动态管道"

Posted by Stephen on March 15, 2022

前言

本文是GStreamer学习笔记,也可以看成是对原文的意译。

这些教程描述了理解其余教程所需的GStreamer主题。

GStreamer教程:

基础教程 : GStreamer 介绍

基础教程 1: Hello world!

基础教程 2: GStreamer 概念

基础教程 3: 动态管道

基础教程 4: 时间管理

基础教程 5: GUI工具包集成

基础教程 6: 媒体格式和pad功能

基础教程 7: 多线程和Pad可用性

基础教程 8: 管道短路操作

基础教程 9: 媒体信息收集

基础教程 10: GStreamer工具

基础教程 11: 调试工具

基础教程 12: 流媒体

基础教程 13: 播放速度

基础教程 14: 有用的元素

基础教程 16: 特定平台元素

环境

系统环境

Distributor ID:	Ubuntu
Description:	Ubuntu 18.04.4 LTS
Release:	18.04
Codename:	bionic
Linux version :       5.3.0-46-generic ( buildd@lcy01-amd64-013 ) 
Gcc version:         7.5.0  ( Ubuntu 7.5.0-3ubuntu1~18.04 )

软件信息

version : 	
     GStreamer-1.0

正文

1. 目标

本教程展示了使用 GStreamer 所需的其余基本概念,它允许在信息可用时“即时”构建管道,而不是在应用程序开始时定义单一管道。

完成本教程后,您将具备开始 播放教程!here

所需的知识。这里学习的要点是:

  • 如何在链接元素时获得更好的控制。
  • 如何收到有趣事件的通知,以便您及时做出反应。
  • 元素可以处于的各种状态。

2. 介绍

如您将看到的,本教程中的管道在设置为播放状态之前并未完全构建。还行吧。如果我们不采取进一步的行动,数据将到达管道的末端,管道将产生错误消息并停止。但我们将采取进一步的行动……

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

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

GStreamer 元素相互通信的端口称为 pad ( GstPad)。存在sink pads,数据通过它进入一个元素,还有source pads,数据通过它离开一个元素。很自然地,源元素仅包含源垫,接收器元素仅包含接收垫,过滤器元素包含两者。

一个分路器包含一个接收器垫,多路复用的数据通过它到达,以及多个源垫,一个用于容器中找到的每个流:

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

处理 demuxer 时的主要复杂性在于,它们无法生成任何信息,直到它们接收到一些数据并且有机会查看容器以查看里面的内容。也就是说,解复用器从没有其他元素可以链接到的开始,因此流水线必须在它们处终止。

解决方案是构建从源到解复用器的管道,并将其设置为运行(播放)。当解复用器收到足够的信息以了解容器中流的数量和种类时,它将开始创建源垫。这是我们完成管道构建并将其连接到新添加的解复用器垫的正确时间。

为简单起见,在此示例中,我们将仅链接到音频垫并忽略视频。

3. 动态Hello world

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

编译运行

   $ gcc basic-tutorial-3.c -o basic-tutorial-3 `pkg-config --cflags --libs gstreamer-1.0`
   $ ./basic-tutorial-3.c
   
   Pipeline state changed from NULL to READY:
   Received new pad 'src_0' from 'source':
   It has type 'video/x-raw' which is not raw audio. Ignoring.
   Received new pad 'src_1' from 'source':
   Link succeeded (type 'audio/x-raw').
   Pipeline state changed from READY to PAUSED:
   Pipeline state changed from PAUSED to PLAYING:
   End-Of-Stream reached.

4. 演练

   /* 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基本上是指向 s 的指针)保存为局部变量。由于本教程(以及大多数实际应用程序)涉及回调,我们将把所有数据分组到一个结构中以便于处理。

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

我们像往常一样创建元素基础教程 14: 有用的元素uridecodebin将在内部实例化所有必要的元素(源、解复用器和解码器)以将 URI 转换为原始音频和/或视频流。它完成了一半的工作playbin。由于它包含解复用器,它的源焊盘最初不可用,我们需要即时链接到它们。

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

audioresample对于在不同的音频采样率之间进行转换很有用,同样确保此示例适用于任何平台,因为音频解码器产生的音频采样率可能不是音频接收器支持的采样率。

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

这里我们链接元素转换器、重采样和接收器,但我们不将它们与源链接,因为此时它不包含源焊盘。我们只是让这个分支(转换器+接收器)不链接,直到稍后。

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

4.1 信号

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

GSignals是 GStreamer 中的一个关键点。它们允许您在发生有趣的事情时收到通知(通过回调)。信号由名称标识,每个GObject信号都有自己的信号。

在这一行中,我们附加uridecodebin到源(一个元素)的“pad- added”信号。为此,我们使用g_signal_connect()并提供要使用的回调函数 ( pad_added_handler) 和数据指针。GStreamer 对这个数据指针不做任何事情,它只是将它转发给回调,以便我们可以与它共享信息。在这种情况下,我们传递一个指向CustomData我们专门为此目的构建的结构的指针。

生成的信号GstElement可以在其文档中找到,也可以使用基础教程 10: GStreamer工具gst-inspect-1.0中描述的工具。

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

4.2 回调

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

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

4.3 GStreamer状态

当我们说直到您将管道带入状态时才会开始播放时,我们已经谈到了一些PLAYING状态。我们将在这里介绍其余的状态及其含义。GStreamer 中有 4 个状态:

状态 描述
NULL 元素的 NULL 状态或初始状态。
READY 元素已准备好进入 PAUSED。
PAUSED 元素已暂停,它已准备好接受和处理数据。然而,接收器元素只接受一个缓冲区然后阻塞。
PLAYING 元素正在播放,时钟正在运行,数据正在流动。

你只能在相邻的之间移动,也就是说,你不能从NULL to PLAYING,你必须经过中间READYPAUSED 状态。

但是,如果您将管道设置为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. 练习

对于许多程序员来说,动态焊盘链接传统上一直是一个难题。autovideosink通过实例化一个(可能videoconvert在前面带有一个)并在出现正确的 pad 时将其链接到解复用器来证明您已经掌握了它。提示:您已经在屏幕上打印了视频板的类型。

您现在应该看到(并听到)与基础教程 1: Hello world!. 在您使用的那个教程中playbin,这是一个方便的元素,可以自动为您处理所有解复用和填充链接。大多数播放教程都专门用于playbin.

6. 结论

在本小节中,学习了:

  • 如何使用事件通知GSingals
  • 如何连接多个GstPad而不是他们的父元素
  • GStreamer元素的各种状态

结合了这些项目构建一个动态管道,它不是在程序开始时定义好的,而是在有关媒体的信息可用时创建的。

在下一小节基础教程 4: 时间管理中了解如何执行搜索和时间相关查询,或者转到播放教程,并获取有关该playbin元素的更多见解

后记