GStreamer 示例代码学习记录

前言

本文是笔者阅读 GStreamer 官方文档中的基础教程和播放教程去学习 GStreamer 时的记录,大部分内容可以在 GStreamer 官方文档中找到。

笔者学习 GStreamer 的主要目的是:使用 GStreamer 处理从 TCP Socket 接收到的 H.265 裸流(使用 live555 的 testRTSPclient 程序从 RTSP URL 中获取到),将其解码并保存为 jpeg 图片。

部分教程与目的关系不大,为涉及到。笔者最终放弃使用 GStreamer 的想法,使用 FFmpeg 完成了上述目的。

基础教程1:

媒体流从 source 元素到 sink 元素,中间通过一系列中间元素比如 filter过滤器元素,所有这些的集合称为 pipeline。管道是一种特殊类型的 bin,也是用于包含其他元素的元素。因此,适用于 bin 的所有方法也适用于管道。pipeline 的状态为 GST_STATE_PLAYING 时播放流经的音/视频。

playbin 是一个特殊的元素,它既作为源又作为接收器,是一个完整的流水线。在内部,它创建并连接所有必要的元素来播放媒体,见下例,URI 作为唯一参数传递给playbin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gst_init (&argc, &argv); // 初始化,必须是第一个GStreamer命令
// gst_parse_launch 采用管道的文本表示,并将其转换为实际的管
pipeline = gst_parse_launch (
"playbin uri=https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
NULL);
// 元素相关
// 可以使用gst_element_factory_make()创建新元素。第一个参数是要创建的元素的类型,第二个参数是我们要给这个特定实例的名称。
source = gst_element_factory_make ("videotestsrc", "source");
sink = gst_element_factory_make ("autovideosink", "sink");

// 总线和消息
// 从总线上获得消息msg,本例的调用将阻塞,直到通过该总线接收到ERROR或 EOS (End-Of-Stream)(参数所设置,可设置其他参数)
GstBus *bus = gst_element_get_bus (pipeline); // 得到管道的总线
GstMessage *msg =
gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

// 设置某个元素的状态,每个元素只有一个关联的状态,
gst_element_set_state (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 (pipeline);
return -1;
}

// 清理
gst_message_unref (msg); // 上面获得的总线返回的消息必须由此释放
gst_object_unref (bus); // 上面获得的总线必须由此释放
gst_element_set_state (pipeline, GST_STATE_NULL); // 将管道设置为NULL状态将确保它释放它已分配的任何资源
gst_object_unref (pipeline); // 上面获得的管道取消引用管道将销毁管道及其所有内容。

基础教程2:

videotestsrc 是一个源元素(产生数据),它创建一个测试视频模式。此元素对于调试目的(和教程)很有用,通常在真实的应用程序中找不到。

autovideosink 是一个接收器元素(消耗数据),它在窗口上显示它接收的图像。根据操作系统的不同,存在几个具有不同功能范围的视频接收器。autovideosink 会自动选择并实例化最好的一个,因此不必担心细节,并且代码更加独立于平台。

管道:

GStreamer 中的所有元素通常必须包含在管道中才能使用,因为它负责一些时钟和消息传递功能。我们使用gst_pipeline_new()创建管道。

属性:

GStreamer元素都是一种特殊的GObject,它是提供属性设施的实体。大多数 GStreamer 元素都具有可自定义的属性:命名属性,可以修改这些属性以更改元素的行为(可写属性),或者查询这些属性以了解元素的内部状态(可读属性)。

使用g_object_get()读取属性,使用g_object_set()写入属性。g_object_set() 接受以 NULL 结尾的属性名称、属性值对列表,因此可以一次性更改多个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 将元素添加到管道中(注意强制转换为bin类型)。此函数接受要添加的元素列表,以NULL结尾。可以使用gst_bin_add()添加单个元素。
GstElement *pipeline, *source, *sink; // 元素类型
pipeline = gst_pipeline_new ("test-pipeline"); // 创建管道
gst_bin_add_many (GST_BIN (pipeline), source, sink, NULL);
// gst_element_link (source, sink)将管道中的元素连接起来,需要注意顺序,第一个参数是源,第二个参数是目的地。只有同一bin中的元素才能连接在一起。
if (gst_element_link (source, sink) != TRUE) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (pipeline);
return -1;
}
// 更改了源的pattern属性,该属性控制元素输出的测试视频的类型。
g_object_set (source, "pattern", 0, NULL);

// 获得总线,阻塞等待错误或者结尾消息,与教程1一样
bus = gst_element_get_bus (pipeline);
msg =
gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
// 但这次简称msg可能的其他错误输出:
if (msg != NULL) {
GError *err;
gchar *debug_info;
// GST_MESSAGE_TYPE() 宏可以分析错误类型,gst_message_parse_error() 可以返回一个错误结构和一个对调试有用的字符串。
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);
break;
case GST_MESSAGE_EOS:
g_print ("End-Of-Stream reached.\n");
break;
default:
/* We should not reach here because we only asked for ERRORs and EOS */
g_printerr ("Unexpected message received.\n");
break;
}
gst_message_unref (msg);
}

基础教程3动态管道

pad:GStreamer 元素相互通信的端口称为 pad ( GstPad )。存在 sink pad(数据通过其进入元素)和 source pad(数据通过其退出元素)。自然地,source 元素仅包含 source pad,sink 元素仅包含 sink pad,而 filter 元素包含两者。

多路分配器 demuxer 包含一个 sink pad(多路复用数据通过其到达)和多个 source pads(一个用于在容器中找到的每个流):

