QGC 视频图传与流媒体开发
6.0 总体架构
QGC 4.0 的视频子系统在编译宏 QGC_GST_STREAMING 开启时,以 GStreamer 1.x 为底层解码引擎,采用 Manager → Receiver → Pipeline → QML Sink 四层结构,将网络/RTP/RTSP 码流解码后渲染到 OpenGL 纹理,再嵌入 QML 界面。
┌─────────────────────────────────────────────────────────────┐
│ View:FlightDisplayViewVideo.qml / QGCVideoBackground.qml │
│ (GstGLVideoItem OpenGL 纹理 + 网格线/全屏/热成像 PIP) │
└──────────────────────────┬──────────────────────────────────┘
│ Q_PROPERTY / 信号槽
┌──────────────────────────▼──────────────────────────────────┐
│ Manager:VideoManager(QGCTool) │
│ URI 配置、双路流(可见光+热成像)、录制、SubtitleWriter │
└──────────────────────────┬──────────────────────────────────┘
│ start/stop/setUri/setVideoSink
┌──────────────────────────▼──────────────────────────────────┐
│ Receiver:VideoReceiver │
│ GStreamer pipeline 构建、tee 分支、watchdog、自动重连 │
└──────────────────────────┬──────────────────────────────────┘
│ udpsrc/rtspsrc → parsebin → decodebin
┌──────────────────────────▼──────────────────────────────────┐
│ Sink:qgcvideosinkbin(glupload → qmlglsink) │
│ 绑定 QML GstGLVideoItem widget │
└─────────────────────────────────────────────────────────────┘
涉及的设计模式:
| 模式 | 体现 |
|---|---|
| QGCTool 模式 | VideoManager 在 QGCToolbox 两阶段初始化中注册 |
| Factory 模式 | QGCCorePlugin::createVideoReceiver() / createVideoManager() |
| Tee 分支模式 | 显示与录制共用 tee,动态挂接 filesink 分支 |
| Observer(信号槽) | videoRunningChanged、gotFirstRecordingKeyFrame、Fact 变更触发 restartVideo() |
| Strategy 模式 | 按 URI 前缀选择不同 GStreamer source 元素 |
| Watchdog 模式 | _frameTimer + _videoSinkProbe 检测帧活性 |
| 延迟初始化 | _initVideo() 在 QQuickWindow 渲染阶段绑定 widget |
涉及语法与技术:
- C++:
QObject、Q_PROPERTY、#if defined(QGC_GST_STREAMING)条件编译 - GStreamer C API:
gst_element_factory_make、g_object_set、pad probe - QML:
GstGLVideoItem(org.freedesktop.gstreamer.GLVideoItem) - Fact 系统:
VideoSettings持久化配置 - MAVLink:
VIDEO_STREAM_INFORMATION自动发现流地址
6.1 相机视频流接收与解码
6.1.1 模块与文件索引
| 文件 | 职责 |
|---|---|
VideoStreaming/VideoManager.h/.cc |
视频总控,双 Receiver,设置同步 |
VideoStreaming/VideoReceiver.h/.cc |
GStreamer 管道生命周期 |
VideoStreaming/VideoStreaming.cc |
GStreamer 初始化、插件注册 |
VideoStreaming/gstqgcvideosinkbin.c |
自定义 sink bin |
VideoStreaming/gstqgc.c |
QGC GStreamer 插件注册 |
Settings/VideoSettings.h/.cc |
视频配置 Fact 组 |
Settings/Video.SettingsGroup.json |
默认值与枚举 |
FlightMap/QGCVideoBackground.qml |
GstGLVideoItem 包装 |
FlightDisplay/FlightDisplayViewVideo.qml |
Fly 视图视频区域 |
Camera/QGCCameraManager.h |
MAVLink 相机/流信息 |
6.1.2 初始化流程
(1)应用启动: QGCApplication 调用 initializeVideoStreaming(argc, argv, gstDebugLevel):
- 设置
GST_PLUGIN_PATH(Windows/macOS 内置 GStreamer) - 移动端静态注册插件:
coreelements、libav、rtp、rtsp、udp、qmlgl、qgc等 - 注册 QML 类型
GstGLVideoItem(无 GStreamer 时用GLVideoItemStub空壳)
(2)Toolbox 初始化: VideoManager::setToolbox():
75:80:src/VideoStreaming/VideoManager.cc
_videoReceiver = toolbox->corePlugin()->createVideoReceiver(this);
_thermalVideoReceiver = toolbox->corePlugin()->createVideoReceiver(this);
_updateSettings();
if(isGStreamer()) {
startVideo();
_subtitleWriter.setVideoReceiver(_videoReceiver);
(3)QML 绑定: VideoManager::_initVideo() 在渲染同步阶段查找 videoContent / thermalVideo 控件,创建 qgcvideosinkbin 并 setVideoSink()。
6.1.3 视频源 URI 与协议映射
VideoManager::_updateSettings() 负责将配置或 MAVLink 自动发现转为 URI:
| 配置/MAVLink 类型 | URI 格式 | GStreamer Source |
|---|---|---|
| UDP H.264 | udp://0.0.0.0:5600 |
udpsrc + RTP H264 caps |
| UDP H.265 | udp265://0.0.0.0:5600 |
udpsrc + RTP H265 caps |
| RTSP | rtsp://host:554/live |
rtspsrc |
| TCP MPEG-TS | tcp://host:port |
tcpclientsrc + tsdemux |
| MPEG-TS UDP | mpegts://0.0.0.0:port |
udpsrc + tsdemux |
| Taisync 移动端 | tsusb://0.0.0.0:port |
专用 udpsrc |
| UVC 摄像头 | Qt QCamera |
非 GStreamer 路径 |
MAVLink 自动发现(QGCVideoStreamInfo)示例:
349:361:src/VideoStreaming/VideoManager.cc
switch(pInfo->type()) {
case VIDEO_STREAM_TYPE_RTSP:
_videoReceiver->setUri(pInfo->uri());
break;
case VIDEO_STREAM_TYPE_RTPUDP:
_videoReceiver->setUri(QStringLiteral("udp://0.0.0.0:%1").arg(pInfo->uri()));
break;
case VIDEO_STREAM_TYPE_MPEG_TS_H264:
_videoReceiver->setUri(QStringLiteral("mpegts://0.0.0.0:%1").arg(pInfo->uri()));
break;
手动配置 fallback 使用 VideoSettings 的 udpPort(默认 5600 )、rtspUrl、tcpUrl。
6.1.4 GStreamer 管道结构
逻辑拓扑(注释描述):
datasource(sourcebin) → tee → queue → decodebin → qgcvideosinkbin
└→ [录制分支: teepad → queue → mux → filesink]
sourcebin 内部:
udpsrc/rtspsrc/tcpclientsrc → [rtpjitterbuffer] → parsebin → [ghost pad]
或 tsdemux → parsebin(MPEG-TS)
sink bin(gstqgcvideosinkbin.c):
glupload → glcolorconvert → qmlglsink(绑定 QML GstGLVideoItem)
qmlglsink 的 widget 属性指向 QML 中的 GstGLVideoItem,实现 OpenGL 纹理零拷贝 渲染到 Qt Quick 场景图。
6.1.5 _makeSource() 协议细节
RTSP:
377:379:src/VideoStreaming/VideoReceiver.cc
g_object_set(source, "location", qPrintable(uri),
"latency", 17, "udp-reconnect", 1, "timeout", _udpReconnect_us, NULL);
latency=17:RTSP 内部 jitter 缓冲 17msudp-reconnect=1:RTP over UDP 断线重连timeout=5000000μs(5s):UDP 重连超时
UDP H.264 RTP caps:
application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264
parsebin + decodebin: 使用 GStreamer 自动插件选择(autoplug),自动匹配 rtph264depay → h264parse → avdec_h264 等,无需硬编码解码链。
6.1.6 start() 管道构建
685:711:src/VideoStreaming/VideoReceiver.cc
gst_bin_add_many(GST_BIN(_pipeline), source, _tee, queue, decoder, _videoSink, nullptr);
g_signal_connect(source, "pad-added", G_CALLBACK(newPadCB), _tee);
gst_element_link_many(_tee, queue, decoder, nullptr);
g_signal_connect(decoder, "pad-added", G_CALLBACK(newPadCB), _videoSink);
running = gst_element_set_state(_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE;
pad-added 回调: source 元素(尤其 rtspsrc)在协商完成后才产生 pad,通过 newPadCB 动态链接到 tee。
Bus 消息: _onBusMessage 处理 ERROR/EOS/STATE_CHANGED,触发 _handleError 自动重启。
6.1.7 RTSP/TCP 预连接探测
rtspsrc 若首次连接失败不会自动重试。QGC 用 QTcpSocket 轮询(5s 间隔)检测服务器可达,成功后才 start() 管道:
639:643:src/VideoStreaming/VideoReceiver.cc
if(!_serverPresent && useTcpConnection) {
_tcp_timer.start(100);
return;
}
6.1.8 QML 显示层
QGCVideoBackground.qml: 极简包装,仅声明 GstGLVideoItem + receiver 属性。
FlightDisplayViewVideo.qml:
- 绑定
QGroundControl.videoManager.videoReceiver - 根据
aspectRatio和videoFit(Fit Width / Fit Height / Stretch)计算显示尺寸 Loader延迟加载QGCVideoBackground(规避部分 Intel 驱动 OpenGL 崩溃)- 无视频时显示 "WAITING FOR VIDEO" / "VIDEO DISABLED"
- 双击切换全屏(
videoManager.fullScreen) - 支持 MAVLink 相机变焦(PinchArea →
QGCCameraControl)
6.1.9 双路视频(可见光 + 热成像)
VideoManager 维护两个独立 VideoReceiver:
_videoReceiver→videoContentwidget_thermalVideoReceiver→thermalVideowidget
MAVLink dynamicCameras() 分别提供 currentStreamInstance() 和 thermalStreamInstance(),支持 PIP 混合显示模式。
6.1.10 视频录制分支
startRecording() 从 tee 请求新 pad,挂接录制分支:
tee → [teepad] → queue → [probe: 等待 I 帧] → mux → filesink
关键帧等待(_keyframeWatch): 在收到第一个非 DELTA 帧(I 帧)前丢弃 buffer,设置 PTS offset 为 0,再挂接 filesink,保证录制文件可立即解码播放。
录制格式:mkv / mov / mp4(VideoSettings.recordingFormat)。
6.2 画面叠加 OSD 飞行信息
6.2.1 重要结论:实时 OSD vs 录制字幕
QGC 4.0 不在实时视频画面上叠加 MAVLink 遥测 OSD 。飞行信息叠加仅发生在 视频录制 时,通过 ASS 字幕文件 (.ass)写入,回放时可显示。
实时视频上仅有 QML 层叠加(非遥测):
- 三分构图网格线(
gridLines设置) - 等待/禁用提示文字
- 热成像 PIP 窗口
- 全屏/变焦 UI
6.2.2 SubtitleWriter --- 录制 OSD 实现
文件: VideoStreaming/SubtitleWriter.h/.cc
工作流程:
VideoReceiver.startRecording()
→ 管道运行,等待 I 帧
→ gotFirstRecordingKeyFrame 信号
→ SubtitleWriter._startCapturingTelemetry()
→ 读取 QSettings ValuesWidget/large + small(仪表板字段列表)
→ 创建 与视频同名的 .ass 文件
→ 1Hz QTimer 启动
→ _captureTelemetry() 每秒执行:
→ 从 activeVehicle 读取 Fact 值
→ 写入 ASS Dialogue 行(1920×1080 坐标系)
→ recordingChanged(false) → 停止写入
ASS 文件头(固定 1920×1080):
96:112:src/VideoStreaming/SubtitleWriter.cc
stream << QStringLiteral(
"[Script Info]\n"
...
"PlayResX: 1920\n"
"PlayResY: 1080\n"
...
"[Events]\n"
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
);
布局算法:
- 屏幕分为 3 列 (
nRows=3) - 每列显示若干 Fact 的 名称(右对齐) 和 数值+单位(左对齐)
- 使用 ASS 定位标签
\pos(x,1075)和\an3(右对齐) - 左上角显示当前日期
\pos(10,35)
数据来源绑定:
143:147:src/VideoStreaming/SubtitleWriter.cc
for (const auto& i : _values) {
valuesStrings << QStringLiteral("%2 %3").arg(vehicle->getFact(i)->cookedValueString())
.arg(vehicle->getFact(i)->cookedUnits());
namesStrings << QStringLiteral("%1:").arg(vehicle->getFact(i)->shortDescription());
}
字段列表来自用户在 Values 仪表板 中配置的 ValuesWidget/large 和 ValuesWidget/small(与 ValuePageWidget.qml 共享配置),默认包括相对高度、地速、飞行时间等。
采样率: _sampleRate = 1 Hz(注释说明 >1Hz 时多数播放器显示异常)。
6.2.3 实时 QML 叠加层
三分网格线(FlightDisplayViewVideo.qml):
95:122:src/FlightDisplay/FlightDisplayViewVideo.qml
Rectangle {
color: Qt.rgba(1,1,1,0.5)
x: parent.width * 0.33 // 竖线 1/3、2/3
visible: _showGrid && !QGroundControl.videoManager.fullScreen
}
Rectangle {
y: parent.height * 0.33 // 横线 1/3、2/3
}
由 VideoSettings.gridLines 控制(enum: Hide/Show)。
若需实现实时 OSD,扩展路径:
- 在
FlightDisplayViewVideo.qml的QGCVideoBackground上层叠加 QMLColumn/Repeater,绑定activeVehicle的 Fact(类似 Fly 视图仪表板) - 或修改 GStreamer 管道,在 decode 后插入
textoverlay/cairooverlay元素(需改 C++VideoReceiver) - Custom 插件可参考
custom-example/src/CustomVideoManager.cc
6.3 图传卡顿、延时优化
6.3.1 延迟来源分析
端到端视频延迟 ≈ 发送端编码缓冲 + 网络传输 + 接收端 jitterbuffer + decode + sink sync + 渲染。
QGC 可控的接收端延迟主要来自:
| 环节 | 默认行为 | 延迟影响 |
|---|---|---|
rtpjitterbuffer |
默认启用(RTP 流) | ~100-200ms |
rtspsrc latency |
17ms | 小 |
queue |
默认无限缓冲 | 可变 |
qmlglsink sync |
sync=true(跟随 clock) | 1-2 帧 |
| decodebin 内部缓冲 | 自动 | 可变 |
6.3.2 lowLatencyMode --- 核心低延迟开关
设置定义:
122:126:src/Settings/Video.SettingsGroup.json
"name": "lowLatencyMode",
"longDescription": "If this option is enabled, the rtpjitterbuffer is removed and the video sink is set to assynchronous mode, reducing the latency by about 200 ms.",
"defaultValue": false
三处生效:
(1)跳过 rtpjitterbuffer:
451:468:src/VideoStreaming/VideoReceiver.cc
if (probeRes & 2 && !_videoSettings->lowLatencyMode()->rawValue().toBool()) {
buffer = gst_element_factory_make("rtpjitterbuffer", nullptr);
// source → buffer → parser
} else {
// 低延迟:source → parser 直连
}
RTP pad 检测(_padProbe)识别 RTP 流后,非低延迟模式插入 jitterbuffer 重排序/缓冲。
(2)Video sink 异步模式:
630:630:src/VideoStreaming/VideoReceiver.cc
g_object_set(_videoSink, "sync", !_videoSettings->lowLatencyMode()->rawValue().toBool(), NULL);
sync=false 经 qgcvideosinkbin 转发到 qmlglsink,不等待系统 clock,收到帧即显示,牺牲帧率稳定性换取低延迟。
(3)设置变更自动重启:
234:237:src/VideoStreaming/VideoManager.cc
void VideoManager::_lowLatencyModeChanged()
{
restartVideo();
}
6.3.3 帧活性 Watchdog
探测机制:
979:979:src/VideoStreaming/VideoReceiver.cc
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _videoSinkProbe, this, nullptr);
每个 buffer 到达 sink 时更新 _lastFrameTime。
超时重启(_updateTimer,1Hz):
1329:1341:src/VideoStreaming/VideoReceiver.cc
if(_videoRunning) {
uint32_t timeout = _videoSettings->rtspTimeout()->rawValue().toUInt(); // 默认 2s
if(now - _lastFrameTime > timeout) {
stop();
_stop = false; // 允许 _updateTimer 自动 restart
}
} else {
if(!_stop && !_running && _uri.isEmpty() == false && streamEnabled) {
start(); // 自动重连
}
}
这解决了 RTSP 断流后管道僵死、画面冻结但不报错的问题。
6.3.4 错误自动重启
管道 ERROR 消息 → _handleError() → _restart_timer(1389ms 单次)→ VideoManager::restartVideo()
RTSP 预连接失败 → _tcp_timeout() → 5s 后重试 QTcpSocket 连接
EOS 消息 → _handleEOS() → 重启管道
6.3.5 其他优化机制
| 机制 | 位置 | 说明 |
|---|---|---|
queue 默认参数 |
start() |
TODO:建议 queue2 max-size-buffers=1 进一步降延迟 |
| ArduSub 自动低延迟 | Vehicle.cc:521-524 |
水下 ROV 默认 UDP H.264 + lowLatencyMode=true |
disableWhenDisarmed |
Vehicle.cc:1704-1707 |
上锁后停流,减少无效解码 CPU 占用 |
streamEnabled |
VideoSettings |
总开关,关闭则完全不建管道 |
| 双路流独立 Receiver | VideoManager |
主/热成像互不影响 |
| Loader 延迟加载 | FlightDisplayViewVideo.qml |
规避 Intel OpenGL 驱动崩溃 |
| 录制 I 帧等待 | _keyframeWatch |
避免录制文件开头花屏 |
| 磁盘空间管理 | _cleanupOldVideos() |
超 maxVideoSize 自动删旧文件 |
6.3.6 命令行对照测试
README 提供的 GStreamer 测试命令(VideoStreaming/README.md):
发送端:
bash
gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! \
x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600
接收端(低延迟对照):
bash
gst-launch-1.0 udpsrc port=5600 \
caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! \
rtph264depay ! h264parse ! avdec_h264 ! autovideosink sync=false
QGC 实际使用 parsebin + decodebin + qgcvideosinkbin,比固定 depay 链更通用但可能多一层缓冲。
6.3.7 卡顿排查建议
- 开启 lowLatencyMode(Settings → General → Video)
- 确认 UDP 端口无冲突(默认 5600,与 MAVROS/其他工具隔离)
- 减小发送端 GOP/关键帧间隔(I 帧间隔过大导致 decode 等待)
- 检查
_lastFrameTimewatchdog 日志(频繁 restart 说明链路不稳定) - RTSP 场景确认预连接成功 (
_serverPresent标志) - 移动端优先 UDP 而非 RTSP(RTSP 握手+TCP 开销更大)
- 关闭不必要的第二路流 (热成像
_thermalVideoReceiver) - CPU/GPU 解码能力 :
decodebin自动选择软解/硬解,弱设备可强制硬件解码插件
6.4 VideoSettings 配置项完整表
| Fact 名 | 类型 | 默认值 | 作用 |
|---|---|---|---|
videoSource |
string | "" | RTSP/UDP/TCP/MPEG-TS/UVC/Disabled |
udpPort |
uint16 | 5600 | UDP 绑定端口 |
rtspUrl |
string | "" | RTSP 地址 |
tcpUrl |
string | "" | TCP 地址 |
aspectRatio |
float | 1.777777 | 16:9 宽高比 |
videoFit |
enum | Fit Height | 显示缩放模式 |
gridLines |
enum | Hide | 三分网格线 |
streamEnabled |
bool | true | 流总开关 |
disableWhenDisarmed |
bool | false | 上锁后停流 |
lowLatencyMode |
bool | false | 低延迟模式 |
rtspTimeout |
uint32 | 2s | 无帧超时 |
recordingFormat |
enum | mkv | 录制容器 |
maxVideoSize |
uint32 | 10240MB | 录制空间上限 |
enableStorageLimit |
bool | false | 自动清理旧录制 |
6.6 关键方法速查
| 类 | 方法 | 作用 |
|---|---|---|
VideoManager |
startVideo() / stopVideo() / restartVideo() |
启停/重启 |
VideoManager |
_updateSettings() |
URI 协议映射 |
VideoManager |
_initVideo() / _makeVideoSink() |
绑定 QML widget |
VideoReceiver |
start() / stop() |
管道 PLAYING/NULL |
VideoReceiver |
_makeSource() |
按 URI 建 source bin |
VideoReceiver |
startRecording() / stopRecording() |
Tee 分支录制 |
VideoReceiver |
_updateTimer() |
帧超时 watchdog |
VideoReceiver |
_videoSinkProbe() |
帧到达探测 |
SubtitleWriter |
_captureTelemetry() |
写 ASS 遥测字幕 |
VideoSettings |
streamConfigured() |
配置完整性检查 |
6.7 本章小结
QGroundControl 4.0 的视频图传子系统以 GStreamer 管道 为核心,通过 VideoManager 统一管理配置与生命周期,VideoReceiver 按 URI 协议动态构建 source→tee→decode→sink 链路,最终经自定义 qgcvideosinkbin 渲染到 QML GstGLVideoItem。
OSD 方面 ,QGC 不在实时画面叠加遥测,而是通过 SubtitleWriter 在录制时以 ASS 字幕 写入 Fact 数据(1Hz,1920×1080 三列布局),回放时可显示;实时仅有网格线等 QML 叠加。
延迟优化 以 lowLatencyMode 为核心(移除 jitterbuffer + sink sync=false,约减 200ms),配合帧活性 watchdog(rtspTimeout)、错误自动重启(1389ms)、RTSP 预连接探测、ArduSub 默认低延迟等机制,在稳定性与实时性之间取得平衡。