3. Gstreamer核心对象详解

好,咱们今天来啃Gstreamer的五个核心对象。说实话,我刚接触Gstreamer那会儿,最头疼的就是这些概念。什么Element、Pad、Caps...听着就头大。但后来我发现,搞懂了这五个东西,整个Gstreamer框架就通透了一半。

我个人习惯把这五个对象比作一个水管系统:GstElement是水管里的各种设备(水泵、过滤器、阀门),GstPad是设备的接口(进水口、出水口),GstCaps是水流能通过的规格(管径、水质要求),GstBuffer是水管里流动的水,而GstBus就是设备上的报警灯和喇叭。

嗯,这个比喻不一定完全准确,但能帮你快速建立印象。咱们一个一个来看。

3.1 GstElement:一切的基础

GstElement,说白了就是Gstreamer里的"积木块"。你搭建一个管道,就是在拼这些积木。每个Element负责一件事:读取文件、解码视频、渲染画面等等。

我在项目中遇到过一个问题:一个Element创建出来,但怎么都加不进管道。后来发现是状态没设置对。Element有四种状态:

状态 含义 说明
GST_STATE_NULL 初始状态 刚创建出来,啥也没干
GST_STATE_READY 就绪状态 资源已分配,但还没开始跑
GST_STATE_PAUSED 暂停状态 数据流已准备好,但没在播放
GST_STATE_PLAYING 播放状态 数据正在流动

你想想看,Element的状态切换是有顺序的:NULL→READY→PAUSED→PLAYING。不能跳着来。我见过有人直接从NULL跳到PLAYING,结果程序直接崩了。

// 创建Element
GstElement *source = gst_element_factory_make("filesrc", "file-source");
if (!source) {
    g_printerr("创建filesrc失败,检查插件是否安装\n");
    return -1;
}

// 设置属性
g_object_set(source, "location", "/home/test.mp4", NULL);

// 状态切换的正确姿势
gst_element_set_state(source, GST_STATE_READY);
gst_element_set_state(source, GST_STATE_PAUSED);
gst_element_set_state(source, GST_STATE_PLAYING);
注意:创建Element时一定要检查返回值。我曾经在某个嵌入式平台上,因为缺少插件,gst_element_factory_make返回了NULL,但代码没检查,结果后面操作空指针,查了一整天才找到问题。

3.2 GstPad:Element的"手脚"

GstPad就是Element上的接口。每个Element可以有多个Pad,分为两种:Sink Pad(输入)和Source Pad(输出)。

举个例子:一个解码器Element,它有一个Sink Pad接收压缩数据,一个Source Pad输出解码后的数据。说白了,数据从Sink Pad流进来,从Source Pad流出去。

我记得有一次调试一个视频播放器,画面就是出不来。后来用gst-inspect一看,发现两个Element的Pad类型不匹配——一个是video/x-raw,另一个是video/x-h264,根本连不上。

// 获取Pad
GstPad *sink_pad = gst_element_get_static_pad(decoder, "sink");
GstPad *src_pad = gst_element_get_static_pad(decoder, "src");

// 检查Pad是否存在
if (!sink_pad || !src_pad) {
    g_printerr("获取Pad失败\n");
    return -1;
}

// 连接两个Element的Pad
GstPadLinkReturn ret = gst_pad_link(src_pad_of_element1, sink_pad_of_element2);
if (GST_PAD_LINK_FAILED(ret)) {
    g_printerr("Pad连接失败,检查Caps是否匹配\n");
}
小技巧:用gst-inspect-1.0命令可以查看Element有哪些Pad。比如 gst-inspect-1.0 videotestsrc,就能看到它的Source Pad支持哪些格式。

3.3 GstCaps:数据格式的"说明书"

GstCaps,全称是GstCapabilities,它描述了数据流的格式。两个Pad能不能连上,就看它们的Caps是否兼容。

你想想看,一个输出RGB的Element,连到一个只接受YUV的Element,那肯定不行。Caps就是用来做这个匹配的。