demuxer 只有在收到视频流之后,收集到流的信息,才会创建自己的 source pad,因此,不能事先构建 demuxer 之后的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 保存在结构体中便于处理
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *convert;
GstElement *resample;
GstElement *sink;
} CustomData;
/* 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");
// 链接元素转换器、重采样和接收器,但我们不将它们与源链接,因为此时它不包含源焊盘。我们只是让这个分支(转换器+接收器)不链接,直到稍后。
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;
}
// 通过属性设置要播放的文件的 URI
g_object_set (data.source, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm", NULL);

uridecodebin 将在内部实例化所有必要的元素(源、解复用器和解码器),以将 URI 转换为原始音频和/或视频流。它完成了 playbin 一半的工作。由于它包含解复用器,因此它的源焊盘最初不可用,我们需要动态链接到它们。

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

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

autoaudiosink 相当于上一教程中的 autovideosink 。它将音频流渲染到声卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 添加给源(uridecodebin 元素)一个“pad-added”信号,第三个参数提供回调函数,第四个参数是数据指针(CustomData 结构的指针),对数据指针不执行任何操作,只是转发到回调函数便于共享信息。
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

// 回调:src 是触发信号的 GstElement,本例只能是uridecodebin,因为它是我们附加的唯一信号。信号处理程序的第一个参数始终是触发它的对象。
// new_pad 是刚刚添加到 src 元素的 GstPad 。这通常是我们想要链接的pad。
// data 是上面提到的数据指针。
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
// gst_element_get_static_pad () 检索uridercodebin 的 sink pad,之前我们直接连接元素(实际上连接的pad,由 GStreamer 自己完成了),现在我们自己连接 pad
// uridecodebin 可以创建任意数量的 pad,对于每一个 pad,都会调用此回调。一旦我们已经链接,下面的代码行将阻止我们尝试链接到新的 pad。
if (gst_pad_is_linked (sink_pad)) {
g_print ("We are already linked. Ignoring.\n");
goto exit;
}
// 获得pad 当前输出的数据类型,存在 GstCaps 结构中。
new_pad_caps = gst_pad_get_current_caps (new_pad, NULL);
// 本例中我们想要的 pad 只有一个音频,使用下面的函数获得第一个 GstStructure
new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
// 得到结构的名称,其中包含格式的主要描述(实际上是其媒体类型)
// 如果名称不是 audio/x-raw ,则这不是解码的音频,忽略。
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;
}
// gst_pad_link() 尝试链接两个 pad。必须指定从 source 到 sink 的链接,并且两个 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);
}

基础教程4

1
2
msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);

从总线上获取消息的函数,这次加上超时,如果100毫秒没消息,返回 NULL,依次来更新终端显示。

事件用数字乘以 GST_MSECOND 等宏表示。

1
2
3
4
5
6
7
8
if (!gst_element_query_position (data.pipeline, GST_FORMAT_TIME, &current)) {
g_printerr ("Could not query current position.\n");
}
if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
if (!gst_element_query_duration (data.pipeline, GST_FORMAT_TIME, &data.duration)) {
g_printerr ("Could not query current duration.\n");
}
}

gst_element_query_position() 隐藏了查询对象的管理,直接为我们提供结果。直接获得当前流的播放的位置。

gst_element_query_duration()函数获得流的总长度。

GST_TIME_FORMATGST_TIME_ARGS 宏的使用提供用户友好的 GStreamer 时间表示。

1
2
3
4
5
6
if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
g_print ("\nReached 10s, performing seek...\n");
gst_element_seek_simple (data.pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
data.seek_done = TRUE;
}

gst_element_seek_simple() 来“简单地”执行查找。第二个参数表示以时间为单位指定目的地。

第三个参数是GstSeekFlags,本例中的两个选项:

GST_SEEK_FLAG_FLUSH :这会在执行查找之前丢弃当前管道中的所有数据。当管道重新填充并且新数据开始显示时可能会暂停一点,但会大大提高应用程序的“响应能力”。如果未提供此标志,则“陈旧”数据可能会显示一段时间,直到新位置出现在管道末尾。

GST_SEEK_FLAG_KEY_UNIT :对于大多数编码视频流,不可能寻找任意位置,而只能寻找称为关键帧的某些帧。使用此标志时,搜索实际上会移动到最近的关键帧并立即开始生成数据。如果不使用此标志,管道将在内部移动到最近的关键帧(它没有其他选择),但数据在到达请求的位置之前不会显示。最后一种选择更准确,但可能需要更长的时间。

第四个参数提供搜索位置,本例是30秒,即跳转到30秒处继续播放。

1
2
3
4
case GST_MESSAGE_DURATION:
/* The duration has changed, mark the current one as invalid */
data->duration = GST_CLOCK_TIME_NONE;
break;

消息类型为GST_MESSAGE_DURATION时,每当流的总长度发生变化时,该消息就会发布到总线上。这里我们只是将总长度(时间)标记为无效,以便稍后重新查询。

1
2
3
4
5
6
7
8
9
case GST_MESSAGE_STATE_CHANGED: {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->pipeline)) {
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));

/* Remember whether we are in the PLAYING state or not */
data->playing = (new_state == GST_STATE_PLAYING);

消息类型为GST_MESSAGE_STATE_CHANGED时打印流的前后状态,搜索和时间查询通常仅在处于 PAUSEDPLAYING 状态时才能获得有效答复,因为所有元素都有机会接收信息并配置自身。在这里,我们使用 playing 变量来跟踪管道是否处于 PLAYING 状态。另外,如果我们刚刚进入 PLAYING 状态,我们将执行第一个查询。我们询问管道是否允许在此流上查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (data->playing) {
/* We just moved to PLAYING. Check if seeking is possible */
GstQuery *query;
gint64 start, end;
query = gst_query_new_seeking (GST_FORMAT_TIME);
if (gst_element_query (data->pipeline, query)) {
gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
if (data->seek_enabled) {
g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
GST_TIME_ARGS (start), GST_TIME_ARGS (end));
} else {
g_print ("Seeking is DISABLED for this stream.\n");
}
}
else {
g_printerr ("Seeking query failed.");
}
gst_query_unref (query);
}

gst_query_new_seeking() 创建一个“寻求”类型的新查询对象,格式为 GST_FORMAT_TIME 。这表明我们有兴趣通过指定我们想要移动到的新时间来进行搜索。我们还可以请求 GST_FORMAT_BYTES ,然后查找源文件内的特定字节位置,但这通常不太有用。
然后,该查询对象通过 gst_element_query() 传递到管道。结果存储在同一查询中,并且可以使用 gst_query_parse_seeking() 轻松检索。它提取一个布尔值,指示是否允许查找以及可以查找的范围。

基础教程6

Pad 允许信息进入和离开元素,Pad 可以指定 Pad 传输的信息类型,比如“分辨率为 320x200 像素、每秒 30 帧的 RGB 视频”,可以支持多种功能。从 Pad 到 Pad 传输的实际信息必须只有一种明确指定的类型。通过”协商“,两个链接的 Pad 就通用类型达成一致,因此 Pad 的功能变得固定(它们只有一种类型且不包含范围)。

