4. 管道与元件生命周期

好,我们进入第四讲。前面几章我们聊了 GStreamer 的宏观架构,也动手跑通了几个命令行管道。但说实话,如果你只停留在 gst-launch-1.0 的层面,那跟普通用户没什么区别。真正的多媒体架构师,得能自己写 Element。

写 Element 绕不开 GObject。很多刚接触 GStreamer 的朋友一看到 GObject 那套宏就头大,觉得 C 语言搞面向对象太别扭。我当年也是这个感觉——明明用 C++ 多省事?但后来在嵌入式平台上做项目,发现 C 的运行时开销确实可控,GObject 这套设计虽然啰嗦,但很扎实。

4.1 为什么 GStreamer 选择 GObject?

GStreamer 本质上是一个插件框架。插件意味着什么?意味着你可以在运行时加载一段代码,让它跟已有的模块无缝协作。这需要一套标准的对象模型。

GObject 提供了三个核心能力:

  • 类型注册与继承——每个 Element 都是一个 GObject 子类
  • 属性系统——通过 g_object_set() / g_object_get() 统一读写参数
  • 信号与回调——事件通知机制,比如 EOS、错误、状态变化

说白了,GObject 就是给 C 语言穿上一件「面向对象」的外衣。虽然穿脱麻烦点,但干活的时候很合身。

核心认知:GStreamer 里的每一个 Element,本质上都是一个 GObject 实例。你操作 Element 时调用的 gst_object_ref()、gst_object_unref(),底层走的就是 GObject 的引用计数机制。

4.2 GObject 的继承链:从 GObject 到 GstElement

我们来看一条典型的继承链:

GObject
  └── GstObject
        └── GstElement
              └── GstBaseSrc
                    └── GstPushSrc
                          └── 你的自定义 Source Element

每一层都在加东西:

层级 新增能力 典型方法
GObject 引用计数、属性、信号 g_object_new()
GstObject 名称、锁、父对象管理 gst_object_set_name()
GstElement Pad 管理、状态机、时钟 gst_element_set_state()
GstBaseSrc 推模式循环、段处理 gst_base_src_set_live()

嗯,这里要注意:你写自定义 Element 时,不需要从 GObject 开始造轮子。GStreamer 提供了很多「基类」,比如 GstBaseSrc、GstBaseTransform、GstBaseSink。你只需要继承它们,重写几个虚函数就行。

4.3 用宏来声明类型:GObject 的「样板代码」

写 GObject 子类,最烦的就是那堆宏。但没办法,这是 C 语言实现反射的代价。我习惯把这段代码称为「类型注册三板斧」:

// 1. 类型结构体
typedef struct _MyFilter {
  GstElement parent;
  // 你的私有数据放这里
  gboolean silent;
} MyFilter;

// 2. 类结构体
typedef struct _MyFilterClass {
  GstElementClass parent_class;
} MyFilterClass;

// 3. 类型注册宏
G_DEFINE_TYPE(MyFilter, my_filter, GST_TYPE_ELEMENT);

这三行代码干了什么?

  • 定义了实例结构体 MyFilter,第一个成员必须是父类类型
  • 定义了类结构体 MyFilterClass,用于存放虚函数表
  • G_DEFINE_TYPE 自动生成了 my_filter_get_type() 函数,这是 GObject 类型系统的入口

我的习惯:私有数据不要直接放在结构体里,而是用 GstPad 的 set_setproperty_function 配合 GstAllocator 做内存管理。直接暴露成员变量容易在插件升级时破坏 ABI 兼容性。

4.4 初始化函数:class_init 与 init

每个 GObject 子类都有两个初始化阶段:

// 类初始化——只执行一次
static void my_filter_class_init(MyFilterClass *klass) {
  GstElementClass *element_class = GST_ELEMENT_CLASS(klass);

  // 设置元数据:长名称、分类、描述
  gst_element_class_set_static_metadata(element_class,
    "My First Filter",
    "Filter/Effect/Video",
    "A simple video filter that does nothing",
    "Your Name <your@email.com>");

  // 注册 Pad 模板
  gst_element_class_add_static_pad_template(element_class,
      &my_filter_src_template);
  gst_element_class_add_static_pad_template(element_class,
      &my_filter_sink_template);
}

