3、第一个Pipeline:创建简单的视频播放Pipeline、理解Pipeline状态机与事件循环

好,咱们正式开始动手了。

这一章,我会带你亲手搭建第一个 GStreamer pipeline。别怕,代码量很少,但背后的概念很重要。说白了,你写的第一行 pipeline 代码,就是理解 GStreamer 整个运行机制的钥匙。

3.1 最简单的视频播放 Pipeline

先看一个最经典的例子——播放一个视频文件。我习惯用 playbin 这个高级元素,它把文件读取、解码、音视频同步全包了。你想想看,一行代码就能搞定播放,是不是很爽?

#include <gst/gst.h>

int main(int argc, char *argv[]) {
    GstElement *pipeline;
    GstBus *bus;
    GstMessage *msg;
    GstStateChangeReturn ret;

    /* 初始化 GStreamer */
    gst_init(&argc, &argv);

    /* 创建 pipeline */
    pipeline = gst_element_factory_make("playbin", "player");
    g_object_set(pipeline, "uri", "file:///path/to/your/video.mp4", NULL);

    /* 开始播放 */
    gst_element_set_state(pipeline, GST_STATE_PLAYING);

    /* 等待错误或 EOS */
    bus = gst_element_get_bus(pipeline);
    msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE,
        GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

    /* 处理消息 */
    if (msg != NULL) {
        GError *err = NULL;
        gchar *debug_info = NULL;

        switch (GST_MESSAGE_TYPE(msg)) {
            case GST_MESSAGE_ERROR:
                gst_message_parse_error(msg, &err, &debug_info);
                g_printerr("Error: %s\n", err->message);
                g_error_free(err);
                g_free(debug_info);
                break;
            case GST_MESSAGE_EOS:
                g_print("播放结束\n");
                break;
            default:
                break;
        }
        gst_message_unref(msg);
    }

    /* 清理 */
    gst_object_unref(bus);
    gst_element_set_state(pipeline, GST_STATE_NULL);
    gst_object_unref(pipeline);

    return 0;
}

这段代码,我建议你亲手敲一遍。嗯,这里要注意:playbin 是一个复合元素,它内部自动创建了 source、demuxer、decoder、sink 等一系列子元素。你只需要给它一个 URI,它就能干活。

3.2 Pipeline 状态机:NULL → READY → PAUSED → PLAYING

GStreamer 的 pipeline 有四种基本状态。这可不是随便设计的,每个状态都有明确的含义。我在项目中遇到过不少新手直接跳状态,结果程序崩溃了还不知道为什么。

状态 含义 实际表现
NULL 初始状态,未分配任何资源 刚创建出来的 pipeline 就是这个状态
READY 资源已分配,但未开始处理数据 打开文件、分配内存,但还没开始解码
PAUSED 数据流已准备好,但时钟暂停 第一帧已经解码完成,等待播放指令
PLAYING 数据正在播放,时钟在运行 视频在屏幕上显示,音频在播放

状态切换是有顺序的。你不能直接从 NULL 跳到 PLAYING,必须经过 READY 和 PAUSED。为什么?

说白了,每个状态切换都在做特定的事情:

  • NULL → READY:分配资源。比如打开文件句柄、申请内存池。
  • READY → PAUSED:启动数据流。元素开始接收数据,但时钟是暂停的。
  • PAUSED → PLAYING:启动时钟。数据开始按时间戳播放。

我曾经犯过一个错误:在 READY 状态下就试图获取视频的宽高信息。结果呢?数据还没开始流动,宽高全是 0。后来才明白,这些信息要在 PAUSED 状态之后才能拿到。

重要提示:状态切换是异步的。你调用 gst_element_set_state() 后,函数会立即返回,但实际切换可能还没完成。要等待状态切换完成,可以用 gst_element_get_state() 或者监听总线消息。

3.3 事件循环与总线消息处理

Pipeline 跑起来了,但你怎么知道它有没有出错?怎么知道播放结束了?这就需要总线(Bus)和事件循环了。

总线,你可以把它想象成一个消息队列。所有元素(包括 pipeline 本身)都会把消息发到总线上。你的程序只需要从总线上取消息,然后做相应的处理。