为了将两个元素连接在一起,他们必须有一个公共的功能子集。

Pad 是根据 Pad 模板创建的,该模板指示 Pad 可能具有的所有可能功能。模板可用于创建多个相似的 Pad,并且还允许提前拒绝元素之间的连接:如果其 Pad 模板的功能没有公共子集(它们的交集为空),则无需进一步协商。Pad 模板可以被视为谈判过程的第一步。随着流程的发展,实际的 Pad 会被实例化,其功能也会得到完善,直到它们被修复(或协商失败)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

/* Shows the CURRENT capabilities of the requested pad in the given element */
static void print_pad_capabilities (GstElement *element, gchar *pad_name) {
GstPad *pad = NULL;
GstCaps *caps = NULL;

/* Retrieve pad */
// 从给定元素中获得指定的 Pad。此 Pad 是静态的,因为它始终存在于元素中。
pad = gst_element_get_static_pad (element, pad_name);
if (!pad) {
g_printerr ("Could not retrieve pad '%s'\n", pad_name);
return;
}

/* Retrieve negotiated caps (or acceptable caps if negotiation is not finished yet) */
// 获得 Pad 的当前 Capability,可能有可能无,可能固定可能不固定
caps = gst_pad_get_current_caps (pad);
if (!caps)
// 获得当前可接受的 Pad 功能
caps = gst_pad_query_caps (pad, NULL);

/* Print and free */
// 打印并释放
g_print ("Caps for the %s pad:\n", pad_name);
print_caps (caps, " ");
gst_caps_unref (caps);
gst_object_unref (pad);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 创建元素工厂 */
source_factory = gst_element_factory_find ("audiotestsrc");
sink_factory = gst_element_factory_find ("autoaudiosink");
if (!source_factory || !sink_factory) {
g_printerr ("Not all element factories could be created.\n");
return -1;
}

/* 打印工厂的 Pad 信息 */
print_pad_templates_information (source_factory);
print_pad_templates_information (sink_factory);

/* 让工厂实例化实际的元素 */
source = gst_element_factory_create (source_factory, "source");
sink = gst_element_factory_create (sink_factory, "sink");

在之前的教程中,我们直接使用 gst_element_factory_make() 创建元素且没有谈论工厂,但现在我们用 GstElementFactory 负责实例化特定类型的元素,由其工厂名称标识。

gst_element_factory_find() 创建类型为“videotestsrc”的工厂,然后使用它通过 gst_element_factory_create() 实例化多个“videotestsrc”元素。 gst_element_factory_make() 实际上是 gst_element_factory_find() + gst_element_factory_create() 的快捷方式。

1
2
3
4
5
6
7
8
9
10
11
case GST_MESSAGE_STATE_CHANGED:
/* We are only interested in state-changed messages from the pipeline */
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (pipeline)) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
g_print ("\nPipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
/* Print the current capabilities of the sink element */
print_pad_capabilities (sink, "sink");
}
break;

每次管道状态发生变化时,这都会简单地打印当前的 Pad Caps。您应该在输出中看到初始 Caps(Pad 模板的 Caps)如何逐渐细化,直到完全固定(它们包含没有范围的单一类型)。

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

GStreamer 是一个多线程框架。这意味着,它在内部根据需要创建和销毁线程,例如,将流与应用程序线程解耦。此外,插件还可以自由创建线程用于自己的处理,例如,视频解码器可以创建 4 个线程以充分利用 4 核 CPU。

除此之外,在构建管道时,应用程序可以明确指定分支(管道的一部分)在不同的线程上运行(例如,让音频和视频解码器同时执行)。

这是使用 queue 元素完成的,其工作原理为:接收器 Pad 只是将数据排队并返回控制。在不同的线程上,数据出队并推送到下游。该元素还用于缓冲,如后面的流教程中所示。队列的大小可以通过属性来控制。

此示例构建以下管道:

image-20240712092033912

source 是一个合成音频信号(连续音调),它使用 tee 元素进行分割(它把 sink pad 接收的所有内容用 source pad 发送出去)。然后,一个分支将信号发送到声卡,另一个分支渲染波形视频并将其发送到屏幕。

如图所示,队列创建一个新线程,因此该管道在 3 个线程中运行。具有多个接收器的管道通常需要多线程,因为为了同步,接收器通常会阻塞执行,直到所有其他接收器准备好为止,并且如果只有一个线程,则它们无法准备好,会被第一个接收器阻塞。

在基础教程 3:动态管道中,我们看到一个元素 ( uridecodebin ) 一开始就没有 pad,它们随着数据开始流动并且媒体流经元素而出现。这些称为 Sometimes Pads,与始终可用且称为 Always Pads的常规 pad 形成对比。

第三种 pad 是 Request Pad,它是按需创建的。典型的示例是 tee 元素,它有一个 sink pad 并且没有初始 source pad:需要请求它们,然后 tee 添加它们。通过这种方式,输入流可以被复制任意多次。缺点是使用 Request Pads 链接元素并不像链接 Always Pads 那样自动,如本示例的演练所示。

此外,要在 PLAYINGPAUSED 状态下请求(或释放)Pad,需要采取额外的注意事项(Pad 阻塞),本教程中未对此进行描述。不过,在 NULLREADY 状态下请求(或释放)pad 是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
/* 创建元素 */
// 都是直接使用gst_element_factory_make(),参数是类型和名称
audio_source = gst_element_factory_make ("audiotestsrc", "audio_source");
tee = gst_element_factory_make ("tee", "tee");
audio_queue = gst_element_factory_make ("queue", "audio_queue");
audio_convert = gst_element_factory_make ("audioconvert", "audio_convert");
audio_resample = gst_element_factory_make ("audioresample", "audio_resample");
audio_sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
video_queue = gst_element_factory_make ("queue", "video_queue");
visual = gst_element_factory_make ("wavescope", "visual");
video_convert = gst_element_factory_make ("videoconvert", "video_convert");
video_sink = gst_element_factory_make ("autovideosink", "video_sink");

audiotestsrc 产生合成音。 wavescope 消耗音频信号并呈现波形,就像它是一个示波器一样。我们已经使用过 autoaudiosinkautovideosink