// 创建一个Caps
GstCaps *caps = gst_caps_new_simple("video/x-raw",
    "format", G_TYPE_STRING, "I420",
    "width", G_TYPE_INT, 1920,
    "height", G_TYPE_INT, 1080,
    "framerate", GST_TYPE_FRACTION, 30, 1,
    NULL);

// 设置到Element上
g_object_set(encoder, "caps", caps, NULL);

// 用完释放
gst_caps_unref(caps);

我在项目中遇到过最坑的一件事:Caps里有个字段叫"pixel-aspect-ratio",默认是1/1。但有个视频源是4/3的,我没设置这个字段,结果画面被拉伸了。查了两天才发现是Caps没写对。

核心要点:Caps的匹配规则是"子集匹配"。也就是说,如果Element A的Caps是Element B的Caps的子集,那就能连上。这给了我们很大的灵活性。

3.4 GstBuffer:流动的数据

GstBuffer就是实际的数据包。它里面装着音频采样、视频帧、或者任何你需要处理的数据。

嗯,这里要注意:GstBuffer不是简单的内存块,它有自己的生命周期管理。每个Buffer都有引用计数,当引用计数归零时,内存会被自动释放。

// 创建一个Buffer
GstBuffer *buffer = gst_buffer_new_allocate(NULL, 1024, NULL);

// 获取Buffer的数据
GstMapInfo map;
gst_buffer_map(buffer, &map, GST_MAP_WRITE);
// 往map.data里写数据...
memcpy(map.data, "hello", 5);
gst_buffer_unmap(buffer, &map);

// 发送Buffer到下游
GstFlowReturn ret = gst_pad_push(src_pad, buffer);
if (ret != GST_FLOW_OK) {
    g_printerr("推送Buffer失败\n");
}
避坑指南:我曾经在写一个插件时,忘记调用gst_buffer_unmap,结果内存泄漏了。嵌入式设备内存本来就紧张,跑了一晚上就OOM了。记住:map和unmap必须成对出现。

3.5 GstBus:消息的"快递员"

GstBus是Gstreamer的消息系统。Element通过Bus向应用程序发送消息:播放结束了、出错了、状态变了等等。

说白了,Bus就是Element和应用程序之间的通信桥梁。你不需要轮询Element的状态,只需要监听Bus上的消息就行了。

// 获取Bus
GstBus *bus = gst_element_get_bus(pipeline);

// 监听消息
GstMessage *msg = gst_bus_timed_pop_filtered(bus,
    GST_CLOCK_TIME_NONE,
    GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

if (msg != NULL) {
    switch (GST_MESSAGE_TYPE(msg)) {
        case GST_MESSAGE_ERROR: {
            GError *err;
            gchar *debug;
            gst_message_parse_error(msg, &err, &debug);
            g_printerr("错误: %s\n", err->message);
            g_error_free(err);
            g_free(debug);
            break;
        }
        case GST_MESSAGE_EOS:
            g_print("播放结束\n");
            break;
        default:
            break;
    }
    gst_message_unref(msg);
}

gst_object_unref(bus);

我记得有一次,客户反馈说播放器偶尔会卡死。我加了一堆日志,最后发现是Bus消息处理线程里做了耗时操作,导致消息队列堵住了。解决方案很简单:把耗时操作放到另一个线程去处理。

建议:Bus消息处理要快,不要在里面做复杂操作。如果非要处理,把消息内容复制出来,扔到工作线程里去搞。

3.6 五个对象的关系

最后,咱们把这五个对象串起来看看:

  1. GstElement是积木块,拼成管道
  2. GstPad是积木块的接口,负责连接
  3. GstCaps是接口的规格,决定能不能连
  4. GstBuffer是流动的数据,在Pad之间传递
  5. GstBus是消息通道,让应用知道管道里发生了什么

你想想看,一个典型的播放流程是这样的:

应用程序创建管道(一堆Element),通过Pad把它们连起来(Caps要匹配),然后启动播放。数据以Buffer的形式在Element之间流动,同时Element通过Bus向应用报告状态。就这么简单。

嗯,这五个对象是Gstreamer的基石。搞懂了它们,后面的学习就会顺畅很多。下一章咱们会讲怎么用这些对象搭建一个完整的管道,到时候你会看到它们是怎么协同工作的。