元心科技分享:GStreamer 初探 (三)

it2023-06-26  70

GStreamer 初探 (三)

这里是之前概述计划里介绍Gstreamer基本概念的第二部分,这次的初探三会将Gstreamer需要用到的剩余基本概念做一个讲解,最主要涉及的是不再在最初就将整个管道创建完成,取而代之的是在数据流可以使用时“动态”的创建管道来进行处理

这次教程结束之后,我们将获得打开GStreamer进阶内容的钥匙,当然,这需要我们在这次教程后,找到这些知识点的答案:1.在连接elements之后如何对其进行更好的控制,2.如何获取关注事件的消息以便及时做出回应,2.element都可能处于哪些不同的states

简介

本次教程我们将看到,在我们设置pipeline的状态为 play 之后,我们的 pipeline 居然还没有完整的构建出来,这怎么可以呢?假使我们的pipeline没有构建完成,数据流向pipeline后定然会出现错误然后停止下来,那有没有可能在设置state后采取行动进一步对pipeline进行操作呢?

在这次的例子里我们会使用一个既有音频数据也有视频数据的数据流,数据流被存放在container,而我们使用demuxer来处理具有多路数据的container,这些数据格式常见的有mkv、mov、ogg、asf、wmv、wma,demuxer会将container中的多路数据分流,根据不同类型分别开的数据流向不同的src,element 之间通过pads(GstPad)进行通信,数据通过sink pad流入一个element,通过source pad从element中流出,很自然的之前用到的source element会只包含source pad,sink element 只包含sink pad,而filter element 两种pad都会包含

然后,刚刚get到的demuxer会包含多个source pad

为了更完整的去理解这些,我们假定构建了这样一个管道,这并不是本次代码所要展示的,仅供理解,demuxer将数据流分成两支,一支用来处理音频数据,一支用来处理视频数据

demuxer的复杂性在于其在最初的时候是没有source pad给其他element连接的,它只有在查看到container里的都有哪些复用数据时才能构建自己的source pad并产生通知消息,所以一个pipeline必然会在它们这里终止,解决的办法是照常构建一个从source element到demuxer的pipeline,然后将pipeline的state设置为play,demuxer获取到container里复用数据的类别后开始创建它的source pad,恰恰这个时候我们完成构建新的pipeline然后将它连接在demuxer创建的pads上

简单起见,本次代码我们仅仅再次构建音频数据流的pipeline而省略了视频数据流的pipeline构建

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-2.c -o basic-tutorial-2 pkg-config --cflags --libs gstreamer-1.0 编译代码进行测试

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

为啥这回要将所有用到的element封装到一个结构体中嘞?是因为这次我们的例子中是有函数回调存在的,

将这些element放进一个结构体中方便在函数回调中传递和使用

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

这里我们正好回顾了上个教程的部分内容,我们实例了这样几个element

uridecodebin

uridecodebin将给定的URI转化为原生的音视频流,它会自动在内部实例所有实现该功能所必要的element,包括sources、demuxers和decoders,实际上这个组件完成的是playbin一半的功能,因为这个组件包含demuxers,所以在最初的时候source pads还没有确定,我们就需要去动态的连接它

audioconvert

audioconvert的功能是将两种不同的音频格式进行转化,一般是用于确保指定的音频数据可以被平台兼容,在本例中,audioconvert将audio decoder生成数据的格式进行转化以与audio sink所需要的数据格式保持一致

audioresample

audioresample的功能是将不同的音频采样率进行转化,同样,确保了音频数据可以工作,audioresample将audio decoder生成数据的采样进行转化以与audio sink所需要采样保持一致

autoaudiosink

跟之前遇到的autovideosink相似,autoaudiosink将音频数据输出输出到音频设备上

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

我们先把converter element和sink连接起来,因为现在的source element还没有生成source pads,我们没有把source element和它们连接起来(这个时候连接很明显会出错的),这部分会在之后进行进一步的处理

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

好,gobject设置属性的办法,看看各位还记不记得

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

GSignals 的概念在GStreamer里面有很多应用,当你感兴趣的事件发生时,GSignals会用函数回调的方式向你设定的回调函数传回数据,Signals通过一个name来区别,注意每一个GObject都有它自己的signals

g_signal_connected

这一行我们使用g_signal_connect将source组件(uridecodebin element)的pad-added信号和我们设置的回调函数pad_added_handler连接起来,同时传递了我们data的指针进去,Gstreamer将它转发给回调函数,这样我们就能在回调函数中使用到它