转换元素( audioconvertaudioresamplevideoconvert )对于保证管道可以链接是必需的。事实上,音频和视频接收器的功能取决于硬件,并且您在设计时不知道它们是否与 audiotestsrcwavescope 生成的 Caps 匹配。不过,如果 Caps 匹配,这些元素将以“直通”模式运行,不会修改信号,对性能的影响可以忽略不计。

1
2
3
/* 配置元素 */
g_object_set (audio_source, "freq", 215.0f, NULL);
g_object_set (visual, "shader", 0, "style", 1, NULL);

audiotestsrc 的“freq”属性控制波的频率(215Hz 使波在窗口中看起来几乎静止), wavescope 使波连续。

1
2
3
4
5
6
7
8
9
10
11
12
/* Link all elements that can be automatically linked because they have "Always" pads */
// 放在一个管道中
gst_bin_add_many (GST_BIN (pipeline), audio_source, tee, audio_queue, audio_convert, audio_sink,
video_queue, visual, video_convert, video_sink, NULL);
// 连接所有可以自动连接的元素,如上图所示的三部分
if (gst_element_link_many (audio_source, tee, NULL) != TRUE ||
gst_element_link_many (audio_queue, audio_convert, audio_sink, NULL) != TRUE ||
gst_element_link_many (video_queue, visual, video_convert, video_sink, NULL) != TRUE) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (pipeline);
return -1;
}

此代码块将所有元素添加到管道中,然后链接可以自动链接的元素(带有 Always Pads 的元素)。

gst_element_link_many() 实际上可以将元素与 Request Pads 链接起来。它在内部请求 Pad,因此不必担心链接的元素具有 Always 或者 Request Pads。这实际上很不方便,因为之后您仍然需要释放所请求的 Pad,而且,如果 Pad 是由gst_element_link_many() 自动请求的,会很容易忘记。始终手动请求 Request Pads ,以避免出现麻烦,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 手动连接具有 "Request" pads 的 tee 元素*/
// 用下面的函数请求 tee 的 source pad(Request pad),负责 audio
tee_audio_pad = gst_element_request_pad_simple (tee, "src_%u");
g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
// 用下面的函数请求 tee 的 sink pad(Always Pad),负责 audio
queue_audio_pad = gst_element_get_static_pad (audio_queue, "sink");
// 用下面的函数请求 tee 的 source pad(Request pad),负责 video
tee_video_pad = gst_element_request_pad_simple (tee, "src_%u");
g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
// 用下面的函数请求 tee 的 sink pad(Always Pad),负责 video
queue_video_pad = gst_element_get_static_pad (video_queue, "sink")
// 连接
if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK) {
g_printerr ("Tee could not be linked.\n");
gst_object_unref (pipeline);
return -1;
}
gst_object_unref (queue_audio_pad);
gst_object_unref (queue_video_pad);

要链接 request pad,需要通过向元素“请求”它们来获取它们。一个元素可能能够生成不同类型的请求 pad,因此,在请求它们时,必须提供所需的 pad 模板名称。tee 有两个 pad 模板,分别为“sink”(用于其接收器 pad)和“src_%u”(用于Request pad)。我们使用gst_element_request_pad_simple()从 tee(用于音频和视频分支)请求两个 pad 。

然后,我们从这些请求 Pad 需要链接到的下游元素获取 Pad。这些是普通的 Always Pad,因此我们使用 gst_element_get_static_pad() 获取它们。

最后,我们将 pad 与 gst_pad_link() 链接起来。这是 gst_element_link()gst_element_link_many() 内部使用的函数。

我们获得的sink Pad需要通过 gst_object_unref() 来释放。当我们不再需要它们时,在程序结束时,请求垫将被释放。

然后,我们将管道设置为正常播放,并等待生成错误消息或 EOS。剩下要做的唯一一件事就是清理请求的 Pad:

1
2
3
4
gst_element_release_request_pad (tee, tee_audio_pad);
gst_element_release_request_pad (tee, tee_video_pad);
gst_object_unref (tee_audio_pad);
gst_object_unref (tee_video_pad);

gst_element_release_request_pad()tee 释放 pad,但仍需要使用 gst_object_unref() 取消引用(释放)它。

基础教程8:简化管道

用于将应用程序数据注入 GStreamer 管道的元素是 appsrc ,其对应元素用于将 GStreamer 数据提取回应用程序是 appsinkappsrc 只是一个常规源,由应用程序提供数据(在GStreamer中视为appsrc 提供。 appsink 是一个常规接收器,流经 GStreamer 管道的数据将被消费(实际上由应用程序获得并使用)。

appsrc 可以在多种模式下工作:在拉模式下,它每次需要时都会向应用程序请求数据。在推送模式下,应用程序按照自己的节奏推送数据。此外,在推送模式下,当已经提供了足够的数据时,应用程序可以选择在推送功能中阻塞,或者可以监听 enough-dataneed-data 信号来控制流量。

数据以称为缓冲区(buffers)的块的形式通过 GStreamer 管道。类型是GstBuffer,Source Pad 产生缓冲区,由 Sink Pad 消耗; GStreamer 获取这些缓冲区并将它们从一个元素传递到另一个元素。

缓冲区仅表示一个数据单元,不要假设所有缓冲区都具有相同的大小,或表示相同的时间量。如果单个缓冲区进入一个元素,那么单个缓冲区也不一定会出来。元素可以随意处理接收到的缓冲区。 GstBuffer 也可能包含多个实际内存缓冲区。实际的内存缓冲区是使用 GstMemory 对象抽象出来的,一个 GstBuffer 可以包含多个 GstMemory 对象。

每个缓冲区都附加了时间戳和持续时间,描述了缓冲区内容应在哪个时刻被解码、渲染或显示。例如, filesrc (读取文件的 GStreamer 元素)生成具有“ANY”上限且没有时间戳信息的缓冲区。解复用后(请参阅基础教程 3:动态管道),缓冲区可以具有一些特定的上限,例如“video/x-h264”。解码后,每个缓冲区将包含一个带有原始上限的视频帧(例如“video/x-raw-yuv”)和非常精确的时间戳(指示何时应显示该帧)。

本教程以两种方式扩展了基础教程 7:多线程和 Pad 可用性:首先,将 audiotestsrc 替换为将生成音频数据的 appsrc 。其次,一个新分支被添加到 tee 中,因此进入音频接收器和波形显示的数据也被复制到 appsink 中。 appsink 将信息上传回应用程序,然后应用程序仅通知用户已收到数据,但它显然可以执行更复杂的任务。

image-20240712101109001

创建管道的代码是基础教程 7:多线程和 Pad 可用性的放大版本。它涉及实例化所有元素,将元素与Always Pads 链接,并手动链接 tee 元素的Request Pads。

关于 appsrcappsink 元素的配置:

1
2
3
4
5
6
7
8
/* Configure appsrc */
// 构建一个 GstAudioInfo info,生成 audio_caps并设置到data.app_source
gst_audio_info_set_format (&info, GST_AUDIO_FORMAT_S16, SAMPLE_RATE, 1, NULL);
audio_caps = gst_audio_info_to_caps (&info);
g_object_set (data.app_source, "caps", audio_caps, NULL);
// 连接信号,当源队列需要数据和数据足够时触发回调函数来启动/停止信号生成
g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);

