☰
GStreamer开发核心:彻底理解Pads与Capabilities
2026/10/24 4:35:21 网站建设 项目流程

构建GStreamer应用绕不开的核心:把Pads和Capabilities彻底吃透

做GStreamer开发,无论你是写一个简单的播放器、做视频推流,还是搞复杂的图像处理管线,最后都会撞上两个绕不开的概念:Pads(很多中文资料里直译成“衬垫”)和Capabilities(通常叫caps)。我在刚开始接触GStreamer时,只是照着手册把管道拼起来,觉得“能跑就行”,结果一到动态场景、多路拼接或者格式协商出现问题,就完全无从下手。后来反复折腾、看源码、抓调试日志,才真正理解这两个概念是整个框架的骨架。这篇内容就围绕Pads和Capabilities展开,讲清楚它们是什么、怎么工作、在应用代码里怎么操作、以及最常遇到的坑。无论你是刚接触GStreamer的初学者,还是已经写过一些管道但一直没吃透原理的开发者,这篇都能给你一个完整可用的参考。

1. 先搞清楚:Pads在GStreamer里究竟扮演什么角色

1.1 从“USB接口”理解src pad和sink pad

我有个百试不爽的类比:把每个GStreamer元素(element)想象成一个外接设备,比如硬盘、U盘、打印机。设备上一定有接口,用来跟外界交换数据。在GStreamer里,这个接口就是Pad。

但Pad分方向。数据从src pad“流出去”,从sink pad“流进来”。举个最直观的例子:

  • filesrc负责从文件读数据,它只有一个src pad,数据从这里冒出来。
  • fakesink只接收数据然后丢弃,它只有一个sink pad。
  • 像videoconvert、x264enc这种处理类元素,既要有sink pad接收上游数据,又要有src pad向下游输出处理后的结果。

所以建立管道时,本质就是把你手里各个元素的src pad和下一个元素的sink pad连接起来,让数据像水流一样一级一级往下走。这个连接的动作,GStreamer也提供了快捷方式:gst_element_link()可以自动找到合适的pad完成连接。

Pad的命名也很有规律。绝大多数元素的sink pad就叫“sink”,src pad就叫“src”,多输入输出场景下会带上编号或后缀,比如src_0、src_1,sink_0、sink_1。这些名称在代码里使用gst_element_get_static_pad(element, "src")这样获取时用得到。

1.2 Always、Sometimes、Request:Pad的三种存在方式

光知道有src和sink还不够。实际开发中你会碰到一种情况:用gst_element_get_static_pad()获取某个pad时返回NULL,怎么找都找不到。这是因为Pad不是“时时刻刻”都存在的。GStreamer把Pad分为三种类型:

  • Always Pad:元素只要存在,这个pad就存在。比如videoconvert的sink和src,无论管道处于什么状态,你都能拿到这两个pad。
  • Sometimes Pad:这种pad要根据媒体数据的内容才会出现。最典型的是各种分离器(demuxer),比如qtdemux解析MP4文件时,一开始并不知道文件里有几路视频、几路音频,只有解析到相应轨道后,才会动态创建src pad。如果你在管道刚创建时就急着去拿audio_0这个pad,一定拿不到。
  • Request Pad:这种pad需要你主动“请求”,而不是直接获取。最典型的例子是tee,你可以随便要任意多个src pad,数据从sink进去,从所有请求得到的src pad各复制一份出来。muxer(比如matroskamux)也类似,它本身没有固定的sink pad,需要为每一路要封装的流请求一个。

很多人第一次写动态管道时栽跟头,就是因为没分清这三种pad。记住:如果是Sometimes pad,用“pad-added”信号去监听;如果是Request pad,用gst_element_request_pad_simple()去拿,用完还要想着释放(gst_element_release_request_pad())。

1.3 Pad连接的基本规则

Pad之间不是随意能连的,实际编码时会遇到编译通过、运行却报错的情况,根源往往就是违反了几条硬规则:

  1. 只能src连sink,不能src连src,也不能sink连sink。这就像电源插头只能插插座,“两个插头互相插”显然不成立。
  2. 同一个元素的pad不能互相连接。比如你不能把videoconvert的src pad连回它自己的sink pad,这样会形成环形依赖,管道直接起不来。
  3. 连接时双方必须协商好数据格式。这个协商就是靠Capabilities完成的。如果两个pad支持的格式完全没有交集,连接时GStreamer会直接给你一个“cannot link”的错误。

