插件骨架:GstPlugin、GstElement、GstPad 基础概念

好,咱们正式开始写代码了。这一章我带你搭起 GStreamer 插件的最小骨架。说白了,就是让 GStreamer 能认出你的插件,并且知道它是个什么东西。

很多初学者一上来就盯着数据处理逻辑写,结果插件加载就崩了。我当年也犯过这毛病——花了两天调试一个 pad 不链接的问题,最后发现是插件注册时类型名字写错了。嗯,咱们先把地基打牢。

三个核心概念:Plugin、Element、Pad

GStreamer 的插件体系其实就三层,你记住这张图就行:

GStreamer 插件三层架构 GstPlugin(插件) 一个 .so 文件,包含多个 Element GstElement(元素) 具体的编解码器、过滤器、源端、接收端 GstPad(垫片) 数据进出的接口,Src Pad 和 Sink Pad 层级关系 1 个 Plugin 可以注册多个 Element 1 个 Element 可以有多个 Pad Pad 负责数据搬运 Src Pad 输出数据 Sink Pad 接收数据

我习惯把 Plugin 比作一个工具箱,Element 是工具箱里的扳手、螺丝刀,Pad 就是扳手咬合螺丝的那个口。没有 Pad,数据就流不出去。

GstPlugin:插件的身份证

每个插件必须有一个 plugin_init 函数。这是 GStreamer 加载插件时的入口点。你想想看,GStreamer 拿到一个 .so 文件,它怎么知道里面有什么元素?就是靠这个函数告诉它的。

核心要点: plugin_init 是插件与 GStreamer 框架之间的契约。名字不能改,签名不能错。

函数签名长这样:

static gboolean
plugin_init(GstPlugin *plugin)
{
    /* 在这里注册你的元素 */
    return TRUE;
}

参数 GstPlugin *plugin 是框架传给你的,你不需要自己创建。返回值 TRUE 表示加载成功,FALSE 表示加载失败——GStreamer 会立刻卸载这个插件。

注意: 我曾经见过有人在这个函数里做复杂的初始化操作,比如打开文件、连接网络。千万别这么干!plugin_init 只应该做注册工作,耗时操作会导致整个管道启动变慢。

GstElement:插件的核心单元

Element 是真正干活的。GStreamer 内置了几百种 Element,从文件读取、解码、编码到渲染,应有尽有。我们写自定义插件,本质上就是定义一个新的 Element 类型。

每个 Element 需要有一个类型结构体,以及对应的初始化函数。看个最简单的例子:

/* 定义元素类型 */
#define GST_TYPE_MY_FILTER (my_filter_get_type())
G_DECLARE_FINAL_TYPE(MyFilter, my_filter, MY, FILTER, GstElement)

/* 元素结构体 */
struct _MyFilter {
    GstElement parent;
    /* 以后会加 pad、属性、内部状态 */
};

/* 类型注册 */
G_DEFINE_TYPE(MyFilter, my_filter, GST_TYPE_ELEMENT)

/* 初始化函数 */
static void
my_filter_init(MyFilter *filter)
{
    /* 这里创建 pad */
}

/* 类初始化函数 */
static void
my_filter_class_init(MyFilterClass *klass)
{
    /* 这里设置元数据 */
}

嗯,这里要注意:G_DECLARE_FINAL_TYPE 和 G_DEFINE_TYPE 是 GLib 的宏,它们帮你生成了类型注册的样板代码。我刚开始写的时候觉得这些宏很烦人,但用久了发现它们确实能避免很多手写错误。

GstPad:数据的出入口

Pad 分两种:

Pad 类型 方向 典型用途
Src Pad 输出数据 源元素(如文件读取)、滤镜的输出端
Sink Pad 接收数据 接收端元素(如渲染器)、滤镜的输入端

一个 Element 可以有多个 Src Pad 和多个 Sink Pad。比如一个视频解码器,可能有 1 个 Sink Pad 接收压缩数据,2 个 Src Pad 分别输出视频和音频。

创建 Pad 的代码:

static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE(
    "sink",                          /* Pad 名字 */
    GST_PAD_SINK,                    /* Pad 方向 */
    GST_PAD_ALWAYS,                  /* 存在性:ALWAYS/SOMETIMES/REQUEST */
    GST_STATIC_CAPS_ANY              /* 支持的 Caps,后面章节细讲 */
);

static void
my_filter_init(MyFilter *filter)
{
    /* 创建 Sink Pad */
    filter->sinkpad = gst_pad_new_from_static_template(
        &sink_template, "sink");
    gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad);
}

个人经验: Pad 的名字不要随便起。GStreamer 的 bin 元素(比如 playbin)会通过名字来查找特定的 Pad。如果你写了一个视频滤镜,Pad 叫 "video_sink" 和 "video_src",别人一看就明白。我曾经见过有人把 Pad 命名为 "aaa" 和 "bbb",调试的时候差点崩溃。

