4、衬垫机制:Pad的静态与动态类型、Pad模板(Pad Template)、Capabilities协商

好,咱们今天聊点实在的——衬垫(Pad)。

你想想看,GStreamer 里那么多元件,它们怎么知道对方能干什么?怎么连起来的?靠的就是 Pad。我刚开始学的时候,觉得这玩意儿不就是个接口嘛,有啥好讲的。后来被坑了几次才明白,Pad 是整个管线的“神经末梢”,不懂它,你连个 mp4 文件都播不顺畅。

4.1 什么是 Pad?为什么它这么重要?

Pad 的中文翻译叫“衬垫”,但我觉得叫“接口”或者“端口”更贴切。每个元件都有 Pad,它是数据进出的唯一通道。

  • Src Pad(源衬垫):数据从这儿流出去。比如一个文件读取元件,它的 Src Pad 会吐出原始字节流。
  • Sink Pad(接收衬垫):数据从这儿流进来。比如一个显示元件,它的 Sink Pad 接收解码后的视频帧。

一个元件可以有多个 Src Pad 和多个 Sink Pad。比如 tee 元件,一个 Sink Pad 进来,多个 Src Pad 分出去——就像水管的三通接头。

核心要点:Pad 是元件之间“握手”的地方。握手成功,数据才能流通。握手失败,管线就报错。

4.2 静态 Pad vs 动态 Pad

这里有个坑,我当年踩过。Pad 分两种:静态和动态。

4.2.1 静态 Pad(Always Pad)

静态 Pad 是元件一创建就存在的,永远在那儿。比如大部分解码器、编码器、过滤器,它们的 Pad 是固定的。

// 伪代码示例:一个静态 Pad 的声明
static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE (
    "sink",                    // Pad 名称
    GST_PAD_SINK,              // 方向:接收
    GST_PAD_ALWAYS,            // 类型:静态
    GST_STATIC_CAPS_ANY        // 支持的 Capabilities:任意格式
);

静态 Pad 的好处是简单。你创建元件,Pad 就在那儿,直接连就行。我早期写插件时全用静态 Pad,省心。

4.2.2 动态 Pad(Sometimes Pad / Request Pad)

动态 Pad 就有点意思了。它不是一开始就存在的,而是在运行时按需创建。比如 demuxer(解复用器)读取一个 MP4 文件,它一开始不知道文件里有几路流——可能一路视频、两路音频、一路字幕。所以它先创建一个 Src Pad 发数据,等解析到新流时,再动态创建一个新的 Src Pad。

实战经验:我在做一个多摄像头拼接项目时,用了 input-selector 元件。它的 Sink Pad 就是动态的——用户随时可以添加或移除摄像头。如果我用静态 Pad,每次改配置都得重建管线,那性能就崩了。

动态 Pad 的典型场景:

  • 解复用器:MP4、TS、MKV 等容器格式,流数量不确定。
  • 复合元件:比如 compositor,你可以动态添加视频层。
  • 分流器:tee 元件的 Src Pad 也是动态的,你可以随时 request 一个新 Pad。

注意:动态 Pad 的连接时机很重要。你不能在元件刚创建时就连它,因为 Pad 还不存在。正确的做法是监听 pad-added 信号,等元件通知你“我有了一个新 Pad”,你再连。

4.3 Pad 模板(Pad Template)

Pad 模板是什么?说白了,它就是 Pad 的“简历”。每个元件在注册时,会声明自己有哪些 Pad 模板,描述每个 Pad 能处理什么格式的数据。

模板包含三个关键信息:

字段 说明 示例
名称 Pad 的名字,用于区分多个 Pad "sink", "src_%u"
方向 GST_PAD_SRC 或 GST_PAD_SINK 源或接收
存在类型 GST_PAD_ALWAYS, GST_PAD_SOMETIMES, GST_PAD_REQUEST 静态、有时、按需
Capabilities 该 Pad 支持的数据格式列表 video/x-h264, audio/mpeg

我习惯把 Pad 模板想象成“招聘启事”。元件说:“我需要一个能处理 H.264 视频的 Sink Pad,谁符合条件谁来。” 另一个元件看到这个启事,如果自己的 Src Pad 能输出 H.264,它俩就能连上。

// 一个典型的 Pad 模板声明
static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE (
    "src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (
        "video/x-raw, "
        "  format = (string) { I420, NV12, YUY2 }, "
        "  width  = (int) [ 16, 1920 ], "
        "  height = (int) [ 16, 1080 ], "
        "  framerate = (fraction) [ 0, 60 ]"
    )
);

看到没?这个模板声明了:我的 Src Pad 能输出原始视频,格式可以是 I420、NV12 或 YUY2,分辨率范围 16x16 到 1920x1080,帧率最高 60fps。这就是 Capabilities 的初步形态。

