GStreamer随笔2 - Pipeline

部分图片来自网络文章,侵权必删


前言

        上一章节中我们介绍了GStreamer的一个基本概念:element,以及由element引申出的pad属性。这一节中我们介绍另一个重要的pipeline概念。


一、什么是pipeline

        单个element的功能相对简单独立(独立其实为了解耦,SB才把所有功能揉到一个模块),需要其他的element配合才可以完成复杂的功能。通过将source element与其他filter-like element以及最终的sink element链接起来,您就创建了一个媒体数据管道----pipeline。

        通过链接这三个元素,我们创建了一个非常简单的元素链。这样做的效果是,source element的输出将用作filter elemetn的输入,filter element将对数据进行处理并将结果发送到最终sink element。将上图想象成一个简单的 mp3 音频解码器:source是从磁盘读取文件,filter是mp3音频解码器,sink是声卡用于播放解码后的音频数据。在代码中,上图的写法如下:

#include <gst/gst.h>

int main (int argc, char *argv[])
{
    GstElement *pipeline;
    GstElement *source, *filter, *sink;

    /* 初始化*/
    gst_init (&argc, &argv);

    /* 创建管道 */
    pipeline = gst_pipeline_new ("my-pipeline");

    /* 创建多个元素 */
    source = gst_element_factory_make ("fakesrc", "source");
    filter = gst_element_factory_make ("identity", "filter");
    sink = gst_element_factory_make ("fakesink", "sink");

    /* 必须在链接之前将元素添加到管道中 */
    gst_bin_add_many (GST_BIN (pipeline), source, filter, sink, NULL);

    /* 链接相关元素 */
    if (!gst_element_link_many (source, filter, sink, NULL)) {
         g_warning ("Failed to link elements!");
    }

    [..]
}

        除了gst_element_link_many()函数可用之外,对于更具体的行为还有函数 gst_element_link () 和 gst_element_link_pads ()。您还可以获取对各个 pad 的引用,并使用各种 gst_pad_link_* () 函数链接这些 pad。

重要提示

您必须先将元素添加到element然后再链接它们,因为将元素添加到容器会断开任何现有的链接。此外您不能直接链接不在同一容器或管道中的元素。

二、pipeline如何工作

        pipeline创建出来了,但是是如何工作的呢?这时候就需要介绍Gst的底层工作原理的。简单一点就是:pipeline运行在mainloop线程中,管理着各个element,element之间通过传递不同形式的buffer来实现数据交换,application与pipeline之间通过bus交互命令与状态。

2.1 消息总线(Bus)

Gstreamer- 消息总线(bus)_gstreamer bus-CSDN博客

        消息总线负责将消息从pipeline流线程转发到其自己的线程上下文中的应用程序。默认情况下,每个pipeline都包含一个消息总线,因此应用程序不需要创建消息总线或任何东西。应用程序唯一应该做的就是在消息总线上设置一个消息处理程序,这类似于目标(object)的信号处理程序。当主循环运行时,消息总线会定期检查新消息,当有消息可用时处理程序会被调用。

2.1.1 消息总线的使用方法

有两种不同的方式使用消息总线:

  • 运行 GLib/Gtk+ 主循环(或自己定期迭代默认的 GLib main context)并将某种监视附加到消息总线。这样 GLib 主循环将检查消息总线是否有新消息,并在有消息时通知您。
           在这种情况下,通常您会使用 gst_bus_add_watch () 或 gst_bus_add_signal_watch () 。要使用消息总线,请使用 gst_bus_add_watch () 将消息处理程序附加到pipeline的消息总线。每当pipeline向消息总线发出消息时,就会调用此处理程序。在此处理程序中,检查信号类型并执行相应的操作。处理程序的返回值应为 TRUE 以保持处理程序连接到消息总线,返回 FALSE 以将其删除。

  • 自己检查消息总线上的消息,这可以使用 gst_bus_peek () 和/或 gst_bus_poll () 来完成。

#include <gst/gst.h>

static GMainLoop *loop;

