Pipeline构建入门:gst_parse_launch()的使用、手动构建Pipeline、Pad与Caps的概念

好,咱们正式开始动手了。这一章我带你走进 GStreamer 的 Pipeline 世界。说白了,Pipeline 就是一条数据流水线——你把源文件丢进去,经过各种处理,最后输出到屏幕或文件。

构建 Pipeline 有两种方式:一种是偷懒的字符串解析法,另一种是手撸代码的硬核构建法。两种我都用过,各有各的妙处。咱们先看第一种。

一、gst_parse_launch():快速原型的神器

我个人习惯在调试阶段先用 gst_parse_launch()。它接受一个字符串,像写命令行一样描述 Pipeline。GStreamer 内部会帮你解析并创建好所有元素。

举个例子,你想播放一个视频文件:

gst-launch-1.0 filesrc location=test.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink

在代码里,你只需要写:

GstElement *pipeline;
pipeline = gst_parse_launch("filesrc location=test.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink", NULL);

嗯,就这么简单。但这里有个坑——错误处理。我曾经在项目里直接传字符串,结果路径写错了,pipeline 返回 NULL,程序直接崩溃。所以记得加个判断:

if (!pipeline) {
    g_printerr("Pipeline 构建失败,请检查字符串语法\n");
    return -1;
}
避坑指南:我曾经在生产环境里用 gst_parse_launch() 构建复杂 Pipeline,结果字符串里少写了一个感叹号,导致整个链路断掉。调试了整整一下午才发现。所以,复杂场景下我建议用手动构建。

二、手动构建 Pipeline:掌控每一处细节

手动构建虽然代码量多,但你能精确控制每个元素的属性和连接方式。咱们来手动实现上面那个播放 Pipeline。

第一步,创建所有元素:

GstElement *pipeline, *source, *demuxer, *parser, *decoder, *converter, *sink;

pipeline = gst_pipeline_new("my-pipeline");
source   = gst_element_factory_make("filesrc", "file-source");
demuxer  = gst_element_factory_make("qtdemux", "qt-demuxer");
parser   = gst_element_factory_make("h264parse", "h264-parser");
decoder  = gst_element_factory_make("avdec_h264", "h264-decoder");
converter = gst_element_factory_make("videoconvert", "video-converter");
sink     = gst_element_factory_make("autovideosink", "video-sink");

第二步,设置属性。比如 filesrc 需要知道文件路径:

g_object_set(source, "location", "test.mp4", NULL);

第三步,把元素加入 Pipeline:

gst_bin_add_many(GST_BIN(pipeline), source, demuxer, parser, decoder, converter, sink, NULL);

第四步,连接元素。这里有个关键点——qtdemux 是动态 Pad,它要等到解析完文件头才知道输出什么格式。所以不能直接连,得用信号回调:

g_signal_connect(demuxer, "pad-added", G_CALLBACK(on_pad_added), parser);

回调函数里,你拿到 demuxer 新创建的 src pad,然后连接到 parser 的 sink pad:

static void on_pad_added(GstElement *element, GstPad *pad, gpointer data) {
    GstElement *parser = (GstElement *)data;
    GstPad *sink_pad = gst_element_get_static_pad(parser, "sink");
    gst_pad_link(pad, sink_pad);
    gst_object_unref(sink_pad);
}
小技巧:我习惯在回调里打印一下 Pad 的 name 和 caps,方便调试。比如 g_print("新 Pad: %s\n", GST_PAD_NAME(pad));

其他静态连接就简单了:

gst_element_link(parser, decoder);
gst_element_link(decoder, converter);
gst_element_link(converter, sink);

三、Pad 与 Caps:数据流动的桥梁和规则

你想想看,Pipeline 里的元素就像一个个黑盒子,它们之间怎么传递数据?靠的就是 Pad(垫片)。每个元素有输入 Pad(sink pad)和输出 Pad(src pad)。

Pad 分两种:

  • 静态 Pad:元素创建时就固定存在,比如 filesrc 的 src pad、videoconvert 的 sink 和 src pad。
  • 动态 Pad:运行时才出现,比如 qtdemux 的 src pad,要等解析完才知道有几个视频流、音频流。

Caps(Capabilities)又是什么?说白了,它就是数据格式的“身份证”。Caps 描述了 Pad 能处理或产生的媒体类型、分辨率、帧率、编码格式等。

举个例子,一个视频解码器的 sink pad 的 Caps 可能是:

video/x-h264, width=1920, height=1080, framerate=30/1

它的 src pad 的 Caps 则是:

video/x-raw, format=I420, width=1920, height=1080

GStreamer 在连接两个 Pad 时,会检查它们的 Caps 是否兼容。如果不兼容,gst_pad_link() 会返回 GST_PAD_LINK_NOT_NEGOTIATED

核心要点:Pad 是连接点,Caps 是连接规则。两者缺一不可。我在调试一个 4K 视频播放器时,就因为 Caps 里没设置正确的色彩空间,导致画面偏色。后来用 gst_pad_get_current_caps() 打印出来才发现问题。

四、实战:检查 Caps 的小工具

调试时,我经常用这个代码片段来查看某个 Pad 的 Caps:

void print_pad_caps(GstPad *pad) {
    GstCaps *caps = gst_pad_get_current_caps(pad);
    if (caps) {
        gchar *caps_str = gst_caps_to_string(caps);
        g_print("Pad %s 的 Caps: %s\n", GST_PAD_NAME(pad), caps_str);
        g_free(caps_str);
        gst_caps_unref(caps);
    } else {
        g_print("Pad %s 还没有协商好 Caps\n", GST_PAD_NAME(pad));
    }
}

你可以在 Pipeline 运行后,在回调里调用这个函数,看看数据到底长什么样。

五、两种构建方式的对比

对比项 gst_parse_launch() 手动构建
代码量 少,一行搞定 多,每个元素都要创建和连接
灵活性 低,难以处理动态 Pad 高,可以精确控制每个细节
调试难度 字符串错误难排查 可以加断点、打印日志
适用场景 快速原型、简单 Pipeline 生产环境、复杂链路

我个人建议:原型阶段用 gst_parse_launch(),正式代码用手动构建。这样既快又稳。

我的习惯:先在命令行用 gst-launch-1.0 调通整个 Pipeline,确认参数和链路正确。然后把字符串复制到代码里,再逐步替换成手动构建。这样效率最高。

好了,这一章的内容就到这里。你掌握了 Pipeline 的两种构建方式,也理解了 Pad 和 Caps 的核心概念。下一章咱们会深入多线程——Pipeline 跑起来之后,怎么让各个元素并行工作,不卡顿。