常见的消息类型有:

  • GST_MESSAGE_ERROR:出错了。比如文件找不到、解码失败。
  • GST_MESSAGE_EOS:播放结束。End of Stream。
  • GST_MESSAGE_STATE_CHANGED:状态变了。可以用来跟踪 pipeline 的状态切换。
  • GST_MESSAGE_WARNING:警告。不影响播放,但值得关注。

处理消息有两种方式:

  1. 阻塞方式:用 gst_bus_timed_pop_filtered(),程序会一直等在那里,直到有消息到来。适合简单的 demo。
  2. 非阻塞方式:用 gst_bus_add_watch() 注册一个回调函数,消息来了自动调用。适合真正的应用。

我的经验:在实际项目中,我几乎不用阻塞方式。因为 GUI 程序需要响应用户操作,不能卡在消息等待上。用 gst_bus_add_watch() 配合 GLib 的主循环,既优雅又高效。

来看看非阻塞方式的代码片段:

static gboolean bus_callback(GstBus *bus, GstMessage *msg, gpointer data) {
    switch (GST_MESSAGE_TYPE(msg)) {
        case GST_MESSAGE_ERROR: {
            GError *err;
            gchar *debug;
            gst_message_parse_error(msg, &err, &debug);
            g_printerr("Error: %s\n", err->message);
            g_error_free(err);
            g_free(debug);
            /* 可以在这里触发退出主循环 */
            break;
        }
        case GST_MESSAGE_EOS:
            g_print("播放结束\n");
            /* 可以在这里触发退出主循环 */
            break;
        case GST_MESSAGE_STATE_CHANGED:
            /* 只关注 pipeline 本身的状态变化 */
            if (GST_MESSAGE_SRC(msg) == GST_OBJECT(pipeline)) {
                GstState old_state, new_state, pending;
                gst_message_parse_state_changed(msg, &old_state, &new_state, &pending);
                g_print("状态从 %s 变为 %s\n",
                    gst_element_state_get_name(old_state),
                    gst_element_state_get_name(new_state));
            }
            break;
        default:
            break;
    }
    return TRUE; /* 保持 watch 继续工作 */
}

/* 在主函数中注册 */
bus = gst_element_get_bus(pipeline);
gst_bus_add_watch(bus, bus_callback, NULL);
gst_object_unref(bus);

注意:在回调函数中不要做耗时操作。如果你需要处理大量数据,把任务丢到另一个线程去。我曾经在回调里直接写文件,结果导致视频播放卡顿,画面一帧一帧地跳。

3.4 完整的运行流程

好了,我们把所有东西串起来。一个典型的 GStreamer 程序运行流程是这样的:

  1. 初始化:调用 gst_init(),解析命令行参数,注册所有插件。
  2. 创建 pipeline:用 gst_element_factory_make() 创建元素,或者用 gst_parse_launch() 直接解析 pipeline 描述字符串。
  3. 设置属性:比如设置 URI、音量、播放速度等。
  4. 设置状态:调用 gst_element_set_state() 切换到 PLAYING。
  5. 事件循环:通过总线消息监听错误、EOS、状态变化等。
  6. 清理:设置状态为 NULL,释放所有资源。

你想想看,这个流程是不是很清晰?每个步骤都有明确的职责。我在做多路视频同步的时候,就是在这个基础上,给每个 pipeline 单独分配一个总线,然后统一管理它们的状态切换。

3.5 避坑指南

最后,分享几个我踩过的坑:

  • 忘记设置 URI:playbin 没有默认 URI,不设置的话 pipeline 会一直卡在 PAUSED 状态,不会报错也不会播放。
  • 状态切换没等待完成:在 PAUSED 状态下查询视频信息,结果全是 0。一定要等状态切换完成后再查询。
  • 总线消息没处理:如果不处理总线消息,错误会被忽略,程序看起来在运行,实际上已经出错了。
  • 忘记清理资源:每个 gst_object_ref() 都要对应一个 gst_object_unref(),否则内存泄漏。

嗯,这一章的内容就到这里。你亲手跑通第一个 pipeline 了吗?下一章我们会深入 pipeline 的内部结构,看看那些看不见的元素是怎么协同工作的。