static gboolean my_bus_callback (GstBus * bus, GstMessage * message, gpointer data)
{
  g_print ("Got %s message\n", GST_MESSAGE_TYPE_NAME (message));

  switch (GST_MESSAGE_TYPE (message)) {
    case GST_MESSAGE_ERROR:{
      GError *err;
      gchar *debug;

      gst_message_parse_error (message, &err, &debug);
      g_print ("Error: %s\n", err->message);
      g_error_free (err);
      g_free (debug);

      g_main_loop_quit (loop);
      break;
    }
    case GST_MESSAGE_EOS:
      /* end-of-stream */
      g_main_loop_quit (loop);
      break;
    default:
      /* unhandled message */
      break;
  }

  /* 我们希望在下次有消息时再次收到通知消息总线上,所以返回TRUE;
   * 如果返回FALSE表示我们想停止监控总线上的消息,回调函数将不会再被调用。
   */
  return TRUE;
}

gint main (gint argc, gchar * argv[])
{
  GstElement *pipeline;
  GstBus *bus;
  guint bus_watch_id;

  /* Gst 初始化 */
  gst_init (&argc, &argv);

  /* 创建 pipeline */
  pipeline = gst_pipeline_new ("my_pipeline");

  /* 在pipeline的消息总线上添加对新消息的监视,以监视默认的 GLib 主循环上下文
   */
  bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
  bus_watch_id = gst_bus_add_watch (bus, my_bus_callback, NULL);
  gst_object_unref (bus);

  /* [...] */

  /* create a mainloop that runs/iterates the default GLib main context
   * (context NULL), in other words: makes the context check if anything
   * it watches for has happened. When a message has been posted on the
   * bus, the default main context will automatically call our
   * my_bus_callback() function to notify us of that message.
   * The main loop will be run until someone calls g_main_loop_quit()
   */
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

  /* 清理并释放pipeline */
  gst_element_set_state (pipeline, GST_STATE_NULL);
  gst_object_unref (pipeline);
  g_source_remove (bus_watch_id);
  g_main_loop_unref (loop);

  return 0;
}

需要注意的是:my_bus_callback回调运行在pipeline主循环的线程上下文中,而应用程序是运行在消息总线上的,这意味着回调和应用程序之间的交互是异步的,因此不适合某些实时处理。如果需要,最简单的方法是编写一个 GStreamer 插件,该插件的主要用途非常就是将消息从pipeline传递到应用程序。这种方法的优点是 GStreamer 在内部进行的所有线程处理对应用程序都是隐藏的,应用程序开发人员根本不必担心线程问题。 

        如果您使用默认的 GLib 主循环集成,您可以连接到消息总线上的message信号,而不是通过gst_bus_add_watch()监视消息。这样你就不必在所有可能的消息类型上执行switch操作;只需以 message::<type> 的形式连接到感兴趣的信号,其中<type>是特定的消息类型。上面的代码段也可以写成:

GstBus *bus;

[..]

bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
gst_bus_add_signal_watch (bus);
g_signal_connect (bus, "message::error", G_CALLBACK (cb_message_error), NULL);
g_signal_connect (bus, "message::eos", G_CALLBACK (cb_message_eos), NULL);

[..]

        如果您不使用 GLib 主循环,则默认情况下异步消息信号将不可用。但是您可以安装一个自定义同步处理程序来唤醒自定义的主循环并使用 gst_bus_async_signal_func () 来发出信号。

2.1.1 消息的类型

        GStreamer 有一些预先定义的消息类型可以通过消息总线传递的。同样这些消息也是可以扩展的,插件可以定义附加消息,应用程序也可以决定处理这些消息或忽略它们。如果是错误消息,应用程序应该通过向用户提供视觉反馈来处理。

        所有消息都有消息源类型时间戳。消息源可用于查看哪个element发出了消息。下面是所有消息的列表,它们的作用,以及如何解析特定消息:

  • 错误、警告和信息通知:如果需要向用户显示有关pipeline状态的消息,则element会使用这些消息。

    1. 错误消息是致命的,会终止数据传递。应用需要修复该错误以便恢复pipeline活动。

    2. 警告不是致命的但仍然暗示存在问题。

    3. 信息消息用于非问题通知。

       所有消息都包含一个带有错误类型和消息的 GError,以及一个可选的调试字符串。两者都可以使用 gst_message_parse_error()、_parse_warning() 和 _parse_info() 提取。(错误和调试字符串必须在使用后释放)

  • 流结束通知:在流结束的时候发出。pipeline的状态不会改变但进一步的媒体处理将停止。应用程序可以使用它来跳到播放列表中的下一首歌曲。在流结束后也可以在流中定位后重新回放,然后播放将自动继续。

  • 标签:在流中找到元数据(metadata)时发出。对于pipeline可以多次发出此信号(例如一次用于描述性元数据,如艺术家姓名或歌曲标题;另一次用于流信息,如采样率和比特率),应用程序需要在内部缓存元数据。 gst_message_parse_tag() 可以用来解析taglist,当不再需要的时候应该调用gst_tag_list_unref()。

  • 状态变化:在状态变化成功后发出。 gst_message_parse_state_changed() 可以用来解析这个转换的新旧状态。

  • 缓冲:在缓存网络流期间发出。通过从 gst_message_get_structure() 返回的结构中提取“buffer-percent”属性,可以手动从消息中提取进度(以百分比为单位)。

  • element消息:这些是特定element独有的特殊消息,通常表示附加功能。element的文档应该详细提及特定element可以发送哪些element消息。例如,如果流包含redirect 指令,“qtdemux” QuickTime demuxer element可能会在某些情况下发送 “redirect” element 消息。

  • 应用程序特定消息:可以通过获取消息结构并读取其字段来提取的信息。通常这些消息可以安全地忽略。应用程序消息主要供应用程序内部使用,以便将来自某个线程的信息汇集并发送到到主线程中,这在应用程序使用element信号时特别有用(因为这些信号将在流线程的上下文中发出)。