// 实例初始化——每个实例创建时执行
static void my_filter_init(MyFilter *filter) {
  // 创建 Pad
  filter->sinkpad = gst_pad_new_from_static_template(
      &my_filter_sink_template, "sink");
  filter->srcpad = gst_pad_new_from_static_template(
      &my_filter_src_template, "src");

  // 安装 Pad 函数
  gst_pad_set_chain_function(filter->sinkpad,
      gst_pad_chain_function(my_filter_chain));
  gst_pad_set_event_function(filter->sinkpad,
      gst_pad_event_function(my_filter_event));

  // 把 Pad 添加到 Element
  gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad);
  gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad);

  // 初始化私有数据
  filter->silent = FALSE;
}

class_init 里我建议把 Pad 模板和元数据都配好。init 里只做实例级别的初始化——创建 Pad、安装回调、设置默认值。

我曾经踩过的坑:在 class_init 里分配了动态内存,结果多个实例共享同一份数据,导致竞态条件。记住:class_init 只放「类级别」的东西,比如虚函数表、静态模板。实例级别的资源全部放 init 里。

4.5 Pad 模板:Element 的「接口声明」

Pad 模板相当于 Element 的 API 文档。它告诉 GStreamer 框架:我这个 Element 支持什么格式的数据。

// 定义静态 Pad 模板
static GstStaticPadTemplate my_filter_sink_template =
  GST_STATIC_PAD_TEMPLATE(
    "sink",                          // Pad 名称
    GST_PAD_SINK,                    // 方向:输入
    GST_PAD_ALWAYS,                  // 存在性:始终存在
    GST_STATIC_CAPS("video/x-raw,"
                    "format=(string)RGB,"
                    "width=[1, 4096],"
                    "height=[1, 4096]")  // 支持的 Caps
  );

static GstStaticPadTemplate my_filter_src_template =
  GST_STATIC_PAD_TEMPLATE(
    "src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS("video/x-raw,"
                    "format=(string)RGB,"
                    "width=[1, 4096],"
                    "height=[1, 4096]")
  );

你想想看,这个模板其实就是一份「能力声明」。框架在连接两个 Element 时,会检查 src Pad 的输出 Caps 是否匹配 sink Pad 的输入 Caps。不匹配?管道就建不起来。

4.6 完整的 Element 骨架代码

我们把上面所有碎片拼起来,就是一个能跑的最小 Element:

#include <gst/gst.h>

/* 类型定义 */
typedef struct _MyFilter {
  GstElement parent;
  GstPad *sinkpad, *srcpad;
  gboolean silent;
} MyFilter;

typedef struct _MyFilterClass {
  GstElementClass parent_class;
} MyFilterClass;

/* Pad 模板 */
static GstStaticPadTemplate sink_template =
  GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK,
      GST_PAD_ALWAYS,
      GST_STATIC_CAPS("video/x-raw,format=(string)RGB"));

static GstStaticPadTemplate src_template =
  GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC,
      GST_PAD_ALWAYS,
      GST_STATIC_CAPS("video/x-raw,format=(string)RGB"));

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

/* Chain 函数——核心处理逻辑 */
static GstFlowReturn
my_filter_chain(GstPad *pad, GstObject *parent, GstBuffer *buf) {
  MyFilter *filter = MY_FILTER(parent);

  if (!filter->silent)
    g_print("Got buffer of size %" G_GSIZE_FORMAT "\n",
            gst_buffer_get_size(buf));

  return gst_pad_push(filter->srcpad, buf);
}

/* class_init */
static void
my_filter_class_init(MyFilterClass *klass) {
  GstElementClass *element_class = GST_ELEMENT_CLASS(klass);

  gst_element_class_set_static_metadata(element_class,
      "My Filter", "Filter/Effect/Video",
      "A minimal filter element", "Author");

  gst_element_class_add_static_pad_template(element_class,
      &sink_template);
  gst_element_class_add_static_pad_template(element_class,
      &src_template);
}

/* init */
static void
my_filter_init(MyFilter *filter) {
  filter->sinkpad = gst_pad_new_from_static_template(
      &sink_template, "sink");
  filter->srcpad = gst_pad_new_from_static_template(
      &src_template, "src");

  gst_pad_set_chain_function(filter->sinkpad,
      my_filter_chain);

  gst_element_add_pad(GST_ELEMENT(filter), filter->sinkpad);
  gst_element_add_pad(GST_ELEMENT(filter), filter->srcpad);

  filter->silent = FALSE;
}

