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 框架图

光说不够直观,我画了一张图。这是星型汇聚模式的典型结构:

多路流 Pipeline 星型汇聚框架 Source 1 Source 2 Source 3 Queue Queue Queue Compositor sink_0 / sink_1 / sink_2 Sink 显示/编码 说明: 每个 Source 分支必须经过 Queue 缓冲,再进入 Compositor 的独立 sink pad。 Compositor 内部根据时间戳对齐各路画面,最终输出到 Sink。

这张图里,每个 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 的组合,确保格式统一。