需要在 appsrc 上设置的第一个属性是 caps 。它指定元素将生成的数据类型,因此 GStreamer 可以检查是否可以与下游元素链接(即,下游元素是否能够理解此类数据)。此属性必须是 GstCaps 对象,可以使用 gst_caps_from_string() 轻松地从字符串构建该对象。

然后我们连接到 need-dataenough-data 信号。当 appsrc 的内部数据队列不足或几乎满时,它们会分别被触发。我们将使用这些信号来(分别)启动和停止我们的信号生成过程。

启动管道、等待消息和最终清理都像往常一样完成。让我们回顾一下我们刚刚注册的回调:

1
2
3
4
5
6
7
//当 appsrc 需要数据时,触发下面的回调函数。主循环添加一个空闲处理程序以开始将数据推送到 appsrc
static void start_feed (GstElement *source, guint size, CustomData *data) {
if (data->sourceid == 0) {
g_print ("Start feeding\n");
data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
}
}

appsrc 的内部队列即将耗尽(数据耗尽)时调用此函数。我们在这里做的唯一一件事就是向 g_idle_add() 注册一个 GLib 空闲函数,该函数将数据提供给 appsrc 直到它再次填满。 GLib 空闲函数是 GLib 在“空闲”时(即没有更高优先级的任务要执行时)从其主循环调用的方法。显然,它需要一个 GLib GMainLoop 来实例化并运行。

这只是 appsrc 允许的多种方法之一。特别是,缓冲区不需要使用 GLib 从主线程输入 appsrc ,并且不需要使用 need-dataenough-data 信号来与 appsrc 同步(尽管据称这是最方便的)。

我们记下 g_idle_add() 返回的 sourceid,以便稍后禁用它。

1
2
3
4
5
6
7
8
9
//当 appsrc 有足够的数据时,此回调会触发,我们可以停止发送。
//我们从主循环中删除空闲处理程序
static void stop_feed (GstElement *source, CustomData *data) {
if (data->sourceid != 0) {
g_print ("Stop feeding\n");
g_source_remove (data->sourceid);
data->sourceid = 0;
}
}

appsrc 的内部队列足够满时调用此函数,因此我们停止推送数据。这里我们简单地使用 g_source_remove() 删除空闲函数(空闲函数被实现为 GSource )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 此方法由主循环中的空闲 GSource 调用,将 CHUNK_SIZE 字节输入到 appsrc 中。
* 当 appsrc 请求我们开始发送数据(需要数据信号)时,ide 处理程序会添加到主循环中
* 并在 appsrc 有足够的数据(足够数据信号)时被删除。
*/
static gboolean push_data (CustomData *data) {
GstBuffer *buffer;
GstFlowReturn ret;
GstMapInfo map;
int i;
gint num_samples = CHUNK_SIZE / 2; /* Because each sample is 16 bits */
gfloat freq;

/* 创建一个新的空缓冲区,本例被设置为固定1024字节 */
buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);

/* Set its timestamp and duration */
// CustomData.num_samples 变量计算到目前为止生成的样本数量(即当前时间是多少)
// 因此可以使用 GstBuffer 中的 GST_BUFFER_TIMESTAMP 宏对该缓冲区添加时间戳。
GST_BUFFER_TIMESTAMP (buffer) = gst_util_uint64_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
// 由于我们生成相同大小的缓冲区,因此它们的持续时间相同
// num_samples是缓冲区的样本数量,即16bits
// 使用 GstBuffer 中的 GST_BUFFER_DURATION 设置缓冲区的持续总时间。
GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale (num_samples, GST_SECOND, SAMPLE_RATE);

/* 产生一些迷幻波形,跳过 */
if (gst_buffer_map (buf, &map, GST_MAP_READ)) {
gint16 *raw = (gint16 *) map.data;

/* 在此处创建样本 */

/* 完成后取消映射缓冲区 */
gst_buffer_unmap (buf, &map);
}

这是提供 appsrc 的函数。 GLib 会以我们无法控制的时间和速率调用它,但我们知道,当它的工作完成时(当 appsrc 中的队列已满时),我们将禁用它。

gst_util_uint64_scale() 是一个实用函数,可以缩放(乘法和除法)可能很大的数字,而不必担心溢出。

为了访问缓冲区的内存,您首先必须将其映射到 gst_buffer_map() ,这将为您提供 GstMapInfo 结构内的指针和大小,其中 gst_buffer_map() 将在成功时填充。请小心,不要写入超过缓冲区末尾的内容:您分配了它,因此您知道它的大小(以字节和样本为单位)。

1
2
3
4
5
/* Push the buffer into the appsrc */
g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);

/* Free the buffer now that we are done with it */
gst_buffer_unref (buffer);

请注意,还有 gst_app_src_push_buffer() 作为 gstreamer-app-1.0 库的一部分,与上面的信回调相比,它可能是一个更好的用于将缓冲区推送到 appsrc 的函数,因为它具有适当的键入签名,这样就很难出错。但是,请注意,如果您使用 gst_app_src_push_buffer() ,它将获得传递的缓冲区的所有权,因此在这种情况下,您不必在推送后取消引用它。

