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_IDENTITY、GST_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。
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了。
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 总结与思考
这一章我们做了三件事:
- 写了一个最简单的identity插件,理解了插件模板的结构
- 通过
gst_element_register把插件注册到GStreamer - 用
gst-launch验证了插件能正常工作
你可能会觉得:「就这?什么都没做啊。」
没错,但你要知道,GStreamer里90%的插件都是在这个骨架基础上加功能的。后面我们会在这个插件上添加属性、处理事件、支持caps协商……每一步都是在这个模板上做加法。
下一章,我们给这个identity插件加点「料」——让它能统计经过的数据量,并输出日志。到时候你就知道,这个看似简单的骨架,其实藏着很多可能性。
📌 本章核心要点
- 插件模板 = 头文件 + 实现文件 + 构建文件
- chain函数是数据处理的核心入口
- GST_PLUGIN_DEFINE宏是插件的「身份证」
- gst-launch是验证插件最快捷的方式