如何知道一个GstElement组件可以生成哪些信号呢?gst-inspect-1.0,当然,详细的介绍在之后的教程进行

这一些准备就绪之后,We are now ready to go!

和之前教程一样,将pipeline的状态设置为PLAYING,然后监听在bus上我们感兴趣的消息

当我们的source element 有足够的信息来生成数据时,它会创建自己的source pads,同时触发自己的pad-added信号,这时候连接信号的回调就会启用

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

src指向触发信号的GstElement,本例中就是uridecodebin,通常来讲,一个signal handler的第一个参数往往是触发信号的对象

new_pad 是src刚刚生成的GstPad实例,往往我们会连接到这个pad上

data就是我们附加到信号里的指针变量了,本例里面它指向了我们构造的CustomData对象

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

在之前的教程里,我们通过GStreamer自动的选择合适的pads,将两个element连接起来,这次我们手动直接的将pads连接起来

gst_element_get_static_pad

通过gst_element_get_static_pad获取到了data中convert组件的sink pad,我们想要将new_pad和sink 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; }

当然,检测new_pad输出的数据类型是必要的,我们为感兴趣的音频数据创建连接了一套管道出来,(audioconvert>>>>audioresample>>>>autoaudiosink),总不能将它连接到视频输出的pad上

gst_pad_get_current_caps

gst_pad_get_current_cap会取回指定pad的capabilities(一个GstCaps结构体),通过cap我们即可获知当前输出数据的类型,我们可以使用gst_pad_query_caps来查询pad能够支持的所有caps,一个pad可以提供许多的capabilities,因此呢Gstpads可以包含许多的GstStructure,每个GstStructure都代表着一个不同的能力,当前的pads上的caps将只拥有单一的GstStructure代表单一的媒体格式,或者可能出现的情况是没有caps,返回NULL

我们所需要的只有音频数据这一个capabilitity,而在这里我们使用gst_caps_get_structure来获取到caps 的第一个GstStructure

gst_structure_get_name

通过gst_structure_get_name,我们得到了structure的名称描述,这其中包含了数据的格式信息

明显的,如果这个数据不是audio/x-raw,那它就不是一个音频的pad,我们不需要

否则,我们就会尝试将两个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

gst_pad_link会尝试将两个pad连接起来,正如gst_element_link一样,这个连接一定需要是从source流向sink的,这两个pads同时必须属于同一个bin(pipeline)

好的,大功告成,当生成正确类型的pad时,它会被连接到剩余的音频处理的pipeline,我们的流程得以继续,直到出现ERROR或EOS

这里,我们扩充一些GStreamer的相关知识

GStreamer States

GStreamer的pipeline有四种状态,我们知道的当pipeline被设置为PLAYING状态后它可以运转起来

这里将所有的states罗列出来

NULL 初始状态或空状态

READY 处于这个状态元素已做好准备进入PAUSED状态

PAUSED 暂停状态,组件准备好接受和处理数据,组件只接受一个buffer或block

PLAYING 播放状态,clock在运行,数据在流动

只能在相邻的两个状态之间进行切换,这意味着如果要从NULL状态切换到PLAYING状态,就必须经过READY和PAUSED,所以当我们直接将状态设置成PLAUYING时,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;

这段代码会检测总线上有关pipeline状态更改的消息,然后将状态的相关信息打印输出出来,通过这些来帮助我们理解pipeline状态的转换,每个element都会将含有它状态转换的message发送到bus上,我们在此做了一个过滤

大部分的应用程序只需要关心在程序运行时切换到PLAYING和PAUSED状态,然后在程序退出时退回NULL状态释放资源

练习

实例化一个autosinkvideo,(其前面可能需要一个videoconvert)然后在demuxer生成需要的pad时将它连接上去,提示:代码中我们已经打印输出了视频pads的描述

通过这个练习,你可以看到和教程一一样的画面了,在教程一中我们使用的是playbin,它会自动的完成我们今天所做的工作,我们很多的教程也都是围绕它playbin展开的

总结

这次我们对动态生成的pad进行了连接,完成了一段音频或视频的播放过程,以下对其中的一些方法进行概括

使用GSignals接受事件的通知直接连接两个pad,而不是父元素elementGStreamer 不同的状态

运用这些知识,我们构建了一个动态的pipeline,pipeline不是在程序启动就构建完成,而是在有关媒体的信息可用时创建的。

下一次的教程我们将介绍Gstreamer中有关时间的查询

(此篇文章翻译自官网,旨在分享技术理念,如有不当敬请指正)

最新回复(0)