2.2 信号(Signals)

         Signals 是 GStreamer 中的关键点,用于通知应用程序一些事情的发生(通过回调)。信号通过名称来标识,每个 GObject 都有自己的信号。示例如下:

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

在这一行中我们连接 source element的“pad-added”信号,我们使用 g_signal_connect() 并提供了信号发生后回调函数(pad_added_handler)和回调中需要传入的数据指针。GStreamer 不会对这个数据指针执行任何操作,它只是将其转发给回调,以便我们可以与它共享信息。

GstElement 生成的信号可以在其文档中找到,也可以使用 gst-inspect-1.0工具查看:

gst-inspect-1.0 uridecodebin

2.3 缓冲区和事件(Buffers and Events)

        流经pipeline的数据由缓冲区和事件组合构成。缓冲区包含实际的媒体数据,事件包含控制信息 ,例如seeking 信息和end-of-stream通知。在pipeline运行时所有这些都是自动流经pipeline,你不需要为此做任何事情。

2.3.1 缓冲区

        缓冲区包含将流经您创建的pipeline的数据。source element通常会创建一个新的缓冲区,并将其通过 pad 传递到链中的下一个element 。使用Gst基础架构创建媒体pipeline时,您不必自己处理缓冲区,elements 会为你做到这一点。除其他外,缓冲区包括:

  • 指向内存对象的指针,内存对象封装内存中的一个区域;

  • 缓冲区的时间戳;

  • 一个引用计数,指示有多少elements正在使用此缓冲区。当没有element引用缓冲区时,此引用计数将用于销毁缓冲区。

  • 缓冲区标志。

        简单的情况是创建一个缓冲区,分配内存并将数据放入其中,然后传递给下一个element。element引用缓冲区,读取数据并执行某些操作(例如创建新缓冲区并对其进行解码),最后取消引用缓冲区。典型的视频或音频解码器是这样工作的。Elements可以不分配新缓冲区而直接修改缓冲区,Elements还可以写入硬件内存(例如来自video-capture sources)或DMABUF。缓冲区可以是读写的,也可以是只读的。

2.3.2 事件

        事件是与缓冲区数据流并行传递的对象,以便在pipeline中向上游和下游element通知各种事件,应用使用事件函数在pad上接收/发送事件。

上游事件用于application-element交互以及element-element交互以请求更改流状态,例如seeks。下游事件向其他elements通知流的状态,例如seeking, flushes, end-of-stream等。应用程序只需要关注上游事件。存在不同类型的事件来实现各种功能:

  • GST_EVENT_FLUSH_START:数据将被丢弃
  • GST_EVENT_FLUSH_STOP:再次允许数据
  • GST_EVENT_CAPS:关于随后的缓冲区的格式信息
  • GST_EVENT_SEGMENT:关于随后的缓冲区的时序信息
  • GST_EVENT_TAG:关于流的元数据信息。
  • GST_EVENT_BUFFERSIZE:缓冲区大小要求。目前还没有使用。
  • GST_EVENT_SINK_MESSAGE:事件被接sink转化为消息
  • GST_EVENT_EOS:pad上不会出现更多数据。
  • GST_EVENT_QOS:流服务质量的通知
  • GST_EVENT_SEEK:应该定位到流的新位置
  • GST_EVENT_NAVIGATION:导航事件。
  • GST_EVENT_LATENCY:配置pipeline的延迟
  • GST_EVENT_STEP:步进事件
  • GST_EVENT_RECONFIGURE:流重新配置事件

        source pad上调用 gst_pad_push_event() 首先将粘滞事件存储在粘滞数组中,然后再将事件发送到peer pad。如果没有peer pad并且事件没有存储在粘性数组中,则返回 FALSE。

        sink pad 上调用 gst_pad_send_event() 发送 pad 上的事件函数。如果事件函数返回成功则粘滞事件存储在粘滞事件数组中,并将该事件标记为更新。

     下面是一个seek 时间的示例:

