Pipeline 构建:多路流的基本框架
好,咱们进入正题。上一章聊了同步的基础概念,这一章咱们得动手了。Pipeline 怎么搭?多路流进来之后,怎么让它们在一个管道里规规矩矩地跑?
我刚开始做多路流的时候,犯过一个挺蠢的错误——直接把两路视频源怼进同一个 pipeline,结果画面卡得跟幻灯片似的。后来才明白,多路流不是简单地把元素堆在一起,你得有个清晰的框架思维。
2.1 多路流 Pipeline 的核心结构
说白了,多路流 pipeline 就三个层次:
- 源端层:各路流的采集或读取
- 汇聚层:把多路流合并到一起
- 输出层:渲染、编码或推流
你想想看,这就像多条高速公路汇入主路。每条路有自己的入口(source),中间有个立交桥(muxer/compositor),最后统一出城(sink)。
核心原则:多路流 pipeline 的难点不在单个元素,而在元素之间的 数据流拓扑。拓扑错了,后面所有同步逻辑都是白搭。
2.2 最简单的多路流 Pipeline:双路视频拼接
咱们先看一个最基础的例子。两路视频源,左右拼接显示。我用的是 compositor 元素,它比 videomixer 更灵活,支持更多布局控制。
gst-launch-1.0 \
videotestsrc pattern=0 ! videoconvert ! queue ! compositor name=mix \
sink_0::xpos=0 sink_0::ypos=0 sink_0::width=640 sink_0::height=480 \
videotestsrc pattern=1 ! videoconvert ! queue ! mix.sink_1 \
mix.sink_1::xpos=640 sink_1::ypos=0 sink_1::width=640 sink_1::height=480 \
mix ! videoconvert ! autovideosink
这段代码干了什么?两路 videotestsrc 分别产生测试画面,经过 videoconvert 转格式,再经过 queue 缓冲,最后进入 compositor。注意 sink_0 和 sink_1 的坐标参数——我手动指定了它们的位置和大小。
我的习惯:每个分支都加一个 queue。别小看这个 queue,它能解耦上下游的线程,避免一个分支卡住拖死整条 pipeline。我在项目中遇到过好几次,去掉 queue 之后画面间歇性撕裂,加上就稳了。
2.3 多路流 Pipeline 的三种拓扑模式
实际项目中,多路流的拓扑不止一种。我归纳了三种常见模式:
| 拓扑模式 | 适用场景 | 核心元素 | 同步难度 |
|---|---|---|---|
| 星型汇聚 | 多路输入合成一路输出 | compositor, audiomixer | 中等 |
| 链式级联 | 视频墙、多屏拼接 | compositor 嵌套 | 较高 |
| 并行分流 | 多路独立编码/录制 | tee, queue | 较低 |
星型汇聚 是最常用的。所有源都连到一个汇聚节点,由汇聚节点负责时序对齐。我建议新手先从这种模式入手。
链式级联 呢?说白了就是 compositor 套 compositor。比如你要做 4×4 的视频墙,不可能一个 compositor 挂 16 路输入——性能扛不住。我一般分两层:每 4 路先合成一组,再把 4 组结果合成最终画面。
并行分流 用 tee 元素。一路信号分给多个下游,每个下游独立处理。这种模式同步压力最小,因为本质上还是单路流。
2.4 多路流 Pipeline 的 SVG 框架图
光说不够直观,我画了一张图。这是星型汇聚模式的典型结构:
这张图里,每个 Source 都配了一个 Queue。嗯,这里要注意——Queue 的位置很关键。我见过有人把 Queue 放在 Compositor 后面,那基本等于没放。Queue 必须放在每个分支上,才能独立缓冲。
2.5 多路流 Pipeline 的代码组织技巧
用 gst-launch-1.0 写多路流,命令行会变得很长。我个人的习惯是:
- 用变量拆分:把每个分支的字符串存成变量,最后拼接
- 用注释标注 sink pad:每个分支末尾标注它连到 compositor 的哪个 sink
- 先搭骨架,再填细节:先让两路流跑通,再加第三路、第四路
举个例子,上面那个双路拼接的命令,用变量拆分会清晰很多:
SRC0="videotestsrc pattern=0 ! videoconvert ! queue ! compositor name=mix"
SRC1="videotestsrc pattern=1 ! videoconvert ! queue ! mix.sink_1"
gst-launch-1.0 \
$SRC0 sink_0::xpos=0 sink_0::ypos=0 sink_0::width=640 sink_0::height=480 \
$SRC1 sink_1::xpos=640 sink_1::ypos=0 sink_1::width=640 sink_1::height=480 \
mix ! videoconvert ! autovideosink
我曾经踩过的坑:在 gst-launch-1.0 里,sink pad 的参数必须紧跟在元素后面,不能写在变量里。比如 sink_0::xpos=0 必须跟在 compositor name=mix 之后,否则解析会失败。这个坑我 debug 了整整一个下午。
2.6 多路流 Pipeline 的线程模型
多路流 pipeline 默认是单线程的。你想想看,如果所有分支都在一个线程里跑,一路卡住,其他路都得等着。
GStreamer 的解决方案是:每个分支通过 queue 创建独立的线程。queue 元素内部有一个线程边界,上游在一个线程里推数据,下游在另一个线程里拉数据。
所以,多路流 pipeline 的线程数大致是:
- 1 个主线程(pipeline 本身)
- N 个源线程(每个 queue 上游一个)
- 1 个汇聚线程(compositor 下游)
总线程数 = N + 2。N 是源路数。
我的建议:如果发现 CPU 占用异常高,先检查线程数。我曾经遇到一个 8 路流的 pipeline,线程数只有 3——说明 queue 没生效,所有分支都在主线程里跑。加上 queue 之后,线程数变成 10,CPU 占用反而降了 30%。
2.7 多路流 Pipeline 的常见错误
我整理了几个新手最容易犯的错误:
| 错误 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| 缺少 queue | 画面卡顿、撕裂 | 分支间互相阻塞 | 每个分支加 queue |
| sink pad 参数位置错误 | compositor 报错 | 参数未绑定到正确元素 | 参数紧跟在 compositor 之后 |
| 格式不匹配 | compositor 拒绝连接 | 各路视频格式不一致 | 统一用 videoconvert 转格式 |
| 时间戳缺失 | 画面不同步 | 源未产生正确时间戳 | 加 timestamp 或 identity |
嗯,这里要特别提一下格式不匹配的问题。Compositor 要求所有输入的视频格式、分辨率、帧率一致。不一致怎么办?用 videoscale 和 videorate 做适配。我一般会在每个分支的 queue 后面加一个 videoscale ! videorate ! videoconvert 的组合,确保格式统一。