编写第一个空插件

好了,理论说完了,咱们动手写一个完整的空插件。这个插件什么都不做,但它能成功加载到 GStreamer 中。

完整的代码:

#include <gst/gst.h>

/* 元素类型声明 */
#define GST_TYPE_MY_FIRST_PLUGIN (my_first_plugin_get_type())
G_DECLARE_FINAL_TYPE(MyFirstPlugin, my_first_plugin, MY, FIRST_PLUGIN, GstElement)

/* 元素结构体 */
struct _MyFirstPlugin {
    GstElement parent;
};

/* 类型注册宏 */
G_DEFINE_TYPE(MyFirstPlugin, my_first_plugin, GST_TYPE_ELEMENT)

/* 元素初始化 */
static void
my_first_plugin_init(MyFirstPlugin *self)
{
    /* 空插件,暂时不创建 Pad */
    (void)self;
}

/* 类初始化 */
static void
my_first_plugin_class_init(MyFirstPluginClass *klass)
{
    GstElementClass *element_class = GST_ELEMENT_CLASS(klass);

    /* 设置元数据:长名、分类、描述、作者 */
    gst_element_class_set_static_metadata(
        element_class,
        "My First Plugin",           /* 长名 */
        "Filter",                    /* 分类 */
        "An empty plugin for demo",  /* 描述 */
        "Your Name <email@example.com>"  /* 作者 */
    );
}

/* 插件入口函数 */
static gboolean
plugin_init(GstPlugin *plugin)
{
    return gst_element_register(
        plugin,                      /* 插件指针 */
        "myfirstplugin",             /* 元素名字,管道中用这个名字 */
        GST_RANK_NONE,               /* 优先级 */
        GST_TYPE_MY_FIRST_PLUGIN     /* 元素类型 */
    );
}

/* 插件描述信息,GStreamer 通过这个宏找到 plugin_init */
GST_PLUGIN_DEFINE(
    GST_VERSION_MAJOR,               /* 主版本号 */
    GST_VERSION_MINOR,               /* 次版本号 */
    my_first_plugin,                 /* 插件名字(小写) */
    "My first GStreamer plugin",     /* 插件描述 */
    plugin_init,                     /* 入口函数 */
    VERSION,                         /* 版本号,由构建系统定义 */
    "LGPL",                          /* 许可证 */
    "GStreamer",                     /* 包名 */
    "http://gstreamer.net/"          /* 项目网址 */
)

关键点:

  • GST_PLUGIN_DEFINE 宏必须放在源文件末尾,它展开后会生成插件加载所需的符号。
  • gst_element_register 把元素类型注册到插件中,这样 GStreamer 才能通过名字创建它。
  • 元数据中的分类("Filter")会影响 gst-inspect-1.0 的搜索结果。

编译与测试

写完了怎么验证?我建议你用 Meson 构建系统,这是 GStreamer 官方推荐的。但为了快速测试,你也可以手动编译:

gcc -shared -fPIC \
    -o libmyfirstplugin.so \
    my_first_plugin.c \
    `pkg-config --cflags --libs gstreamer-1.0`

然后设置环境变量让 GStreamer 能找到你的插件:

export GST_PLUGIN_PATH=$(pwd)
gst-inspect-1.0 myfirstplugin

如果一切正常,你会看到类似这样的输出:

Factory Details:
  Long name:              My First Plugin
  Classification:         Filter
  Description:            An empty plugin for demo
  Author:                 Your Name <email@example.com>

Plugin Details:
  Name:                   my_first_plugin
  Description:            My first GStreamer plugin
  Filename:               /path/to/libmyfirstplugin.so
  Version:                1.20.0
  License:                LGPL
  Source module:          GStreamer
  Source release date:    unknown
  Binary package:         GStreamer
  Origin URL:             http://gstreamer.net/

避坑指南: 我曾经因为 GST_PLUGIN_PATH 没设置对,折腾了半小时。GStreamer 默认只搜索系统路径和 GST_PLUGIN_PATH 指定的路径。如果你把 .so 文件放在当前目录但没设环境变量,gst-inspect 是找不到的。

另外,注意插件名字的对应关系:GST_PLUGIN_DEFINE 的第二个参数是 my_first_plugin,生成的 .so 文件名必须是 libmyfirstplugin.so。GStreamer 会通过文件名推断插件名,如果对不上,加载会失败。

小结一下

这一章我们搭好了插件的最小骨架。你掌握了:

  • Plugin、Element、Pad 三层架构的关系
  • plugin_init 函数的写法
  • Element 的类型注册和初始化
  • Pad 的基本概念和创建方法
  • 一个完整的空插件代码