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之间的关系。你看一遍应该就能理解它们是怎么串起来的。
2.6 总结一下
这一章我们走了一遍GStreamer插件框架的四个核心概念:
- GObject:C语言面向对象的基础,所有插件都从这里继承。
- GstElement:插件的本体,负责数据处理和状态管理。
- GstPad:数据的出入口,通过caps协商格式。
- GstPlugin:插件的注册描述,让GStreamer能找到你的代码。
说白了,写插件就是:继承GstElement,定义好pad模板,实现数据处理函数,然后用GstPlugin注册上去。后面的章节,我们会一步步把这个流程走通。
个人建议:刚开始学的时候,别急着写复杂逻辑。先搭一个空壳插件,能注册成功、能被gst-inspect识别,就算迈出第一步了。我当年就是这么过来的。