4、第一个插件:编写最简单的identity插件、理解插件模板、注册插件到GStreamer、用gst-launch测试

好,咱们终于要动手写第一个插件了。

说实话,我当年第一次写GStreamer插件时,心里也挺没底的。看着那堆宏定义和模板代码,总觉得这东西很神秘。但等你真正写过一个之后就会发现——其实核心逻辑就那么几块,剩下的都是套路。

这一章,我们就从零开始,写一个最简单的identity插件。它什么都不做,就是把数据原封不动地传过去。但你别小看它,这个骨架撑起了GStreamer里90%的插件结构。

4.1 什么是identity插件?

identity,顾名思义,就是「身份」——数据进来什么样,出去还是什么样。

你可能会问:「那它有什么用?」

嗯,我在项目中经常拿它做两件事:一是调试,在pipeline中间插一个identity,看看数据流是否正常;二是作为模板,后面加功能时直接在上面改。说白了,它就是插件开发的「Hello World」。

4.2 插件模板长什么样?

每个GStreamer插件,本质上就是一个C源文件。它必须包含以下几块内容:

  • 头文件包含:引入GStreamer核心库
  • 插件结构体定义:描述这个插件是什么
  • pad模板定义:输入输出接口的描述
  • 初始化函数:插件被加载时调用
  • 注册函数:把插件告诉GStreamer框架

我个人习惯把插件模板分成三个文件:

  • gstmyidentity.h — 头文件,声明结构体和函数
  • gstmyidentity.c — 实现文件,写核心逻辑
  • meson.build — 构建文件,告诉编译器怎么编译

当然,你也可以把所有代码塞到一个文件里。但项目大了之后,拆开会好维护很多。

4.3 手写第一个插件:gstmyidentity

我们先看头文件。它很简单,就是声明一个结构体:

// gstmyidentity.h
#ifndef __GST_MY_IDENTITY_H__
#define __GST_MY_IDENTITY_H__

#include <gst/gst.h>

G_BEGIN_DECLS

#define GST_TYPE_MY_IDENTITY   (gst_my_identity_get_type())
#define GST_MY_IDENTITY(obj)   (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_MY_IDENTITY, GstMyIdentity))
#define GST_MY_IDENTITY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_MY_IDENTITY, GstMyIdentityClass))
#define GST_IS_MY_IDENTITY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_MY_IDENTITY))
#define GST_IS_MY_IDENTITY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_MY_IDENTITY))

typedef struct _GstMyIdentity      GstMyIdentity;
typedef struct _GstMyIdentityClass GstMyIdentityClass;

struct _GstMyIdentity {
    GstElement element;
    GstPad    *sinkpad, *srcpad;
};

struct _GstMyIdentityClass {
    GstElementClass parent_class;
};

GType gst_my_identity_get_type (void);

G_END_DECLS

#endif /* __GST_MY_IDENTITY_H__ */

看到这些宏了吗?GST_TYPE_MY_IDENTITYGST_MY_IDENTITY……它们都是GObject类型系统的标准套路。你不需要背,复制粘贴改个名字就行。

接下来是实现文件。这是核心:

// gstmyidentity.c
#include "gstmyidentity.h"

GST_DEBUG_CATEGORY_STATIC (gst_my_identity_debug);
#define GST_CAT_DEFAULT gst_my_identity_debug

/* 定义pad模板 */
static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE (
    "sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS_ANY
);

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE (
    "src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS_ANY
);

/* chain函数:数据来了怎么处理 */
static GstFlowReturn
gst_my_identity_chain (GstPad *pad, GstObject *parent, GstBuffer *buffer)
{
    GstMyIdentity *identity = GST_MY_IDENTITY (parent);

    /* 这里什么都不做,直接推给下游 */
    return gst_pad_push (identity->srcpad, buffer);
}

/* 初始化插件实例 */
static void
gst_my_identity_init (GstMyIdentity *identity)
{
    /* 创建sink pad */
    identity->sinkpad = gst_pad_new_from_static_template (&sink_template, "sink");
    gst_pad_set_chain_function (identity->sinkpad, gst_my_identity_chain);
    gst_element_add_pad (GST_ELEMENT (identity), identity->sinkpad);

    /* 创建src pad */
    identity->srcpad = gst_pad_new_from_static_template (&src_template, "src");
    gst_element_add_pad (GST_ELEMENT (identity), identity->srcpad);
}

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

    gst_element_class_set_details_simple (element_class,
        "MyIdentity",
        "Filter/Identity",
        "A simple identity plugin that passes data through",
        "Your Name <your@email.com>");

    gst_element_class_add_pad_template (element_class,
        gst_static_pad_template_get (&sink_template));
    gst_element_class_add_pad_template (element_class,
        gst_static_pad_template_get (&src_template));
}