准备好缓冲区后,我们将其与 push-buffer 操作信号一起传递给 appsrc (请参阅播放教程 1:Playbin 用法末尾的信息框),然后 gst_buffer_unref() 因为我们不再需要它了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Configure appsink */
// 设置收到新缓冲区时的回调函数,当 sink 收到新缓冲区时调用,
g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
g_signal_connect (data.app_sink, "new-sample", G_CALLBACK (new_sample),
&data);
gst_caps_unref (audio_caps);

/* The appsink has received a buffer */
static GstFlowReturn new_sample (GstElement *sink, CustomData *data) {
GstSample *sample;
/* Retrieve the buffer */
// 使用 pull-sample 操作信号来获得缓冲区 sample,这里仅仅打印提示符
g_signal_emit_by_name (sink, "pull-sample", &sample);
if (sample) {
/* The only thing we do in this example is print a * to indicate a received buffer */
g_print ("*");
gst_sample_unref (sample);
return GST_FLOW_OK;
}
return GST_FLOW_ERROR;
}

appsink 接收缓冲区时调用上面的函数。使用 pull-sample 操作信号来检索缓冲区,然后在屏幕上打印一些指示符。

gst_app_src_pull_sample() 作为 gstreamer-app-1.0 库的一部分,与上面的信号发射相比,它可能是一个更好的用于从 appsink 中提取样本/缓冲区的函数,因为它有一个正确的类型签名,所以很难出错。

为了获取数据指针,我们需要像上面一样使用 gst_buffer_map() ,它将使用指向数据的指针和数据大小(以字节为单位)填充 GstMapInfo 辅助结构。处理完数据后,不要忘记再次 gst_buffer_unmap() 缓冲区。

此缓冲区不必与我们在 push_data 函数中生成的缓冲区匹配,路径中的任何元素都可以以任何方式更改缓冲区(本例中没有:只有一个 teeappsrcappsink 之间的路径中,并且 tee 不会更改缓冲区的内容)。

最后 gst_sample_unref() 检索到的样本,本教程就完成了。

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

感觉用处不大

基础教程10:

无用

基础教程11:调试工具

GStreamer 及其插件充满了调试跟踪,即代码中将特别有趣的信息打印到控制台的位置,以及时间戳、进程、类别、源代码文件、函数和元素信息。调试输出由 GST_DEBUG 环境变量控制。

GStreamer 调试日志非常详细,完全启用会打印很多。设置 GST_DEBUG=2就可以获得 ERRORWARNING 消息。

使用 GST_ERROR()GST_WARNING()GST_INFO()GST_LOG()GST_DEBUG() 宏。它们接受与 printf 相同的参数,并使用 default 类别( default 将在输出日志中显示为“DEBUG”类别,对应值为5)。

获取管道图:

GStreamer 能够输出图形文件。这些是 .dot 文件,可以使用 GraphViz 等免费程序读取,描述管道的拓扑结构以及每个链接中协商的上限。

要获取 .dot 文件,只需将 GST_DEBUG_DUMP_DOT_DIR 环境变量设置为指向要放置文件的文件夹即可。 gst-launch-1.0 将在每次状态更改时创建一个 .dot 文件,因此您可以看到上限协商的演变。取消设置变量以禁用此功能。在您的应用程序中,您可以在方便时使用 GST_DEBUG_BIN_TO_DOT_FILE()GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS() 宏生成 .dot 文件。

基础教程 12:流式传输

感觉用处不大

基础教程13:播放速度

所要做的就是改变播放速率,该变量对于正常播放来说等于 1.0,对于快速模式大于 1.0(绝对值),对于慢速模式小于 1.0(绝对值),对于向前播放为正值,反向播放时为负值。

GStreamer 提供了两种更改播放速率的机制:Step Events 和 Seek Events。除了更改后续播放速率(仅限正值)之外,Step Events 还允许跳过给定数量的媒体。此外,“Seek Events”允许跳转到流中的任何位置并设置正和负播放速率。

Step Events 是更改播放速率的更方便的方法,因为创建它们所需的参数数量减少了;但是,它们有一些缺点,因此本教程中使用 Seek Events。Step Events 仅影响接收器(在管道末端),因此只有当管道的其余部分可以支持以不同的速度运行时,它们才会起作用, Seek Events 会一直通过管道,因此每个元素都可以对它们做出反应。 Step Events 事件的优点是行动速度更快。步骤事件也无法改变播放方向。

要使用这些事件,需要创建它们,然后将其传递到管道,它们在管道中向上游传播,直到到达可以处理它们的元素。如果一个事件被传递到像 playbin 这样的 bin 元素上,它只会将事件提供给它的所有接收器,这将导致执行多次查找。常见的方法是通过 video-sinkaudio-sink 属性检索 playbin 的接收器之一,并将事件直接送入接收器。

帧步进是一种允许逐帧播放视频的技术。它是通过暂停管道,然后发送 Step Events 每次跳过一帧来实现的。

