系列文章目录
- GStreamer 简明教程(一):环境搭建,运行 Basic Tutorial 1 Hello world!
- GStreamer 简明教程(二):基本概念介绍,Element 和 Pipeline
文章目录
- 系列文章目录
- 前言
- 一、静态与动态
- 二、Basic tutorial 3: Dynamic pipelines
- 2.1 准备阶段
- 2.2 信号
- 2.2 回调函数
- 2.2.1 函数定义
- 2.2.2 获取 pad 信息
- 2.2.3 尝试连接
- 2.2.4 获取 pad 的能力
- 2.3 GStreamer 状态
- 2.3.1 四个主要状态
- 2.3.2 状态切换
- 2.4 练习
- 参考
前言
本章来了解 GStreamer 动态调整 Pipeline 的流程,这章代码略长,略微有些复杂,各位看官稍稍耐心些。
一、静态与动态
在前两章中,我们遵循固定的流程:
- 创建 Elements
- 创建 Pipeline,将 Elements 相互连接
- 切换 Pipeline 的状态,开始音视频任务
- 等待任务结束
这种情况下,我们先确定 Pipeline 的拓扑结构,然后启动它,在整个运行阶段 Pipeline 结构是不会发生变化的,它是静态的。
现在要介绍的动态调整 Pipeline 在音视频任务中是非常需要的。一个视频文件它可能包含多个流(这块基础知识请参考 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)),假设现在有个元素负责解封装叫 demuxer,可以想象到它应该有一个 sink pad 用于接收文件流数据,那么它有几个 src pad 呢?答案是取决于输入文件。如果这个视频文件中包含多个视频或者音频流,那么它有包含对应个数的 src pad。
在如图所示的场景中,如果一个视频文件包含两个流:音频流和视频流,那么输入给 Demuxer 后,Demuxer 会有两个输出 pad;如果只有一个视频流,那么输出 pad 的数量为 1。
然而,Demuxer 的复杂之处在于,它需要先检查文件内容才能确定自身结构。这意味着在创建之初,Demuxer 的 source pad 数量为 0,从而无法立即与其他元素进行连接。
解决这一问题的方法是:首先连接那些已知能够配对连接的元素,然后启动 Pipeline。当 Demuxer 接收到足够的文件信息以确定容器中的流数量和类型时,它将开始创建相应的 source pad。此时,我们可以继续构建管道,并将新的元素附加到 Demuxer 的新创建的输出 pad 上。
这种方法确保了灵活性和动态性,使得管道可以在运行时根据实际数据进行调整,以便正确处理多种不同的媒体文件格式。
二、Basic tutorial 3: Dynamic pipelines
2.1 准备阶段
接下来过代码实现,具体代码太长了,不贴了,参考 Basic tutorial 3: Dynamic pipelines
注意,为了简化代码,在 GStreamer 的示例中只处理了视频文件中的音频流,因此运行程序后,你可以听到声音,但看不到画面。
/* 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;
先定义了一个结构体,这个结构体中包含所有使用到的 Element。这是因为本示例会使用到回调函数,我们需要在回调函数中获取到全局的信息,因此定义这样一个结构体会方便我们处理信息。
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);
回调函数上,通过名字你大致就可以知道,这个回调函数在 pad 被添加的时候调用。
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 (
source
):- 功能: 它是一个自动探测和解码多种URI(Uniform Resource Identifier)媒体资源的元素。这个元素可以处理多种输入格式,并根据输入的媒体类型自动选择并连接合适的解码器。
- 作用: 当你提供一个媒体文件的URI(比如文件路径、http流、rtsp流等)给这个元素,它会自动检测这个资源的类型,解析并生成相应的解码元素,以便将媒体数据传递给下一个处理元素。
-
audioconvert (
convert
):- 功能: 这个元素用于在不同的音频格式之间进行转换。它能够处理多种音频样本格式(如从S16LE(16位小端序)到FLT(浮点型)等)以及不同的通道布局和采样率。
- 作用: 当媒体数据被解码成原始音频流后,
audioconvert
元素保证这些音频数据被转换成适当的格式,以便进一步处理或播放。
-
audioresample (
resample
):- 功能: 这个元素用于重新采样音频数据。它可以将音频数据从一个采样率转换到另一个采样率,确保音频数据满足播放或处理段的要求。
- 作用: 例如,将48kHz的音频重新采样到44.1kHz,以适应某些播放设备或者后续处理元素的需求。
-
autoaudiosink (
sink
):- 功能: 这个元素是一个自动音频输出插件,能够根据系统环境自动选择最适合的音频输出设备(比如ALSA、OSS、PulseAudio等)。
- 作用: 它将处理后的音频数据输出到用户的音频设备(例如扬声器或耳机),使音频可以被播放出来。
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;
}
接着连接元素 convert -> resampler -> sink,注意这里我们并没有连接 source 元素,正如我们前面提到的,此时的 source 没有一个可用的 pad 与其他元素连接。如果你尝试连接 source 节点,gst_element_link_many
将会返回失败。
g_object_set (data.source, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm", NULL);
最后设置 source 的播放文件路径
2.2 信号
/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);
GSignal 是 GStreamer 中重要的工具,它们允许你在某些有趣的事情发生时通过回调函数得到通知。信号通过名字来识别,每个 GObject 都有它自己的信号。
在这行代码中,我们在 source 元素上的 “pad-added” 信号槽中注册了一个回调函数。当 source 元素发出了 “pad-added” 信号时,它会在信号槽中逐个地调用回调函数。我们使用 g_signal_connect() 并提供要使用的回调函数 (pad_added_handler) 和一个数据指针。GStreamer 对这个数据指针不做任何处理,只是将其传递给回调函数,这样我们就可以与回调函数共享信息。在这个例子中,我们传递的是为此目的专门构建的 CustomData 结构体的指针。
GStreamer 的元素中有哪些信号呢?这个数据你可以通过文档或者 gst-inspect-1.0 工具进行查询,具体参考 GStreamer 简明教程(二):基本概念介绍,Element 和 Pipeline
。
以 uridecodebin
为例,它包含的信号信息如下:
Element Signals:
"pad-added" : void user_function (GstElement * object,
GstPad * arg0,
gpointer user_data);
"pad-removed" : void user_function (GstElement * object,
GstPad * arg0,
gpointer user_data);
"no-more-pads" : void user_function (GstElement * object,
gpointer user_data);
"unknown-type" : void user_function (GstElement * object,
GstPad * arg0,
GstCaps * arg1,
gpointer user_data);
"autoplug-continue" : gboolean user_function (GstElement * object,
GstPad * arg0,
GstCaps * arg1,
gpointer user_data);
"autoplug-factories" : GValueArray * user_function (GstElement * object,
GstPad * arg0,
GstCaps * arg1,
gpointer user_data);
"autoplug-sort" : GValueArray * user_function (GstElement * object,
GstPad * arg0,
GstCaps * arg1,
GValueArray * arg2,
gpointer user_data);
"autoplug-select" : GstAutoplugSelectResult user_function (GstElement * object,
GstPad * arg0,
GstCaps * arg1,
GstElementFactory * arg2,
gpointer user_data);
"autoplug-query" : gboolean user_function (GstElement * object,
GstPad * arg0,
GstElement * arg1,
GstQuery * arg2,
gpointer user_data);
"drained" : void user_function (GstElement * object,
gpointer user_data);
"source-setup" : void user_function (GstElement * object,
GstElement * arg0,
gpointer user_data);
2.2 回调函数
2.2.1 函数定义
在 gst-inspect-1.0
输出中,可以看到 “pad-added” 信号的回调函数原型为:
void user_function (GstElement * object,
GstPad * arg0,
gpointer user_data);
在代码示例中,回调函数被定义为:
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
-
src
参数表示触发信号的 GstElement,在本例中通常为 uridecodebin,因为它是唯一附加了该信号的元素。信号处理函数的第一个参数始终是触发信号的对象。 -
new_pad
参数表示刚刚添加到src
元素的 GstPad。通常,我们希望连接的就是这个 pad。例如,如果输入视频包括视频流和音频流,那么该回调函数将被调用两次:- 第一次,添加的 pad 负责输出视频数据
- 第二次,添加的 pad 负责输出音频数据
-
data
参数是我们在附加信号时提供的指针。在这个例子中,我们使用它来传递 CustomData 指针。
启动 Pipeline 后,当 source 元素从视频文件中获取足够的信息后,它将执行 pad 添加操作,从而触发 “pad-added” 信号并调用我们的回调函数。在回调函数中,我们需要动态调整 Pipeline 的拓扑结构,以便正确处理新添加的 pad。
目前 pipeline 结构如下图,source (uridecodebin)元素并没有与 audioconvert 相连。接下来看回调函数中的逻辑,我们在回调函数中连接 source 元素
2.2.2 获取 pad 信息
GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");
首先从 audioconvert
元素中获取它的 sink pad,这个 pad 是 source 节点想要连接的那个。
gst_element_get_static_pad
是 GStreamer 库中的一个函数,用于获取指定元素的静态 pad。静态 pad 是在元素创建时就已经存在的 pad(与动态创建的 pad 区别开来,例如通过信号 pad-added
生成的 pad)。
函数原型
GstPad* gst_element_get_static_pad(GstElement *element, const gchar *name);
参数
-
element
:- 类型:
GstElement*
- 描述:要获取静态 pad 的元素。
- 类型:
-
name
:- 类型:
const gchar*
- 描述:要获取的静态 pad 的名称。
- 类型:
好的,下面是对你提供的表述进行优化后的版本:
在 GStreamer 中,静态 pad 是在元素创建时就已经存在的 pad,而动态 pad 则是在特定条件下(例如状态切换)才会动态创建的 pad。以 Demuxer 元素为例,动态 pad 通常是在媒体解析过程中,当新的数据流被识别后才创建。
为了查看元素的 pad 信息,可以使用 gst-inspect-1.0
工具。例如,查看 audioconvert
元素的 pad 信息,可以发现它有一个静态的 sink pad 和一个静态的 source pad,如下所示:
Pads:
SINK: 'sink'
Pad Template: 'sink'
SRC: 'src'
Pad Template: 'src'
上面的输出说明 audioconvert
元素在创建时就包含了一个静态的 sink pad 和一个静态的 source pad。也就是说,这些 pad 在元素创建之初就已经存在,并且不会在运行时动态添加或移除。
2.2.3 尝试连接
if (gst_pad_is_linked (sink_pad)) {
g_print ("We are already linked. Ignoring.\n");
goto exit;
}
uridecodebin 创建的 pad 数量取决于视频文件,视频文件中存在多个流,它就会创建多个 source pad,因此回调函数也会被调用多次。因此我们先进行判断,如果 audioconvert 的 sink pad 已经被连接了,那么就不在处理。
2.2.4 获取 pad 的能力
每个元素中的 pad 它的能力是不同的,以 audioconvert 举例说明,使用 gst-inspect-1.0 查看 audioconvert 的信息,你可以看到 Pad Templates 的描述,在 GStreamer 中,Pad Templates
是一种描述元素的 pad 及其能力(caps)的结构。它告诉我们一个元素可以创建什么样的 pad(SINK 或 SRC)以及这些 pad 能处理的数据格式是什么。Pad Templates 可以帮助开发者了解如何将不同的元素连接在一起。
Pad Templates:
SINK template: 'sink'
Availability: Always
Capabilities:
audio/x-raw
format: { (string)F64LE, ..., (string)U8 }
rate: [ 1, 2147483647 ]
channels: [ 1, 2147483647 ]
layout: { (string)interleaved, (string)non-interleaved }
SRC template: 'src'
Availability: Always
Capabilities:
audio/x-raw
format: { (string)F64LE, ..., (string)U8 }
rate: [ 1, 2147483647 ]
channels: [ 1, 2147483647 ]
layout: { (string)interleaved, (string)non-interleaved }
对于 SINK pad:
SINK template: 'sink'
Availability: Always
Capabilities:
audio/x-raw
format: { (string)F64LE, ..., (string)U8 }
rate: [ 1, 2147483647 ]
channels: [ 1, 2147483647 ]
layout: { (string)interleaved, (string)non-interleaved }
解释:
'sink'
是 pad 的名称,表示这是一个 SINK pad(用于接受数据)。Availability: Always
表示这个 pad 总是可用。Capabilities
部分描述了该 pad 支持的所有格式,表示这个 pad 可以处理各种不同格式的原始音频(audio/x-raw
),支持多种音频格式、采样率、通道数和布局。
对于 SRC pad:
SRC template: 'src'
Availability: Always
Capabilities:
audio/x-raw
format: { (string)F64LE, ..., (string)U8 }
rate: [ 1, 2147483647 ]
channels: [ 1, 2147483647 ]
layout: { (string)interleaved, (string)non-interleaved }
解释:
'src'
是 pad 的名称,表示这是一个 SRC pad(用于输出数据)。Availability: Always
表示这个 pad 总是可用。Capabilities
部分描述了该 pad 可以输出的所有格式,类似于 SINK pad,支持多种音频格式、采样率、通道数和布局。
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。我们之前已经创建了一个处理音频的管道(包括 audioconvert、audioresample 和 autoaudiosink),而这个管道无法连接到生成视频的 pad。
使用 gst_pad_get_current_caps()
获取 pad 当前的能力(即输出的数据类型),这些信息被封装在一个 GstCaps
结构中。pad 支持的所有可能能力可以通过 gst_pad_query_caps()
查询。一个 pad 可以提供多种能力,因此 GstCaps
可能包含多个 GstStructure
,每个代表一种能力。而当前能力只包含一个 GstStructure
,或者为 NULL(如果还没有能力)。
在这种情况下,我们知道目标 pad 仅支持一种能力(音频)。因此,我们用 gst_caps_get_structure()
获取第一个 GstStructure
。
接着,使用 gst_structure_get_name()
获取结构的名称,它包含了格式的主要描述(即媒体类型)。
如果名称不是 audio/x-raw
,则表示这不是一个解码后的音频 pad,我们不需要处理它。
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()
尝试链接两个 pad。与 gst_element_link()
的情况一样,链接必须从源 pad 到接收 pad 指定,并且两个 pad 必须由同一 bin(或 pipeline)中的元素拥有。连接后 pipeline 拓扑结构如下图
到这里,我们的工作就完成了!当出现合适类型的 pad 时,它会被链接到剩余的音频处理管道,然后执行将继续直到出现 ERROR 或 EOS。但是,我们将通过引入状态(State)的概念,从这个教程中获得更多的内容。
2.3 GStreamer 状态
在 GStreamer 中,状态(State)是一个非常重要的概念,它控制了元素(Element)和管道(Pipeline)的行为和运行状态。GStreamer 提供了四个主要状态以及一些中间状态变化,它们能够帮助管理多媒体数据流的各个阶段。
2.3.1 四个主要状态
-
NULL
- 描述: 元素的初始状态。此时,元素没有资源分配,没有打开设备,并且不会处理任何数据。
- 用途: 通常用于元素的初始化或重置。
-
READY
- 描述: 元素已经分配了必要的资源,但还没有开始处理数据。在这个状态下,元素已经准备好,但还没有开始进行播放或处理数据。
- 用途: 用于元素的准备阶段,确保资源就绪但没有实际启动数据处理。
-
PAUSED
- 描述: 元素处于暂停状态,已经开始处理数据但没有进行流动。此状态可用于一些需要在处理过程中暂停数据传输的场景。
- 用途: 可以用于预览、缓冲和准备跳转到播放状态。同时,这也是播放停止后的状态,可以在恢复播放时继续从此状态播放。
-
PLAYING
- 描述: 元素正在实时处理和处理数据,数据流动和处理是在进行中的。此状态下,元素将实际进行数据的处理和播放。
- 用途: 元素处于活动状态,进行音视频播放或其他数据处理任务。
2.3.2 状态切换
你只能在相邻的状态之间进行切换,这就是说,你不能直接从 NULL 切换到 PLAYING,你需要先经过中间的 READY 和 PAUSED 状态。然而,如果你将 pipeline 设置为 PLAYING 状态,GStreamer 会为你处理中间的状态转换。
case GST_MESSAGE_STATE_CHANGED:
/* 我们只对来自 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 设为 PLAYING 以开始播放,然后设为 PAUSED 以执行暂停操作,最后在程序退出时设为 NULL 以释放所有资源。
2.4 练习
Base tutorial 3 只能听到声音,添加 autovideosink 和 videoconvert 使其也能看到视频画面。
完成代码参考 basic-tutorial-3-exercise.c
重点代码进行说明
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *video_convert;
GstElement *video_sink;
GstElement *audio_convert;
GstElement *audio_resample;
GstElement *audio_sink;
} CustomData;
在 CustomData
中添加 video_convert
和 video_sink
两个元素
// link audio elements
if (!gst_element_link_many(data.audio_convert, data.audio_resample, data.audio_sink, NULL)) {
g_printerr("Audio Elements could not be linked.\n");
gst_object_unref(data.pipeline);
return -1;
}
// link video elements
if (!gst_element_link_many(data.video_convert, data.video_sink, NULL)) {
g_printerr("Video Elements could not be linked.\n");
gst_object_unref(data.pipeline);
return -1;
}
除了连接音频处理链路外,也连接视频链路上的 convert 和 sink。
static void pad_added_handler(GstElement *src, GstPad *new_pad, CustomData *data) {
GstPad *audio_sink_pad = gst_element_get_static_pad (data->audio_convert, "sink");
GstPad *video_sink_pad = gst_element_get_static_pad (data->video_convert, "sink");
// ....
// if not audio pad type, skip
if (g_str_has_prefix (new_pad_type, "audio/x-raw")) {
// link audio pad
ret = gst_pad_link(new_pad, audio_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);
}
} else
{
// link video pad
ret = gst_pad_link(new_pad, video_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);
}
}
//...
回调函数中,如果创建的 pad 是视频类型,则与 videoconvert 进行连接。Pipeline 的拓扑结构如下图
参考
- Basic tutorial 3: Dynamic pipelines
- 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)