static void seek_to_time (GstElement *element, guint64 time_ns)
{
  GstEvent *event;

  event = gst_event_new_seek (1.0, GST_FORMAT_TIME,
                  GST_SEEK_FLAG_NONE,
                  GST_SEEK_METHOD_SET, time_ns,
                  GST_SEEK_TYPE_NONE, G_GUINT64_CONSTANT (0));

  gst_element_send_event (element, event);
}

2.4 通信(Communication)

基于上述功能模块,GStreamer 提供了用于应用程序和pipeline之间的通信和数据交换的机制。

  • 缓冲区:用于在管道中的元素之间传递流数据的对象。缓冲区始终从源element传输到接收element;
  • 事件:element之间或从应用程序发送到element的对象。事件可以向上游和下游传输,下游事件可以同步到数据流。
  • 消息:element通过消息总线在pipeline发布的对象,它们将保存在总线上供应用程序收集。消息可以被发布消息的element所在线程同步拦截处理,但通常被被应用程序的主线程异步处理。消息用来将诸如错误、标签、状态更改、缓冲状态、重定向等信息从element传输到应用程序,这些传输通常是线程安全的。
  • 查询:允许应用程序从pipeline请求信息(例如持续时间或当前播放位置),查询始终被同步响应。element还可以使用查询从其对等element请求信息(例如文件大小或持续时间)。查询可以在pipeline内双向使用但上游查询更常见。

三、创建应用程序

        前面分析了GStreamer中用到的基础模块信息,是时候搭建一个自己的应用程序。下面将总结您在前面学到的所有内容,描述一个简单的 GStreamer 应用程序的所有方面,包括初始化库、创建元素、在管道中打包元素以及播放此管道。通过完成所有这些,您将能够构建一个简单的 Ogg/Vorbis 音频播放器。

#include <gst/gst.h>
#include <glib.h>


static gboolean bus_call (GstBus     *bus,
          GstMessage *msg,
          gpointer    data)
{
  GMainLoop *loop = (GMainLoop *) data;

  switch (GST_MESSAGE_TYPE (msg)) {

    case GST_MESSAGE_EOS:
      g_print ("End of stream\n");
      g_main_loop_quit (loop);
      break;

    case GST_MESSAGE_ERROR: {
      gchar  *debug;
      GError *error;

      gst_message_parse_error (msg, &error, &debug);
      g_free (debug);

      g_printerr ("Error: %s\n", error->message);
      g_error_free (error);

      g_main_loop_quit (loop);
      break;
    }
    default:
      break;
  }

  return TRUE;
}


static void on_pad_added (GstElement *element,
              GstPad     *pad,
              gpointer    data)
{
  GstPad *sinkpad;
  GstElement *decoder = (GstElement *) data;

  /* We can now link this pad with the vorbis-decoder sink pad */
  g_print ("Dynamic pad created, linking demuxer/decoder\n");

  sinkpad = gst_element_get_static_pad (decoder, "sink");

  gst_pad_link (pad, sinkpad);

  gst_object_unref (sinkpad);
}