/* 注册插件到GStreamer */
static gboolean
plugin_init (GstPlugin *plugin)
{
    GST_DEBUG_CATEGORY_INIT (gst_my_identity_debug, "myidentity", 0, "My Identity plugin");

    return gst_element_register (plugin, "myidentity", GST_RANK_NONE, GST_TYPE_MY_IDENTITY);
}

GST_PLUGIN_DEFINE (
    GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    myidentity,
    "My Identity plugin",
    plugin_init,
    "1.0",
    "LGPL",
    "GStreamer",
    "http://gstreamer.net/"
)

代码不长,但每一行都有它的作用。我重点说几个地方:

  • chain函数:这是数据处理的入口。你想想看,GStreamer里数据流就像水流,chain函数就是水龙头——数据从sink pad流进来,你处理完,再从src pad推出去。这里我们什么都没做,直接推走。
  • pad模板GST_STATIC_CAPS_ANY表示这个插件接受任何格式的数据。实际项目中你会指定具体的caps,比如视频、音频。
  • GST_PLUGIN_DEFINE:这个宏是插件的「身份证」。它告诉GStreamer:我叫myidentity,版本1.0,采用LGPL协议,初始化函数是plugin_init。
💡 小技巧:写插件时,我习惯先写一个空的chain函数,编译通过后再加逻辑。这样能快速验证框架是否搭对了。

4.4 构建文件:meson.build

现在GStreamer官方推荐用Meson构建系统。你需要在插件目录下放一个meson.build

project('gst-myidentity', 'c', version : '1.0.0')

gst_dep = dependency('gstreamer-1.0')

shared_library('gstmyidentity',
    'gstmyidentity.c',
    dependencies : [gst_dep],
    install : true,
    install_dir : get_option('libdir') / 'gstreamer-1.0'
)

然后执行:

meson setup builddir
cd builddir
ninja
sudo ninja install

嗯,这里要注意:安装路径一定要是GStreamer的插件目录。你可以用gst-inspect-1.0 --gst-plugin-path查看当前搜索路径。如果装错了位置,GStreamer是找不到你的插件的。

4.5 注册插件到GStreamer

其实注册这件事,代码里已经做了。就是那个gst_element_register调用。它把插件名字"myidentity"和类型GST_TYPE_MY_IDENTITY绑定在一起。

你安装好插件后,可以用gst-inspect-1.0 myidentity来验证:

$ gst-inspect-1.0 myidentity
Factory Details:
  Long name: MyIdentity
  Class:     Filter/Identity
  Description: A simple identity plugin that passes data through
  Author:    Your Name <your@email.com>

Pad Templates:
  SRC template: 'src'
    Availability: Always
    Capabilities: ANY

  SINK template: 'sink'
    Availability: Always
    Capabilities: ANY

看到这个输出,说明插件已经成功注册到GStreamer了。

⚠️ 避坑指南:我曾经遇到过一个问题——插件编译安装后,gst-inspect死活找不到。折腾了半天,发现是插件目录权限不对。GStreamer在加载插件时,如果目录不可读,它会静默跳过,不会报错。所以安装后记得检查一下目录权限。

4.6 用gst-launch测试

插件写好了,怎么验证它能不能跑?最直接的方式就是用gst-launch-1.0

我们来搭一个最简单的pipeline:

gst-launch-1.0 audiotestsrc ! myidentity ! fakesink

这条命令的意思是:

  • audiotestsrc:生成测试音频数据
  • myidentity:我们的插件,什么都不做,直接透传
  • fakesink:一个虚拟的接收端,把数据丢掉

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

Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
Redistribute latency...
^Chandling interrupt.
Interrupt: Stopping pipeline ...
Setting pipeline to NULL ...
Freeing pipeline ...

没有报错,没有崩溃,说明你的第一个插件跑起来了!

你也可以试试视频:

gst-launch-1.0 videotestsrc ! myidentity ! autovideosink

如果能看到测试画面,恭喜你——你已经成功写出了一个GStreamer插件。

4.7 总结与思考

这一章我们做了三件事:

  1. 写了一个最简单的identity插件,理解了插件模板的结构
  2. 通过gst_element_register把插件注册到GStreamer
  3. gst-launch验证了插件能正常工作

你可能会觉得:「就这?什么都没做啊。」

没错,但你要知道,GStreamer里90%的插件都是在这个骨架基础上加功能的。后面我们会在这个插件上添加属性、处理事件、支持caps协商……每一步都是在这个模板上做加法。

下一章,我们给这个identity插件加点「料」——让它能统计经过的数据量,并输出日志。到时候你就知道,这个看似简单的骨架,其实藏着很多可能性。

📌 本章核心要点

  • 插件模板 = 头文件 + 实现文件 + 构建文件
  • chain函数是数据处理的核心入口
  • GST_PLUGIN_DEFINE宏是插件的「身份证」
  • gst-launch是验证插件最快捷的方式