关于第3条,我后面专门讲协商过程,这里先记住结论:连接看似是一个简单的动作,背后其实还有格式匹配的“契约”。

2. Capabilities:媒体数据的“格式说明书”

2.1 Caps的结构长什么样

Capabilities,简称caps,描述的是“一段数据是什么格式”。它是GStreamer协商机制的核心。caps以GstCaps对象存在,里面由一个或多个GstStructure组成。每个GstStructure包含一个媒体类型(media type)和若干键值对属性。

比如一个非常常见的视频格式描述:

video/x-raw, format=(string)I420, width=(int)1920, height=(int)1080, framerate=(fraction)30/1

拆开看:

  • video/x-raw是媒体类型,表示“未压缩的原始视频”。
  • format=I420表示像素格式是I420(YUV420 planar)。
  • width=1920, height=1080是分辨率。
  • framerate=30/1是帧率,注意这里是个分数结构,GStreamer里很多数值用分数表达,为的是精确。

再看音频的例子:

audio/x-raw, format=(string)S16LE, rate=(int)44100, channels=(int)2, layout=(string)interleaved

这表示“16位有符号整型、小端序、44100Hz采样率、双声道交错布局的裸音频”。

你还会看到很多压缩格式的caps,比如:

video/x-h264, stream-format=(string)avc, alignment=(string)au, profile=(string)high

压缩格式的property往往更少,因为编码细节由码流本身承载,caps只需要描述关键的容器信息和档次。

这里有个必须理解的细节:一个GstCaps可以包含多个GstStructure。什么意思?表示这一段数据“支持这些格式中的任意一种”,具体用哪种,在协商时再定。比如videotestsrc的src pad模板可能声明支持几十种像素格式,它对应的caps就是一个大列表。

2.2 Capabilities的获取与理解

开发中最频繁的操作之一就是“看一个元素支持什么格式”。GStreamer给了一个命令行工具,非常方便:

gst-inspect-1.0 videotestsrc

输出里有一块“Pad Templates”,会清楚列出这个元素的sink和src pad模板。模板里有 “ALLOWED CAPS” 一栏,那就是这个pad能处理的所有格式。你在写代码前,用gst-inspect-1.0把上下游元素的caps模板都扫一遍,就能大致预判两个pad能不能连上。

运行时想要看真实的caps,可以用:

gst-launch-1.0 -v videotestsrc ! videoconvert ! autovideosink

-v参数会让GStreamer把所有caps协商的内容打到命令行,方便你看最终的协商结果。我调试格式问题时,通常也会开调试日志:

GST_DEBUG=gst_caps:5,gst_pad:5 gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080 ! fakesink

其中gst_caps:5、gst_pad:5会把caps的创建、复制、交集运算和pad链接时协商的详细信息全部打印出来。遇到“协商失败”或者“格式不对”的报错,这套调试命令是我第一个搬出来的工具。

2.3 代码里操作GstCaps

在应用程序里,你经常会需要构造一个caps来限制格式。最常用的做法是用gst_caps_from_string():

GstCaps *caps = gst_caps_from_string("video/x-raw, width=(int)640, height=(int)480");

这样很方便地把字符串转成GstCaps对象。如果是自己逐个字段构造,也可以用gst_caps_new_simple()和gst_structure_set():

GstCaps *caps = gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, "I420", "width", G_TYPE_INT, 640, "height", G_TYPE_INT, 480, NULL);

再用完了要gst_caps_unref()。不过实际生产代码里,我喜欢直接用gst_caps_from_string,因为可读性好,字符串一目了然,维护起来也方便。

还有一个几乎所有GStreamer应用都要用到的元素:capsfilter。它的作用很单纯:把管道中某个位置的caps固定住。用法很简单,比如强制视频分辨率:

gst-launch-1.0 videotestsrc ! video/x-raw,width=1280,height=720 ! videoconvert ! autovideosink

