2、GStreamer插件框架:GObject基础、GstElement、GstPad、GstPlugin

好,咱们正式开始写插件之前,得先把GStreamer的骨架摸清楚。说白了,GStreamer就是一个搭积木的游戏。你写的每个插件,就是一块积木。那这块积木长什么样?怎么跟别的积木拼在一起?这就是本章要聊的事。

我个人习惯,学一个新框架,先看它的对象模型。GStreamer底层是GObject,这是GLib的对象系统。不理解GObject,后面写插件会处处碰壁。我刚开始接触的时候,也觉得这东西绕,但用顺手了会发现,它其实很规整。

2.1 GObject:一切的基础

GObject是GLib提供的面向对象C框架。你没看错,C语言也能搞面向对象。它通过宏和结构体模拟了类、继承、接口这些概念。

一个典型的GObject结构体长这样:

// 对象实例结构体
typedef struct _MyFilter {
  GstElement parent;  // 父类实例,必须放在第一字段
  gboolean enabled;
  gint threshold;
} MyFilter;

// 对象类结构体
typedef struct _MyFilterClass {
  GstElementClass parent_class;  // 父类类结构体
  void (*process)(MyFilter *filter, GstBuffer *buf);
} MyFilterClass;

这里有个关键点:实例结构体的第一个字段必须是父类实例。为什么?因为C语言里,结构体首地址就是第一个字段的地址。GStreamer通过强制类型转换,把子类指针当成父类指针用。你想想看,如果顺序错了,整个类型系统就崩了。

核心要点:GObject的继承靠的是结构体嵌套,不是虚函数表。所有GStreamer插件最终都继承自GstObject → GstElement。

我记得第一次写插件时,忘了在实例结构体里放parent字段,结果gst-inspect-1.0直接报段错误。嗯,这个坑踩过一次就不会再犯了。

2.2 GstElement:插件的本体

GstElement是GStreamer里所有处理单元的基类。你的插件,本质上就是一个GstElement的子类。它负责接收数据、处理数据、发送数据。

每个GstElement都有几个关键属性:

属性 说明 我见过的坑
name 元素名称,用于调试和管道定位 名字重复会导致管道启动失败
state 当前状态(NULL/READY/PAUSED/PLAYING) 状态转换顺序不能跳,必须逐级走
pads 元素的输入输出端口列表 pad数量必须在初始化时确定

GstElement的状态机是四态的:NULL → READY → PAUSED → PLAYING。反过来也一样,不能跳。我曾经在调试一个音画不同步的问题时,发现插件在PAUSED状态还没准备好数据就切到了PLAYING,结果时间戳全乱了。

小技巧:在change_state函数里,一定要调用父类的change_state,否则状态机不会完整执行。我见过有人忘了这步,结果插件永远卡在READY状态。

2.3 GstPad:数据的出入口

GstPad就是元素上的接口,数据从src pad出去,从sink pad进来。一个元素可以有多个sink pad(比如多路混音器),也可以有多个src pad(比如分路器)。

Pad有两种类型:

  • 静态Pad:在元素初始化时就固定创建,数量不变。比如一个解码器,通常只有一个sink和一个src。
  • 请求Pad:运行时按需创建,比如muxer在添加新流时动态生成pad。

Pad之间通过Capabilities(简称caps)协商。caps描述了pad能处理的数据格式,比如视频的宽高、编码格式,音频的采样率、通道数等。两个pad连接时,必须找到双方都支持的格式。

// 定义一个sink pad的caps:只接受I420格式的视频
static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE (
  "sink",
  GST_PAD_SINK,
  GST_PAD_ALWAYS,
  GST_STATIC_CAPS ("video/x-raw, format=I420")
);

这里要注意,GST_PAD_ALWAYS表示这个pad始终存在。还有GST_PAD_SOMETIMES(有时存在)和GST_PAD_REQUEST(按需创建)。

避坑指南:我曾经写过一个插件,sink pad的caps写得太宽泛,结果上游送来了NV12格式,我的处理函数只认I420,画面直接花屏。后来我学乖了,caps一定要写精确,或者在chain函数里做格式检测。

2.4 GstPlugin:插件的注册与加载

GstPlugin是插件的描述结构体。它告诉GStreamer框架:我这个插件叫什么名字,是什么类型,有哪些pad模板,版本号多少。

每个插件都需要一个插件初始化函数,名字固定为plugin_init。GStreamer在加载插件时,会调用这个函数来注册元素。

// 插件初始化函数
static gboolean plugin_init(GstPlugin *plugin) {
  // 注册元素类型
  return gst_element_register(
    plugin,           // 插件指针
    "myfilter",       // 元素名称,管道里用这个名字
    GST_RANK_NONE,    // 优先级
    MY_TYPE_FILTER    // 元素类型ID
  );
}

// 插件描述宏
GST_PLUGIN_DEFINE (
  GST_VERSION_MAJOR,
  GST_VERSION_MINOR,
  myfilter,
  "My custom video filter plugin",
  plugin_init,
  "1.0.0",
  "LGPL",
  "GStreamer",
  "https://example.com"
)

这个GST_PLUGIN_DEFINE宏会生成插件入口点。编译成.so文件后,放到GStreamer的插件目录下,就能被gst-inspect-1.0识别了。

我建议你把插件版本号管理好。有一次我升级了插件,但忘了改版本号,结果gst-launch-1.0一直加载旧版本,排查了半天才发现是缓存问题。

2.5 整体框架关系图

下面这张图展示了GObject、GstElement、GstPad、GstPlugin之间的关系。你看一遍应该就能理解它们是怎么串起来的。

GStreamer 插件框架核心关系图 GObject GstObject GstElement 包含 GstPad sink pad / src pad GstPlugin 注册 说明: 1. GObject → GstObject → GstElement 是继承链。 2. GstElement 包含一个或多个 GstPad,pad 负责数据收发。 3. GstPlugin 负责将 GstElement 注册到 GStreamer 框架中。 4. 插件编译后通过 plugin_init 函数完成注册。

2.6 总结一下

这一章我们走了一遍GStreamer插件框架的四个核心概念:

  • GObject:C语言面向对象的基础,所有插件都从这里继承。
  • GstElement:插件的本体,负责数据处理和状态管理。
  • GstPad:数据的出入口,通过caps协商格式。
  • GstPlugin:插件的注册描述,让GStreamer能找到你的代码。

说白了,写插件就是:继承GstElement,定义好pad模板,实现数据处理函数,然后用GstPlugin注册上去。后面的章节,我们会一步步把这个流程走通。

个人建议:刚开始学的时候,别急着写复杂逻辑。先搭一个空壳插件,能注册成功、能被gst-inspect识别,就算迈出第一步了。我当年就是这么过来的。