main 函数中的初始化代码没有什么新内容:实例化了 playbin 管道,安装了 I/O 监视器来跟踪击键,并执行了 GLib 主循环。然后,在键盘处理函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Process keyboard input */
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data) {
gchar *str = NULL;

if (g_io_channel_read_line (source, &str, NULL, NULL, NULL) != G_IO_STATUS_NORMAL) {
return TRUE;
}

switch (g_ascii_tolower (str[0])) {
case 'p':
data->playing = !data->playing;
gst_element_set_state (data->pipeline, data->playing ? GST_STATE_PLAYING : GST_STATE_PAUSED);
g_print ("Setting state to %s\n", data->playing ? "PLAYING" : "PAUSE");
break;

与之前的教程一样,暂停/播放切换是通过 gst_element_set_state() 处理的。

1
2
3
4
5
6
7
8
9
10
11
12
case 's':
if (g_ascii_isupper (str[0])) {
data->rate *= 2.0;
} else {
data->rate /= 2.0;
}
send_seek_event (data);
break;
case 'd':
data->rate *= -1.0;
send_seek_event (data);
break;

使用“S”和“s”将当前播放速率加倍或减半,使用“d”反转当前播放方向。在这两种情况下,都会更新 rate 变量并调用 send_seek_event 。我们来回顾一下这个函数。

1
2
3
4
5
6
7
8
9
10
/* Send seek event to change rate */
static void send_seek_event (CustomData *data) {
gint64 position;
GstEvent *seek_event;

/* Obtain the current position, needed for the seek event */
if (!gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position)) {
g_printerr ("Unable to retrieve current position.\n");
return;
}

该函数创建一个新的 Seek Event 并将其发送到管道以更新速率。首先,使用 gst_element_query_position() 恢复当前位置。这是必需的,因为 Seek Events 跳转到流中的另一个位置,并且由于我们实际上不想移动,所以我们跳转到当前位置。使用 Step Events 会更简单,但该事件目前尚未完全发挥作用,如简介中所述。

1
2
3
4
5
6
7
8
/* Create the seek event */
if (data->rate > 0) {
seek_event = gst_event_new_seek (data->rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH
| GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_END, 0);
} else {
seek_event = gst_event_new_seek (data->rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH
| GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, position);
}

Seek 事件是使用 gst_event_new_seek() 创建的。它的参数基本上是新的速率、新的开始位置和新的停止位置。无论哪种播放方向,起始位置都必须小于停止位置,因此两个播放方向要区别对待。

1
2
3
4
if (data->video_sink == NULL) {
/* If we have not done so, obtain the sink through which we will send the seek events */
g_object_get (data->pipeline, "video-sink", &data->video_sink, NULL);
}

新的事件最终通过 gst_element_send_event() 发送到选定的接收器。

回到键盘处理程序及,看一下帧步进代码,这非常简单:

1
2
3
4
5
6
7
8
9
10
case 'n':
if (data->video_sink == NULL) {
/* If we have not done so, obtain the sink through which we will send the step events */
g_object_get (data->pipeline, "video-sink", &data->video_sink, NULL);
}

gst_element_send_event (data->video_sink,
gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS (data->rate), TRUE, FALSE));
g_print ("Stepping one frame\n");
break;

使用 gst_event_new_step() 创建一个新的步进事件,其参数基本上指定要跳过的量(示例中为 1 帧)和新速率(我们不更改)。

视频接收器是从 playbin 获取的,以防万一我们还没有,就像以前一样。

这样我们就完成了。测试本教程时,请记住,向后播放在许多元素中都不是最佳的。

基础教程 14:方便的元素:

都是用在 gst-launch-1.0 工具中的参数,其中部分:

uridecodebin

该元素将数据从 URI 解码为原始媒体。它选择一个可以处理给定 URI 方案的源元素并将其连接到 decodebin 元素。它的作用就像一个解复用器,因此它提供与媒体中找到的流一样多的源 pad。

decodebin

该元素通过自动插入使用可用的解码器和解复用器自动构建解码管道,直到获得原始媒体。它由 uridecodebin 内部使用,通常使用起来更方便,因为它也创建了合适的源元素。它取代了旧的 decodebin 元素。它的作用就像一个解复用器,因此它提供与媒体中找到的流一样多的源 pad。

videoconvert

元素从一种颜色空间(例如 RGB)转换为另一种颜色空间(例如 YUV)。它还可以在不同的 YUV 格式(例如 I420、NV12、YUY2 …)或 RGB 格式排列(例如 RGBA、ARGB、BGRA …)之间进行转换。这通常是解决谈判问题时的首选。当不需要时,由于其上游和下游元素已经可以相互理解,因此它以直通模式运行,对性能的影响最小。根据经验,每当您使用大写字母在设计时未知的元素(如 autovideosink )或可能因外部因素(如解码用户)而变化时,请始终使用 videoconvert 。提供的文件。

videorate

该元素接受带有时间戳的视频帧的传入流,并生成与源板的帧速率匹配的流。校正是通过丢弃和复制帧来执行的,没有使用花哨的算法来插值帧。这对于允许需要不同帧速率的元素进行链接非常有用。与其他适配器一样,如果不需要(因为两个 Pad 都可以同意帧速率),它会以直通模式运行并且不会影响性能。

videoscale

该元素调整视频帧的大小。默认情况下,元素会尝试在源焊盘和接收焊盘上协商到相同的大小,以便不需要缩放。因此,如果不需要扩展,则可以安全地将此元素插入管道中以获得更稳健的行为,而无需任何成本。该元素支持广泛的色彩空间,包括各种 YUV 和 RGB 格式,因此通常能够在管道中的任何位置运行。如果要将视频输出到大小由用户控制的窗口,则最好使用 videoscale 元素,因为并非所有视频接收器都能够执行缩放操作。

播放教程3:缩短管道

基础教程 8:缩短管道展示了应用程序如何使用名为 appsrc 和 appsink 的两个特殊元素手动提取数据或将数据注入到管道中。playbin 也允许使用这些元素,但连接它们的方法不同。要将 appsink 连接到 playbin ,请参阅播放教程 7:自定义 playbin 接收器。本教程展示 如何将 appsrcplaybin 连接并配置appsrc

要使用 appsrc 作为管道源,只需实例化 playbin 并将其 URI 设置为 appsrc://

1
2
/* Create the playbin element */
data.pipeline = gst_parse_launch ("playbin uri=appsrc://", NULL);

playbin 将创建一个内部 appsrc 元素并触发 source-setup 信号以允许应用程序对其进行配置:

1
g_signal_connect (data.pipeline, "source-setup", G_CALLBACK (source_setup), &data);

设置 appsrccaps 属性非常重要,因为一旦信号处理程序返回, playbin 将根据这些上限实例化管道中的下一个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 当 playbin 创建 appsrc 元素时会调用此函数,因此我们有机会对其进行配置。*/
static void source_setup (GstElement *pipeline, GstElement *source, CustomData *data) {
GstAudioInfo info;
GstCaps *audio_caps;

g_print ("Source has been created. Configuring.\n");
data->app_source = source;

/* Configure appsrc */
gst_audio_info_set_format (&info, GST_AUDIO_FORMAT_S16, SAMPLE_RATE, 1, NULL);
audio_caps = gst_audio_info_to_caps (&info);
g_object_set (source, "caps", audio_caps, "format", GST_FORMAT_TIME, NULL);
g_signal_connect (source, "need-data", G_CALLBACK (start_feed), data);
g_signal_connect (source, "enough-data", G_CALLBACK (stop_feed), data);
gst_caps_unref (audio_caps);
}

appsrc 的配置与基础教程 8:缩短管道完全相同:将 caps 设置为 audio/x-raw ,并注册了两个回调,因此元素可以告诉应用程序何时需要启动和停止推送数据。有关更多详细信息,请参阅基础教程 8:缩短管道。

从这一点开始, playbin 负责处理管道的其余部分,应用程序只需要担心在被告知时生成更多数据。

要了解如何使用 appsink 元素从 playbin 中提取数据,请参阅播放教程 7:自定义 playbin 接收器。

播放教程 7:自定义 playbin 接收器

playbin 可以通过手动选择其音频和视频接收器来进一步自定义。这允许应用程序依赖 playbin 来检索和解码媒体,然后自行管理最终的渲染/显示。

playbin 的两个属性允许选择所需的音频和视频接收器: audio-sinkvideo-sink (分别)。应用程序只需要实例化适当的 GstElement 并通过这些属性将其传递给 playbin 。但是,此方法只允许使用单个元素作为接收器。如果需要更复杂的管道,例如均衡器加音频接收器,则需要将其包装在 Bin 中,因此它看起来 playbin 就好像它是单个 Element 一样。

Bin ( GstBin ) 是一个封装部分管道的容器,因此可以将它们作为单个元素进行管理。例如,我们在所有教程中使用的 GstPipelineGstBin 的类型,它不与外部元素交互。 Bin 内的元素通过 Ghost Pad ( GstGhostPad ) 连接到外部元素,这是 Bin 表面上的 Pad,它只是将数据从外部 Pad 转发到内部元素上的给定 Pad。

GstBin 也是 GstElement 的一种类型,因此它们可以在需要 Element 的任何地方使用,特别是作为 playbin 的接收器(然后它们被称为水槽)。

image-20240712144952972

图 1:带有两个 Elements 和一个 Ghost Pad 的 Bin。

GstBin 也是 GstElement 的一种类型,因此它们可以在需要 Element 的任何地方使用,特别是作为 playbin 的接收器(然后它们被称为水槽)。

1
2
3
4
5
6
7
8
/* Create the elements inside the sink bin */
equalizer = gst_element_factory_make ("equalizer-3bands", "equalizer");
convert = gst_element_factory_make ("audioconvert", "convert");
sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
if (!equalizer || !convert || !sink) {
g_printerr ("Not all elements could be created.\n");
return -1;
}

组成的 sink-bin 的所有元素都被实例化。我们使用 equalizer-3bandsautoaudiosink ,中间有 audioconvert ,因为我们不确定音频接收器的功能(因为它们是硬件 -依赖)。

1
2
3
4
/* Create the sink bin, add the elements and link them */
bin = gst_bin_new ("audio_sink_bin");
gst_bin_add_many (GST_BIN (bin), equalizer, convert, sink, NULL);
gst_element_link_many (equalizer, convert, sink, NULL);

这会将新元素添加到 Bin 中并将它们链接起来,就像我们在管道中所做的那样。

1
2
3
4
5
6
7
8
9
// 获取bin的第一个元素 equalize r的 sink pad
pad = gst_element_get_static_pad (equalizer, "sink");
// 用上面获取的pad new 一个 ghost_pad 并启用
ghost_pad = gst_ghost_pad_new ("sink", pad);
gst_pad_set_active (ghost_pad, TRUE);
// 为 bin 添加 ghost_pad
gst_element_add_pad (bin, ghost_pad);
// 释放旧 pad,ghost_pad 所有权在 bin
gst_object_unref (pad);

现在我们需要创建一个 Ghost Pad,以便 Bin 内的部分管道可以连接到外部。该 Ghost Pad 将连接到内部 Elements 之一的 Pad(均衡器的接收器 pad),因此我们使用 gst_element_get_static_pad() 检索该 Pad。请记住基础教程 7:多线程和 Pad 可用性,如果这是 Request Pad 而不是 Always Pad,我们将需要使用 gst_element_request_pad()

Ghost Pad 使用 gst_ghost_pad_new() 创建(指向我们刚刚获取的内部 Pad),并使用 gst_pad_set_active() 激活。然后使用 gst_element_add_pad() 将其添加到 Bin,将 Ghost Pad 的所有权转移到 bin,因此我们不必担心释放它。

最后,我们从均衡器 equalizer 获得的 sink Pad 需要用 gst_object_unref() 释放。

此时,我们有了一个功能性的 sink-bin,我们可以将其用作 playbin 中的音频接收器。我们只需要指示 playbin 使用它:

1
2
/* Set playbin's audio sink to be our sink bin */
g_object_set (GST_OBJECT (pipeline), "audio-sink", bin, NULL);

就像将 playbin 上的 audio-sink 属性设置为新创建的接收器一样简单。

1
2
3
/* Configure the equalizer */
g_object_set (G_OBJECT (equalizer), "band1", (gdouble)-24.0, NULL);
g_object_set (G_OBJECT (equalizer), "band2", (gdouble)-24.0, NULL);

剩下的唯一一点就是配置均衡器。对于此示例,两个较高频段被设置为最大衰减,因此低音得到增强。稍微调整一下这些值以感受差异(请参阅 equalizer-3bands 元素的文档以了解允许的值范围)。

gst_app_src_push_buffer

1
2
3
GstFlowReturn
gst_app_src_push_buffer (GstAppSrc * appsrc,
GstBuffer * buffer)

将缓冲区添加到缓冲区队列中,appsrc 元素将其推送到其源焊盘。该函数取得缓冲区的所有权。
当 block 属性为 TRUE 时,此函数可以阻塞,直到队列中出现可用空间。

gst_app_src_get_stream_type

1
2
GstAppStreamType
gst_app_src_get_stream_type (GstAppSrc * appsrc)

获取流类型。使用 gst_app_src_set_stream_type 控制 appsrc 的流类型。

参数:

appsrc – a GstAppSrc

返回流类型。

gst_app_src_get_size

1
2
gint64
gst_app_src_get_size (GstAppSrc * appsrc)

获取流的大小(以字节为单位)。值 -1 表示大小未知。

参数:

appsrc – a GstAppSrc

返回 – 之前使用 gst_app_src_set_size 设置的流的大小;

1
2
3
gcc -g src/server.cc -o server `pkg-config --cflags --libs gstreamer-1.0 gstreamer-video-1.0` -lstdc++ -lpthread -lstdc++ -lstdc++fs -lstdc++ -lgstapp-1.0 -lgstvideo-1.0 -lgstbase-1.0

gcc -g src/extract_frame.c -o extract_frame `pkg-config --cflags --libs gstreamer-1.0`