4、Caps协商机制:什么是Caps、Caps的结构、Caps协商过程、固定Caps与可变Caps、实战:查看Caps信息

好,咱们今天聊聊Caps协商。说实话,这是GStreamer里最容易让人懵圈的概念之一。我刚开始接触的时候,也被它绕得晕头转向。但等你真正理解了,就会发现它其实特别巧妙——说白了,它就是管道里各个元件之间互相确认「你能给我什么格式的数据?我该给你什么格式?」的一套沟通规则。

什么是Caps?

Caps,全称是Capabilities,翻译过来就是「能力」。但我觉得叫「格式描述」更贴切。每个元件都有自己支持的格式范围,比如视频解码器能输出什么分辨率、什么色彩空间,编码器能接受什么输入格式。Caps就是用来描述这些信息的。

我打个比方你就明白了。你想想看,这就像两个人要合作干活,一个人说「我只会用中文交流」,另一个说「我只会用英文」。那他俩就没法沟通。但如果一个人说「我可以用中文或英文」,另一个说「我英文还行」,那就能用英文合作。Caps协商干的就是这个事——让上下游元件找到双方都支持的格式。

核心理解:Caps不是数据本身,而是对数据格式的描述。它告诉管道:「我这边出来的数据长什么样」。

Caps的结构

Caps的结构其实不复杂。我拆开给你看:

  • Caps:最外层,可以包含多个结构体(Structure)
  • Structure:每个结构体描述一种可能的格式,比如视频宽高、编码类型
  • Field:结构体里的字段,比如width=1920, height=1080

举个例子,一个典型的视频Caps长这样:

video/x-raw, format=I420, width=[16, 1920], height=[16, 1080], framerate=[0/1, 30/1]

这里面:

  • video/x-raw 是媒体类型,表示未压缩的视频
  • format=I420 是色彩格式
  • width=[16, 1920] 表示宽度范围从16到1920
  • framerate=[0/1, 30/1] 表示帧率范围

嗯,这里要注意:方括号表示这是一个范围,不是固定值。这在协商时特别重要。

Caps协商过程

协商过程,说白了就是上下游元件互相「讨价还价」。我画个流程给你看:

  1. 上游元件说:「我能输出这些格式」——它把自己的Caps贴出来
  2. 下游元件说:「我只能接受这些格式」——它也贴出自己的Caps
  3. 双方取交集:找到共同支持的格式
  4. 固定Caps:从交集中选一个具体值,确定下来

我在项目中遇到过一个问题:一个解码器输出Caps是 width=[16, 3840],下游的显示元件只支持 width=[320, 1920]。按理说取交集应该是 [320, 1920],但当时因为一个字段没对齐,协商失败了。排查了半天才发现是framerate字段的格式写错了。

我的经验:调试Caps协商问题时,最常用的命令就是 GST_DEBUG=*CAPS*:5。它能打印出所有Caps协商的细节,谁拒绝了谁、为什么拒绝,一目了然。

固定Caps与可变Caps

这个区分很重要,我当年就吃过亏。

固定Caps(Fixed Caps):所有字段都是具体值,没有范围。比如:

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

这种Caps表示「我只能处理这个精确的格式」,没得商量。

可变Caps(Variable Caps):包含范围或列表。比如:

video/x-raw, format={I420, NV12}, width=[16, 1920], height=[16, 1080]

这种Caps表示「我比较灵活,这些格式我都能接受」。

你想想看,为什么要有这个区分?因为有些元件是固定的,比如一个硬件编码器,它可能只支持一种分辨率。而有些元件是灵活的,比如一个缩放器,它能处理各种尺寸。协商的过程,就是把可变Caps逐步缩小,最终变成固定Caps。

避坑指南:我曾经写了一个自定义元件,Caps里忘了加framerate字段。结果下游元件一直协商失败,报错信息也不明确。后来才发现,GStreamer要求某些字段必须存在。所以写Caps时,尽量把关键字段都带上,别偷懒。

实战:查看Caps信息

光说不练假把式。咱们来点实际的。怎么查看管道里各个元件的Caps?

方法一:命令行查看

gst-inspect-1.0 可以查看元件的模板Caps:

gst-inspect-1.0 videotestsrc | grep "Pad Templates" -A 20

输出会显示这个元件的src pad支持哪些格式。

方法二:运行时查看

写个小程序,在管道运行时打印Caps:

#include <gst/gst.h>

static void on_pad_added(GstElement *src, GstPad *new_pad, gpointer data) {
    GstCaps *caps = gst_pad_get_current_caps(new_pad);
    if (caps) {
        gchar *caps_str = gst_caps_to_string(caps);
        g_print("当前Caps: %s\n", caps_str);
        g_free(caps_str);
        gst_caps_unref(caps);
    }
}

int main(int argc, char *argv[]) {
    gst_init(&argc, &argv);
    
    GstElement *pipeline = gst_parse_launch(
        "videotestsrc ! videoconvert ! autovideosink", NULL);
    
    GstElement *src = gst_bin_get_by_name(GST_BIN(pipeline), "videotestsrc0");
    g_signal_connect(src, "pad-added", G_CALLBACK(on_pad_added), NULL);
    
    gst_element_set_state(pipeline, GST_STATE_PLAYING);
    
    // 跑几秒后退出
    g_usleep(5000000);
    gst_element_set_state(pipeline, GST_STATE_NULL);
    gst_object_unref(pipeline);
    
    return 0;
}

方法三:调试日志

这是我最常用的方法:

GST_DEBUG=*CAPS*:5 ./my_app

你会看到类似这样的输出:

0:00:00.123456789 12345 0x7f1234567890 capsnegotiation gstpad.c:XXXX:gst_pad_negotiate<videotestsrc0:src> 进行Caps协商...
0:00:00.123456890 12345 0x7f1234567890 capsnegotiation gstcaps.c:XXXX:gst_caps_intersect 取交集结果: video/x-raw, format=I420, width=1920, height=1080

小技巧:如果你只想看某个特定元件的Caps协商,可以这样:GST_DEBUG=videotestsrc*CAPS*:5。这样日志就不会被其他元件的输出淹没了。

好了,关于Caps协商的核心内容就这些。记住一句话:Caps就是格式的「身份证」,协商就是「对暗号」。对上了,数据就能流通;对不上,管道就罢工。下次遇到管道跑不起来,先查Caps,十有八九能解决问题。