这里video/x-raw,...实际上隐式创建了一个capsfilter。在应用代码里,你也能显式创建capsfilter元素并设置它的caps属性来固定格式。这个工具在协商问题排查时是“救命的”,它可以切断上游给下游提供过多可能性时造成的“选择困难”。

3. Caps协商:数据流的“谈判桌”

3.1 协商是怎么发生的

管道连接不是“接上线”就完事。真正开始传输数据前,上下游必须坐下来谈判,确定一个双方都接受的格式。这个过程就是caps negotiation(协商)。

GStreamer的协商方向很特别:数据从上游流向下游,但协商往往是从下游往上游反馈的。GStreamer里有几个关键概念:

  • gst_pad_query_caps():查询某个pad当前支持的caps。
  • gst_pad_get_current_caps():获取当前已经协商好的caps。
  • gst_pad_get_allowed_caps():获取pad当前允许的caps范围。

协商过程的通俗描述:上游src pad提出“我能输出这些格式”,下游sink pad检查“这些格式里我能接受哪些”,最终取一个交集。如果交集为空,报错“could not negotiate format”。

实际协商还会因为pad是否固定(fixated)而不同:

  • 固定caps:比如你用capsfilter强制指定了width=1920,height=1080,那协商时就只能在这一种格式上达成一致。
  • 非固定caps:比如只指定video/x-raw,那协商空间就是所有裸视频格式,具体哪一个由双方在运行时确定。

协商通常发生在管道状态从READY向PAUSED推进时,也就是 preroll 阶段。你经常会看到,管道起不来、卡在PAUSED状态,日志提示协商失败,这正是因为数据还没开始流动,格式就先谈崩了。

3.2 理解Pad Templates和Pad Caps的区别

这里很容易混淆。Pad Template是元素的“能力声明”,描述“我理论上支持哪些格式”,在元素创建时就是静态的。而Pad当前的caps是动态的,会随着协商结果改变。

看一个例子:videoconvert的sink pad模板支持的格式可能很多,但一旦管道协商确定为video/x-raw,format=I420,那么sink pad的current caps就固定为这一种格式。此后数据必须按这个格式流入。

这个区别在调试时非常重要。当你用gst_pad_query_caps()查询一个pad时,得到的是它能接受的所有格式;而用gst_pad_get_current_caps()得到的是当前实际协商好的格式。很多人拿当前caps和另一端的allowed caps去做比较,结果白白绕了远路。

3.3 手动协商会用到哪些API

在应用里,有些场景需要你主动去操作协商。最典型的:动态创建元素后手动连接、手动设置caps、以及在多个候选格式里替上下游做决定。

我会用下面这几个API的组合:

// 查询下游sink pad允许的caps GstCaps *sinkcaps = gst_pad_query_caps(sinkpad, NULL); // 查询上游src pad允许的caps GstCaps *srccaps = gst_pad_query_caps(srcpad, NULL); // 计算两者交集 GstCaps *intersection = gst_caps_intersect(srccaps, sinkcaps);

gst_caps_intersect()会算出两个caps的重合部分。如果结果是空的,那这两个pad根本连不上。在我写的工具里,这是判断能否连接的第一步。

如果交集有多个候选格式,你可以自己帮它选一个。比如我想要一个确定的格式,可以把交集和目标的caps再做一次intersect:

GstCaps *filter = gst_caps_from_string("video/x-raw, format=(string)I420"); GstCaps *result = gst_caps_intersect(intersection, filter); if (!gst_caps_is_empty(result)) { // 用result来设置srcpad或capsfilter }

这种手动选择候选格式的方法,在编写动态分流、录制多路流时极其常用。

4. 在应用程序里操作Pads和Caps的实战

4.1 获取元素上的Pad

最基础的操作是拿某个元素上指定的pad。对于Always pad:

GstPad *pad = gst_element_get_static_pad(element, "src");

如果pad是sometimes类型的,那就需要监听“pad-added”信号。例如处理qtdemux或者uridecodebin:

static void on_pad_added(GstElement *element, GstPad *pad, gpointer user_data) { GstElement *convert = (GstElement *)user_data; GstPad *sinkpad = gst_element_get_static_pad(convert, "sink"); if (gst_pad_link(pad, sinkpad) != GST_PAD_LINK_OK) { // 处理错误 } gst_object_unref(sinkpad); }

如果元素可能有很多pad,也可以用gst_element_iterate_pads()遍历所有pad:

GstIterator *it = gst_element_iterate_pads(element); GValue item = G_VALUE_INIT; while (gst_iterator_next(it, &item) == GST_ITERATOR_OK) { GstPad *pad = g_value_get_object(&item); // 处理pad g_value_reset(&item); } gst_iterator_free(it);

对于request pad,用gst_element_request_pad_simple():

GstPad *teepad = gst_element_request_pad_simple(tee, "src_%u");

注意这里传递的是pad模板名字。你可以从gst-inspect-1.0 tee看到它的模板名叫“src_%u”。

4.2 查询与设置Caps

查询某个pad当前的真实caps:

GstCaps *caps = gst_pad_get_current_caps(pad); if (caps) { gchar *caps_string = gst_caps_to_string(caps); g_print("Current caps: %s\n", caps_string); g_free(caps_string); gst_caps_unref(caps); }

想让某个元素以特定格式运行,可以用capsfilter。在代码里创建capsfilter的方式:

GstElement *filter = gst_element_factory_make("capsfilter", NULL); GstCaps *caps = gst_caps_from_string("video/x-raw, format=(string)I420, width=(int)1280, height=(int)720"); g_object_set(filter, "caps", caps, NULL); gst_caps_unref(caps);

还有一种更直接的方式,对支持设置的元素属性直接设置,比如某些编码器支持bitrate属性,但格式本身的指定还是要靠capsfilter。

4.3 手动连接两个pad

自动连接元素用gst_element_link(),它内部会找到“最合适”的pad来连接。但在动态场景里,我们经常需要手动指定某个src pad连到某个sink pad:

GstPad *srcpad = gst_element_get_static_pad(element_a, "src_1"); GstPad *sinkpad = gst_element_get_static_pad(element_b, "sink"); if (gst_pad_link(srcpad, sinkpad) == GST_PAD_LINK_OK) { // 连接成功 } gst_object_unref(srcpad); gst_object_unref(sinkpad);

还有一种更优雅的写法,直接用pad名字连接两个元素:

gst_element_link_pads(element_a, "src_1", element_b, "sink");

上面这句相当于“让element_a的src_1和element_b的sink连接”,少写了好几行代码。

还有一点值得注意:gst_pad_link()返回GstPadLinkReturn,常见的有GST_PAD_LINK_OK、GST_PAD_LINK_WRONG_HIERARCHY、GST_PAD_LINK_WAS_LINKED、GST_PAD_LINK_NOCOMPAT等。代码里一定要检查返回值,尤其是NOCOMPAT,它明确告诉你“caps不兼容”,管道起不来时排查方向就清晰了。

4.4 Pad Probes:给数据流装一个“探测器”

Pad probe是GStreamer应用开发里非常实用、甚至可以说是杀手级的能力。你可以把它理解成在数据流过pad时挂一个钩子,能监听到、甚至拦截或修改buffer。

我自己常用的场景是:

  • 统计管道里的帧率、码率。
  • 选择性丢帧(比如某些时间段不要视频数据)。
  • 在固定时间点向管道注入或替换数据。
  • 在动态管道里,通过blocking probe暂停数据流,等待重新配置。

probe的类型有这么几种:

  • GST_PAD_PROBE_TYPE_BUFFER:监听数据buffer。
  • GST_PAD_PROBE_TYPE_BUFFER_LIST:监听buffer list(批量buffer)。
  • GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM/GST_PAD_PROBE_TYPE_EVENT_UPSTREAM:监听流事件。
  • GST_PAD_PROBE_TYPE_BLOCK和GST_PAD_PROBE_TYPE_BLOCKING:阻塞数据流。

添加一个buffer probe的示例:

static GstPadProbeReturn on_buffer_probe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data) { // 可选操作 return GST_PAD_PROBE_OK; } guint probe_id = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, on_buffer_probe, NULL, NULL);

回调返回值的含义很关键:

  • GST_PAD_PROBE_OK:放行数据。
  • GST_PAD_PROBE_DROP:丢弃数据。
  • GST_PAD_PROBE_REMOVE:移除probe并放行。
  • GST_PAD_PROBE_PASS:对于blocking probe来说,强制通过并且忽略block状态。

我第一次用DROP时栽过跟头:在videotestsrc后面丢掉所有视频buffer,但是下游的视频sink依然收不到数据而卡住preroll。所以如果你只想“看”不想“丢”,就用OK;想丢弃时,要考虑下游是否依赖这些buffer来推进状态。

4.5 动态添加和释放Request Pad

在用tee做多路分发时,你会遇到request pad的典型场景。比如把视频源同时送到预览窗口和编码器:

GstPad *tee_src_pad = gst_element_request_pad_simple(tee, "src_%u"); GstPad *encoder_sink_pad = gst_element_get_static_pad(encoder, "sink"); gst_pad_link(tee_src_pad, encoder_sink_pad);

用完要释放,特别是设备内存紧张或需要反复重建分支时:

gst_element_release_request_pad(tee, tee_src_pad);

有一个我踩过的坑:释放request pad之前,必须先解除它和下游的pad链接。否则你会看到类似“pad still linked”的警告,而且释放后旧分支可能仍在隐形工作。正确顺序是:

gst_pad_unlink(tee_src_pad, encoder_sink_pad); gst_element_release_request_pad(tee, tee_src_pad); gst_object_unref(encoder_sink_pad);

同理,muxer的request sink pad也是这么处理。不过muxer的sink pad通常是要等所有stream都加入之后才完全就绪,因此动态添加时也要在适当的时机一次性配置好。

5. 常见报错与排查技巧实录

5.1 “cannot link”报错:先查caps交集

这是每个GStreamer开发者迟早都会遇到的报错。比如:

WARNING: erroneous pipeline: could not link xxx to yyy

我的一般排错顺序是:

  1. 用gst-inspect-1.0查看两个元素的pad template,确认pad的类型和存在方式。
  2. 检查是不是把src连到了src、sink连到了sink。
  3. 检查是不是两个元素根本不在同一个bin里,或者层级关系不对。
  4. 如果以上都对,那大概率是caps交集为空。我把两个pad的caps查出来,用gst_caps_intersect验证一下:
GST_DEBUG=*:5 gst-launch-1.0 -v element_a ! element_b ! fakesink

日志里会出现“not mutually compatible”之类的字样。这时候最常见的补法是插入转换或capsfilter。

5.2 PAUSED状态卡住:Preroll不完成

管道卡在PAUSED,一个典型表现是应用程序发出GST_STATE_CHANGE_FAILURE,日志里显示“preroll pending”。这通常意味着数据没有流到sink。我总结出来三个高概率原因:

  • 某个元素没有sink pad或src pad,导致链接失败但没有及时检查返回值。
  • 动态pad还没有出现,你却用get_static_pad去拿,拿到NULL后硬连接。
  • caps协商失败,上游产出的格式下游不接受。

排查手段上用调试日志最直接:

GST_DEBUG=gst_pad:5,gst_caps:5,gst_element:5 ./my-application

日志里会显示每个pad当前协商的状态,卡在哪两个元素之间一目了然。

5.3 动态demuxer已经解析完,却总是错过pad-added

使用uridecodebin或者qtdemux这类元素时,pad是在解析到媒体轨道后异步动态创建的。如果在pad-added信号发出之后才建立监听,你就会错过链接时机。正确做法是在设置管道状态的之前,就把“pad-added”信号连好,并在回调里完成链接。

我见过一个同学的代码,先gst_element_set_state(pipeline, GST_STATE_PLAYING),再g_signal_connect去接pad-added信号,结果视频永远黑屏。把两个顺序调换后就好了。这是一个顺序敏感的问题,值得记下来。

5.4 Probe没有触发或者一直触发

probe没触发,十有八九是加错了pad位置,或者probe类型没配对。比如数据是buffer流,你加的却是GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,那自然等不到回调。

probe一直触发也常见,尤其是在PLAYING状态下不加限制地处理buffer,会影响实时性。如果你只是做统计,建议在probe回调里只做轻量操作,把数据处理放到另外的线程,避免阻塞数据流。

还有一种情况:probe加了之后没有保持ID,后面想移除时找不到。probe ID是guint,移除用gst_pad_remove_probe(pad, probe_id)。我在动态场景里通常把probe ID存到user_data结构体里,方便统一清理。

5.5 排查工具和技巧速查表

下面是我平时调试时最常用的“三件套”,奉上简便版:

工具/命令使用场景关键输出
gst-inspect-1.0 <element>查看元素pad模板和允许的capsPad Templates、元素属性
gst-launch-1.0 -v观察运行时的caps协商结果各pad的协商后caps
GST_DEBUG=gst_caps:5,gst_pad:5深度排查协商内部过程caps交集、链接细节

新手上路时,我强烈建议先把前两个工具用熟。用它们基本能定位80%以上的pad和caps问题。剩下那20%,调试日志翻出来慢慢看,通常也能解决。

6. 实战示例:一段简单的动态管道代码

最后还是给大家留一个完整的参考代码,演示了怎么在应用里获取pad、监听pad-added、手动连接、并使用probe做简单的帧数统计。这段代码我通常在验证环境里跑一遍,逻辑简单但能覆盖本篇文章提到的多数核心点。

#include <gst/gst.h> static guint frame_count = 0; static GstPadProbeReturn on_frame_probe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data) { frame_count++; g_print("Frame count: %u\n", frame_count); return GST_PAD_PROBE_OK; } static void on_pad_added(GstElement *element, GstPad *pad, gpointer user_data) { GstElement *pipeline = (GstElement *)user_data; GstElement *convert = gst_bin_get_by_name(GST_BIN(pipeline), "myconvert"); GstPad *sinkpad = gst_element_get_static_pad(convert, "sink"); if (!gst_pad_link(pad, sinkpad)) { g_print("Dynamic pad linked successfully\n"); } else { g_print("Dynamic pad link failed\n"); } gst_object_unref(sinkpad); gst_object_unref(convert); } int main(int argc, char *argv[]) { gst_init(&argc, &argv); GstElement *pipeline = gst_pipeline_new("test-pipeline"); GstElement *src = gst_element_factory_make("uridecodebin", "src"); GstElement *convert = gst_element_factory_make("videoconvert", "myconvert"); GstElement *sink = gst_element_factory_make("autovideosink", "sink"); g_object_set(src, "uri", "file:///path/to/sample.mp4", NULL); gst_bin_add_many(GST_BIN(pipeline), src, convert, sink, NULL); if (!gst_element_link(convert, sink)) { g_print("Static link failed\n"); gst_object_unref(pipeline); return -1; } GstPad *probe_pad = gst_element_get_static_pad(convert, "sink"); gst_pad_add_probe(probe_pad, GST_PAD_PROBE_TYPE_BUFFER, on_frame_probe, NULL, NULL); gst_object_unref(probe_pad); g_signal_connect(src, "pad-added", G_CALLBACK(on_pad_added), pipeline); gst_element_set_state(pipeline, GST_STATE_PLAYING); // ... 运行事件循环,按需停止 gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(pipeline); return 0; }

需要注意,这段示例代码里我用gst_element_factory_make("uridecodebin", "src")命名了一个元素叫“src”,只是为了演示方便。实际开发中命名不要和pad名混淆,否则代码可读性差。

我个人在实际开发中的体会是:pads和capabilities就像是GStreamer里的“神经”和“语言”。管道搭好了不代表一切顺利,pad是否连接、caps是否匹配,决定了数据能不能按预期流动。遇到莫名其妙的卡顿、黑屏、花屏,与其盲目试错,不如沉下心把pad的current caps打印出来,把两个pad的allowed caps取个交集看看,多半问题就清楚了。

最后再分享一个小技巧:在开发阶段,给每个关键pad都挂一个buffer probe加个计数,打印到日志里。这样你不需要gdb就能快速知道数据停在哪一环。等整个管道工作稳定后,再把这些计数probe统一移除,性能不会受任何影响。这个习惯我一直沿用,尤其处理动态分支的复杂管道时,简直是一颗“定心丸”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询