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/.ccGStreamer 管道生命周期
VideoStreaming/VideoStreaming.ccGStreamer 初始化、插件注册
VideoStreaming/gstqgcvideosinkbin.c自定义 sink bin
VideoStreaming/gstqgc.cQGC GStreamer 插件注册
Settings/VideoSettings.h/.cc视频配置 Fact 组
Settings/Video.SettingsGroup.json默认值与枚举
FlightMap/QGCVideoBackground.qmlGstGLVideoItem 包装
FlightDisplay/FlightDisplayViewVideo.qmlFly 视图视频区域
Camera/QGCCameraManager.hMAVLink 相机/流信息

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():

    _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.264udp://0.0.0.0:5600udpsrc + RTP H264 caps
UDP H.265udp265://0.0.0.0:5600udpsrc + RTP H265 caps
RTSPrtsp://host:554/livertspsrc
TCP MPEG-TStcp://host:porttcpclientsrc + tsdemux
MPEG-TS UDPmpegts://0.0.0.0:portudpsrc + tsdemux
Taisync 移动端tsusb://0.0.0.0:port专用 udpsrc
UVC 摄像头Qt QCamera非 GStreamer 路径

MAVLink 自动发现(QGCVideoStreamInfo)示例:

            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:

    g_object_set(source, "location", qPrintable(uri),
                 "latency", 17, "udp-reconnect", 1, "timeout", _udpReconnect_us, NULL);
  • latency=17:RTSP 内部 jitter 缓冲 17ms
  • udp-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() 管道构建

        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() 管道:

    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 → videoContent widget
  • _thermalVideoReceiver → thermalVideo widget

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):

    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)

数据来源绑定:

    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):

                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,扩展路径:

  1. 在 FlightDisplayViewVideo.qml 的 QGCVideoBackground 上层叠加 QML Column/Repeater,绑定 activeVehicle 的 Fact(类似 Fly 视图仪表板)
  2. 或修改 GStreamer 管道,在 decode 后插入 textoverlay / cairooverlay 元素(需改 C++ VideoReceiver)
  3. Custom 插件可参考 custom-example/src/CustomVideoManager.cc

6.3 图传卡顿、延时优化

6.3.1 延迟来源分析

端到端视频延迟 ≈ 发送端编码缓冲 + 网络传输 + 接收端 jitterbuffer + decode + sink sync + 渲染。

QGC 可控的接收端延迟主要来自:

环节默认行为延迟影响
rtpjitterbuffer默认启用(RTP 流)~100-200ms
rtspsrc latency17ms小
queue默认无限缓冲可变
qmlglsink syncsync=true(跟随 clock)1-2 帧
decodebin 内部缓冲自动可变

6.3.2 lowLatencyMode — 核心低延迟开关

设置定义:

    "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:

        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 异步模式:

    g_object_set(_videoSink, "sync", !_videoSettings->lowLatencyMode()->rawValue().toBool(), NULL);

sync=false 经 qgcvideosinkbin 转发到 qmlglsink,不等待系统 clock,收到帧即显示,牺牲帧率稳定性换取低延迟。

(3)设置变更自动重启:

void VideoManager::_lowLatencyModeChanged()
{
    restartVideo();
}

6.3.3 帧活性 Watchdog

探测机制:

    gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _videoSinkProbe, this, nullptr);

每个 buffer 到达 sink 时更新 _lastFrameTime。

超时重启(_updateTimer,1Hz):

    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
disableWhenDisarmedVehicle.cc:1704-1707上锁后停流,减少无效解码 CPU 占用
streamEnabledVideoSettings总开关,关闭则完全不建管道
双路流独立 ReceiverVideoManager主/热成像互不影响
Loader 延迟加载FlightDisplayViewVideo.qml规避 Intel OpenGL 驱动崩溃
录制 I 帧等待_keyframeWatch避免录制文件开头花屏
磁盘空间管理_cleanupOldVideos()超 maxVideoSize 自动删旧文件

6.3.6 命令行对照测试

README 提供的 GStreamer 测试命令(VideoStreaming/README.md):

发送端:

gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! \
  x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600

接收端(低延迟对照):

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 卡顿排查建议

  1. 开启 lowLatencyMode(Settings → General → Video)
  2. 确认 UDP 端口无冲突(默认 5600,与 MAVROS/其他工具隔离)
  3. 减小发送端 GOP/关键帧间隔(I 帧间隔过大导致 decode 等待)
  4. 检查 _lastFrameTime watchdog 日志(频繁 restart 说明链路不稳定)
  5. RTSP 场景确认预连接成功(_serverPresent 标志)
  6. 移动端优先 UDP 而非 RTSP(RTSP 握手+TCP 开销更大)
  7. 关闭不必要的第二路流(热成像 _thermalVideoReceiver)
  8. CPU/GPU 解码能力:decodebin 自动选择软解/硬解,弱设备可强制硬件解码插件

6.4 VideoSettings 配置项完整表

Fact 名类型默认值作用
videoSourcestring“”RTSP/UDP/TCP/MPEG-TS/UVC/Disabled
udpPortuint165600UDP 绑定端口
rtspUrlstring“”RTSP 地址
tcpUrlstring“”TCP 地址
aspectRatiofloat1.77777716:9 宽高比
videoFitenumFit Height显示缩放模式
gridLinesenumHide三分网格线
streamEnabledbooltrue流总开关
disableWhenDisarmedboolfalse上锁后停流
lowLatencyModeboolfalse低延迟模式
rtspTimeoutuint322s无帧超时
recordingFormatenummkv录制容器
maxVideoSizeuint3210240MB录制空间上限
enableStorageLimitboolfalse自动清理旧录制

6.6 关键方法速查

类方法作用
VideoManagerstartVideo() / stopVideo() / restartVideo()启停/重启
VideoManager_updateSettings()URI 协议映射
VideoManager_initVideo() / _makeVideoSink()绑定 QML widget
VideoReceiverstart() / stop()管道 PLAYING/NULL
VideoReceiver_makeSource()按 URI 建 source bin
VideoReceiverstartRecording() / stopRecording()Tee 分支录制
VideoReceiver_updateTimer()帧超时 watchdog
VideoReceiver_videoSinkProbe()帧到达探测
SubtitleWriter_captureTelemetry()写 ASS 遥测字幕
VideoSettingsstreamConfigured()配置完整性检查

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 默认低延迟等机制,在稳定性与实时性之间取得平衡。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