4.4 Capabilities 协商:Pad 之间的“谈判”

好,现在两个元件要连起来了。但它们的 Pad 模板只是“愿望清单”,实际能不能跑,还得坐下来谈——这就是 Capabilities 协商。

协商的过程,说白了就是两个 Pad 互相问:“你能给我什么?” “我能给你这个,你行吗?” “不行,换一个。” “那这个呢?” “可以,成交!”

4.4.1 协商的两种方式

  • 固定协商(Fixed Negotiation):上游 Pad 直接决定格式,下游只能接受或拒绝。简单粗暴,效率高。适合格式单一的场景。
  • 交集协商(Intersect Negotiation):双方列出自己能支持的格式列表,取交集。如果交集为空,连接失败。这是最常用的方式。

举个例子:一个视频源元件输出 video/x-raw, format=I420,一个编码器元件接收 video/x-raw, format={ I420, NV12 }。交集是 I420,协商成功。如果源只能输出 YUY2,交集为空,连接失败。

4.4.2 协商的时机

协商发生在两个地方:

  1. 连接时:当你调用 gst_pad_link() 时,GStreamer 会尝试做一次初步协商。如果 Capabilities 完全不匹配,直接返回错误。
  2. 运行时:数据开始流动后,上游元件可能会改变格式(比如分辨率变化)。这时会触发重新协商。我遇到过的一个坑是:摄像头突然从 1080p 切换到 720p,下游的编码器没准备好,管线直接挂了。后来加了 capsfilter 才稳住。

4.4.3 用 capsfilter 强制协商

有时候你不想让元件自己瞎商量,你想指定一个固定格式。这时候就用 capsfilter 元件。

# 强制输出为 NV12 格式
gst-launch-1.0 videotestsrc ! video/x-raw,format=NV12 ! autovideosink

这行命令里,video/x-raw,format=NV12 就是一个 capsfilter 的简写。它告诉上游:“我只要 NV12,别的不要。” 上游元件会尝试转换成 NV12,如果转不了,就报错。

我的习惯:在调试管线时,我经常在关键节点插入 capsfilter 来固定格式。这样能快速定位是哪个环节的协商出了问题。比如视频花屏了,我就在解码器后面加一个 capsfilter 固定成 I420,如果还花屏,那就是解码器的问题;如果不花了,那就是下游的颜色空间转换有问题。

4.5 一张图看懂 Pad 机制

说了这么多,咱们用一张 SVG 图把整个流程串起来。这张图展示了两个元件通过 Pad 连接、模板匹配、Capabilities 协商的完整链路。

Pad 机制核心流程 Pad 机制核心流程 元件 A(源) 元件 B(接收) Src Pad Sink Pad Pad 模板(Pad Template) 名称: src 类型: ALWAYS Caps: video/x-raw 名称: sink 类型: ALWAYS Caps: video/x-raw Capabilities 协商 上游提供: format=I420, NV12 下游需求: format=I420, YUY2 交集结果: format=I420 → 协商成功 ✅ Pad 连接成功,数据开始流动 如果交集为空,连接失败并返回错误

4.6 常见问题与避坑指南

最后,分享几个我实际项目中踩过的坑,希望能帮你省点时间。

坑一:动态 Pad 连接时机不对

我曾经写了一个播放器,用 uridecodebin 自动解码。我以为它一创建就有 Src Pad,直接连了。结果运行时一直报错“no such pad”。后来才发现,uridecodebin 的 Src Pad 是动态的,得等 pad-added 信号触发后再连。正确的做法是:

g_signal_connect(uridecodebin, "pad-added", G_CALLBACK(on_pad_added), pipeline);

坑二:Capabilities 写得太宽泛

有个同事写插件时,Pad 模板的 Capabilities 写了 GST_STATIC_CAPS_ANY,意思是“什么格式都行”。结果协商时下游给了个奇怪的格式,插件处理不了,直接崩溃。我建议:永远不要用 ANY,除非你确定你的元件能处理所有格式。写具体一点,比如 video/x-raw, format=I420,反而更安全。

坑三:忘记处理重新协商

有一次做视频会议系统,网络波动导致摄像头自动切换分辨率。下游的编码器没监听 renegotiate 事件,还在用旧的分辨率编码,结果出来的视频全是绿的。解决方案是在 Sink Pad 上设置 accept-caps 回调,动态响应格式变化。

嗯,Pad 机制就讲到这里。内容不少,但核心就三句话:Pad 是接口,模板是简历,协商是谈判。把这三点吃透了,GStreamer 管线对你来说就不再是黑盒了。