4、Pipeline构建:静态pipeline与动态pipeline、gst_parse_launch()使用、手动构建pipeline、pipeline状态管理。
Pipeline,说白了就是Gstreamer的灵魂。你想想看,没有pipeline,那些element就像一堆散落的乐高积木,拼不出任何东西。这一章,我们就来聊聊怎么把这些积木搭起来,以及搭好之后怎么让它跑起来。
我个人习惯把pipeline构建分成两种:静态和动态。静态的,就是一开始就把整个图定死;动态的,则是运行时根据需要往里加东西。这两种方式各有各的用武之地,我们一个一个来看。
4.1 静态Pipeline:一把梭到底
静态pipeline,说白了就是「一把梭」。你一次性把所有的element都创建好,连接好,然后启动。这种方式最简单,也最直观。适合那些结构固定的场景,比如一个简单的播放器,或者一个固定的转码任务。
构建静态pipeline,最常用的就是 gst_parse_launch() 这个函数。嗯,这里要注意,这个函数是Gstreamer提供的一个「快捷方式」,它能把一个字符串描述直接解析成一个完整的pipeline。
核心函数:
GstElement *pipeline;
pipeline = gst_parse_launch("filesrc location=test.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink", NULL);
你看,一行代码,一个pipeline就建好了。这比手动创建几十个element再一个个连接,不知道省了多少事。
我在项目中遇到过,有些新手工程师特别喜欢用 gst_parse_launch(),觉得它方便。确实方便,但我要提醒你:它只适合原型验证和简单场景。为什么?因为一旦pipeline复杂起来,或者你需要精细控制每个element的参数,这个字符串就会变得又臭又长,而且调试起来非常痛苦。
我的建议:
原型阶段,大胆用 gst_parse_launch()。但进入产品阶段,尤其是需要处理错误、动态调整参数时,还是老老实实手动构建吧。
4.2 动态Pipeline:边跑边搭
动态pipeline,就是「边跑边搭」。你一开始只搭一个骨架,然后在运行过程中,根据数据流的情况,动态地插入或移除element。这听起来有点复杂,但实际场景中非常常见。
举个例子,一个网络摄像头,它可能一开始只传输视频,但后来用户想同时获取音频。这时候,你不能停掉整个pipeline再重建,而是要在运行中动态添加一个音频解码分支。
动态pipeline的核心,是 Pad(衬垫) 的概念。每个element都有输入和输出的pad,你可以通过监听pad的 pad-added 信号,来动态连接新的element。
动态连接的核心代码模式:
// 假设有一个 demuxer,它会在运行时动态创建输出 pad
static void on_pad_added(GstElement *src, GstPad *new_pad, gpointer data) {
GstElement *sink = GST_ELEMENT(data);
GstPad *sink_pad = gst_element_get_static_pad(sink, "sink");
// 检查这个新 pad 的 caps,决定是否连接
GstCaps *new_pad_caps = gst_pad_get_current_caps(new_pad);
GstStructure *new_pad_struct = gst_caps_get_structure(new_pad_caps, 0);
const gchar *new_pad_type = gst_structure_get_name(new_pad_struct);
if (g_str_has_prefix(new_pad_type, "video/x-h264")) {
// 如果是视频,连接到视频解码器
gst_pad_link(new_pad, sink_pad);
}
gst_caps_unref(new_pad_caps);
gst_object_unref(sink_pad);
}
// 在构建pipeline时,连接信号
g_signal_connect(demuxer, "pad-added", G_CALLBACK(on_pad_added), video_decoder);
我曾经在一个项目中,需要处理一个多路复用的RTSP流。那个流里既有H.264视频,又有AAC音频,还有字幕轨道。如果不用动态pipeline,我根本不知道什么时候该创建哪个解码器。嗯,动态pipeline就是解决这类问题的利器。
避坑指南:
我曾经在动态添加element时,忘记检查pad的caps就直接连接,结果导致pipeline状态切换失败,整个应用崩溃。记住:连接前一定要检查caps兼容性,否则你会被各种「not negotiated」错误折磨到怀疑人生。
4.3 手动构建Pipeline:精细控制
手动构建,就是不用 gst_parse_launch(),而是用API一个个创建element,然后手动连接。这种方式虽然代码量大,但给了你最大的控制权。
手动构建的步骤其实很固定,我总结了一个「三步走」套路:
- 创建element:用
gst_element_factory_make()一个个创建。 - 连接element:用
gst_bin_add_many()把element加入pipeline,然后用gst_element_link_many()连接。 - 设置属性:用
g_object_set()设置每个element的参数。
手动构建示例:
GstElement *pipeline, *source, *demuxer, *decoder, *converter, *sink;
// 1. 创建所有element
pipeline = gst_pipeline_new("my-pipeline");
source = gst_element_factory_make("filesrc", "file-source");
demuxer = gst_element_factory_make("qtdemux", "demuxer");
decoder = gst_element_factory_make("avdec_h264", "decoder");
converter = gst_element_factory_make("videoconvert", "converter");
sink = gst_element_factory_make("autovideosink", "video-sink");
if (!pipeline || !source || !demuxer || !decoder || !converter || !sink) {
g_printerr("创建element失败!\n");
return -1;
}
// 2. 设置属性
g_object_set(source, "location", "test.mp4", NULL);
// 3. 加入pipeline并连接
gst_bin_add_many(GST_BIN(pipeline), source, demuxer, decoder, converter, sink, NULL);
if (!gst_element_link_many(source, demuxer, NULL)) {
g_printerr("连接失败!\n");
gst_object_unref(pipeline);
return -1;
}
// 注意:demuxer 到 decoder 的连接需要动态处理,因为 demuxer 的 pad 是动态创建的
// 这里需要连接 pad-added 信号
你想想看,手动构建虽然麻烦,但每个element的生命周期、每个连接的状态,你都能了如指掌。这对于调试和性能优化来说,价值巨大。
4.4 Pipeline状态管理:让数据流起来
Pipeline建好了,怎么让它跑起来?这就涉及到状态管理了。Gstreamer的pipeline有四种状态,形成一个状态机:
| 状态 | 含义 | 典型操作 |
|---|---|---|
GST_STATE_NULL |
初始状态,未分配任何资源 | 创建后的默认状态 |
GST_STATE_READY |
已分配资源,但未开始处理数据 | 打开文件、分配内存池 |
GST_STATE_PAUSED |
已准备好处理数据,但处于暂停状态 | 等待第一个buffer到达 |
GST_STATE_PLAYING |
正在处理数据,时钟在运行 | 播放、编码、推流 |
状态切换的核心函数是 gst_element_set_state()。比如,启动pipeline:
// 从 NULL 到 PLAYING,Gstreamer 会自动经过 READY 和 PAUSED
gst_element_set_state(pipeline, GST_STATE_PLAYING);
这里有个重要的概念:状态切换是异步的。你调用 gst_element_set_state() 后,函数会立即返回,但实际的切换可能还没完成。如果你需要等待切换完成,可以用 gst_element_get_state() 来阻塞等待。
我的经验:
在嵌入式设备上,状态切换往往很慢,尤其是从 READY 到 PAUSED 这一步,因为要分配硬件解码器、申请DMA内存等。我曾经在一个海思平台上,这个切换花了将近2秒。所以,不要在状态切换期间做任何假设,一定要用 gst_element_get_state() 确认状态到位了再继续。
还有一个常见的坑:状态切换失败。比如,你从 PAUSED 切换到 PLAYING,但某个element因为缺少数据而卡住了。这时候,pipeline会进入一个错误状态。你需要监听 error 消息来捕获这些异常。
避坑指南:
我曾经在调试一个USB摄像头驱动时,发现pipeline在 PAUSED 状态卡住不动。查了半天,发现是摄像头驱动在 READY 到 PAUSED 时,需要发送一个URB请求,但USB总线被其他设备占用了。解决方案是在 READY 状态时,先释放其他USB设备的资源。嗯,这种问题,不踩一次坑,你根本想不到。
最后,别忘了在退出时清理状态:
// 发送 EOS 事件,优雅地停止
gst_element_send_event(pipeline, gst_event_new_eos());
// 等待 EOS 消息
// ... 监听 bus 上的 GST_MESSAGE_EOS ...
// 然后设置状态为 NULL,释放资源
gst_element_set_state(pipeline, GST_STATE_NULL);
gst_object_unref(pipeline);
好了,这一章的内容就到这里。Pipeline构建和状态管理,是Gstreamer开发的基础中的基础。你想想看,如果连pipeline都搭不好,数据流都跑不起来,那后面的什么音视频同步、性能优化,全都是空谈。下一章,我们会深入聊聊Bus和消息机制,看看怎么跟pipeline「对话」。