int main (int   argc,
      char *argv[])
{
  GMainLoop *loop;

  GstElement *pipeline, *source, *demuxer, *decoder, *conv, *sink;
  GstBus *bus;
  guint bus_watch_id;

  /* Initialisation */
  gst_init (&argc, &argv);

  loop = g_main_loop_new (NULL, FALSE);


  /* Check input arguments */
  if (argc != 2) {
    g_printerr ("Usage: %s <Ogg/Vorbis filename>\n", argv[0]);
    return -1;
  }


  /* Create gstreamer elements */
  pipeline = gst_pipeline_new ("audio-player");
  source   = gst_element_factory_make ("filesrc",       "file-source");
  demuxer  = gst_element_factory_make ("oggdemux",      "ogg-demuxer");
  decoder  = gst_element_factory_make ("vorbisdec",     "vorbis-decoder");
  conv     = gst_element_factory_make ("audioconvert",  "converter");
  sink     = gst_element_factory_make ("autoaudiosink", "audio-output");

  if (!pipeline || !source || !demuxer || !decoder || !conv || !sink) {
    g_printerr ("One element could not be created. Exiting.\n");
    return -1;
  }

  /* Set up the pipeline */

  /* we set the input filename to the source element */
  g_object_set (G_OBJECT (source), "location", argv[1], NULL);

  /* we add a message handler */
  bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
  bus_watch_id = gst_bus_add_watch (bus, bus_call, loop);
  gst_object_unref (bus);

  /* we add all elements into the pipeline */
  /* file-source | ogg-demuxer | vorbis-decoder | converter | alsa-output */
  gst_bin_add_many (GST_BIN (pipeline),
                    source, demuxer, decoder, conv, sink, NULL);

  /* we link the elements together */
  /* file-source -> ogg-demuxer ~> vorbis-decoder -> converter -> alsa-output */
  gst_element_link (source, demuxer);
  gst_element_link_many (decoder, conv, sink, NULL);
  g_signal_connect (demuxer, "pad-added", G_CALLBACK (on_pad_added), decoder);

  /* note that the demuxer will be linked to the decoder dynamically.
     The reason is that Ogg may contain various streams (for example
     audio and video). The source pad(s) will be created at run time,
     by the demuxer when it detects the amount and nature of streams.
     Therefore we connect a callback function which will be executed
     when the "pad-added" is emitted.*/


  /* Set the pipeline to "playing" state*/
  g_print ("Now playing: %s\n", argv[1]);
  gst_element_set_state (pipeline, GST_STATE_PLAYING);


  /* Iterate */
  g_print ("Running...\n");
  g_main_loop_run (loop);


  /* Out of the main loop, clean up nicely */
  g_print ("Returned, stopping playback\n");
  gst_element_set_state (pipeline, GST_STATE_NULL);

  g_print ("Deleting pipeline\n");
  gst_object_unref (GST_OBJECT (pipeline));
  g_source_remove (bus_watch_id);
  g_main_loop_unref (loop);

  return 0;
}

现在我们已经创建了一个完整的pipeline。我们可以按如下方式可视化该管道:

这段代码是在Docker容器中执行的一系列命令,用于安装一些软件包和依赖项。具体来说,它执行以下操作: 1. `apt-get clean`:清理apt-get缓存,以释放磁盘空间。 2. `apt-get update`:更新apt-get软件包列表。 3. `apt-get install -y`:安装以下软件包和依赖项: - `python3`:Python 3的主要二进制文件。 - `python3-pip`:Python 3的包管理工具pip。 - `libopencv-dev`:OpenCV开发库的头文件和静态库。 - `python3-opencv`:Python 3的OpenCV绑定。 - `build-essential`:构建软件包所需的基本工具和编译器。 - `yasm`:视频编解码器的汇编器。 - `cmake`:跨平台的构建工具。 - `libtool`:通用库支持脚本工具。 - `libc6`、`libc6-dev`:C标准库的运行时库和开发文件。 - `unzip`:解压缩工具。 - `wget`:网络下载工具。 - `libnuma1`、`libnuma-dev`:NUMA(非统一内存访问)系统的库和开发文件。 - `libgstreamer1.0-0`:GStreamer多媒体框架的核心库。 - `gstreamer1.0-plugins-base`、`gstreamer1.0-plugins-good`、`gstreamer1.0-plugins-bad`、`gstreamer1.0-plugins-ugly`、`gstreamer1.0-libav`:GStreamer插件和解码器。 - `gstreamer1.0-doc`、`gstreamer1.0-tools`、`gstreamer1.0-x`、`gstreamer1.0-alsa`、`gstreamer1.0-gl`、`gstreamer1.0-gtk3`、`gstreamer1.0-qt5`、`gstreamer1.0-pulseaudio`:GStreamer的文档、工具和相关库。 - `libglib2.0-dev`:GLib开发库的头文件。 - `libgstrtspserver-1.0-dev`:GStreamer RTSP服务器库的开发文件。 - `gstreamer1.0-rtsp`:GStreamer的RTSP插件。 这些操作旨在为容器配置一个适合开发的环境,使其能够支持Python编程、OpenCV图像处理和GStreamer多媒体处理等任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值