/* 插件入口 */
static gboolean
plugin_init(GstPlugin *plugin) {
  return gst_element_register(plugin, "myfilter",
      GST_RANK_NONE, MY_FILTER_TYPE);
}

GST_PLUGIN_DEFINE(
  GST_VERSION_MAJOR,
  GST_VERSION_MINOR,
  myfilter,
  "My first filter plugin",
  plugin_init,
  "1.0", "LGPL", "GStreamer", "http://gstreamer.net/"
);

这段代码虽然只有 80 行,但已经是一个完整的 GStreamer 插件了。编译后可以用 gst-launch-1.0 videotestsrc ! myfilter ! videoconvert ! autovideosink 来测试。

4.7 GObject 属性系统:让 Element 可配置

光有 Chain 函数还不够。一个专业的 Element 应该允许用户调节参数。GObject 的属性系统就是干这个的。

enum {
  PROP_0,
  PROP_SILENT,
  PROP_LAST
};

static void
my_filter_set_property(GObject *object, guint prop_id,
    const GValue *value, GParamSpec *pspec) {
  MyFilter *filter = MY_FILTER(object);

  switch (prop_id) {
    case PROP_SILENT:
      filter->silent = g_value_get_boolean(value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
      break;
  }
}

static void
my_filter_get_property(GObject *object, guint prop_id,
    GValue *value, GParamSpec *pspec) {
  MyFilter *filter = MY_FILTER(object);

  switch (prop_id) {
    case PROP_SILENT:
      g_value_set_boolean(value, filter->silent);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
      break;
  }
}

// 在 class_init 中安装属性
static void
my_filter_class_init(MyFilterClass *klass) {
  GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->set_property = my_filter_set_property;
  gobject_class->get_property = my_filter_get_property;

  g_object_class_install_property(gobject_class, PROP_SILENT,
      g_param_spec_boolean("silent", "Silent",
          "Whether to print debug messages",
          FALSE, G_PARAM_READWRITE));
}

有了属性系统,用户就可以用 gst-launch-1.0 videotestsrc ! myfilter silent=true ! ... 来控制行为。我个人觉得,属性系统是 GObject 最优雅的设计之一——它把配置接口标准化了,任何语言绑定都能自动支持。

4.8 本章知识体系总览

下面这张图总结了从 GObject 到 GstElement 的核心脉络:

GObject 与 GStreamer Element 核心结构图 GObject → GstElement 核心结构 GObject 核心机制 类型注册 (G_DEFINE_TYPE) | 引用计数 (ref/unref) | 属性系统 (get/set_property) | 信号 (signal) GstObject(GObject 子类) 名称管理 | 锁机制 | 父对象层级 | 调试分类 GstElement(GstObject 子类) Pad 管理 (添加/移除) | 状态机 (NULL→READY→PAUSED→PLAYING) | 时钟与延迟 GstBaseSrc 推模式数据源 GstBaseTransform 数据变换处理 GstBaseSink 数据接收输出 你的自定义 Element(继承任意基类) 继承关系 每层增加能力

4.9 避坑指南:新手写 Element 的常见问题

最后,分享几个我这些年积累的经验:

  • 别忘了调用父类的 class_init——G_DEFINE_TYPE 会自动处理,但如果你手动写 get_type 函数,一定要链式调用父类的初始化。
  • Pad 名称要唯一——同一个 Element 里不能有两个同名的 Pad,否则 gst_element_add_pad 会返回 FALSE。
  • Chain 函数里不要阻塞——GStreamer 的数据流是异步的,你在 Chain 里做耗时操作会卡住整个管道。需要异步处理的话,用 GstTask 或 GstBaseSrc 的 create 虚函数。
  • 引用计数要对称——gst_object_ref 和 gst_object_unref 必须成对出现。我曾经在 Pad 的 _get 函数里忘了 unref,导致内存泄漏查了两天。

嗯,写 Element 确实比单纯用 gst-launch 复杂得多。但这是成为 GStreamer 高手的必经之路。你掌握了 GObject 这套机制,就等于拿到了 GStreamer 世界的「造物主」权限——任何数据处理逻辑,你都能用 Element 的形式封装起来,供整个生态复用。