QGC 视频图传与流媒体开发

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 模式 VideoManagerQGCToolbox 两阶段初始化中注册
Factory 模式 QGCCorePlugin::createVideoReceiver() / createVideoManager()
Tee 分支模式 显示与录制共用 tee,动态挂接 filesink 分支
Observer(信号槽) videoRunningChangedgotFirstRecordingKeyFrame、Fact 变更触发 restartVideo()
Strategy 模式 按 URI 前缀选择不同 GStreamer source 元素
Watchdog 模式 _frameTimer + _videoSinkProbe 检测帧活性
延迟初始化 _initVideo() 在 QQuickWindow 渲染阶段绑定 widget

涉及语法与技术:

  • C++:QObjectQ_PROPERTY#if defined(QGC_GST_STREAMING) 条件编译
  • GStreamer C API:gst_element_factory_makeg_object_set、pad probe
  • QML:GstGLVideoItemorg.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)
  • 移动端静态注册插件:coreelementslibavrtprtspudpqmlglqgc
  • 注册 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 控件,创建 qgcvideosinkbinsetVideoSink()

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 使用 VideoSettingsudpPort(默认 5600 )、rtspUrltcpUrl

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)

qmlglsinkwidget 属性指向 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 缓冲 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),自动匹配 rtph264depayh264parseavdec_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
  • 根据 aspectRatiovideoFit(Fit Width / Fit Height / Stretch)计算显示尺寸
  • Loader 延迟加载 QGCVideoBackground(规避部分 Intel 驱动 OpenGL 崩溃)
  • 无视频时显示 "WAITING FOR VIDEO" / "VIDEO DISABLED"
  • 双击切换全屏(videoManager.fullScreen
  • 支持 MAVLink 相机变焦(PinchArea → QGCCameraControl

6.1.9 双路视频(可见光 + 热成像)

VideoManager 维护两个独立 VideoReceiver

  • _videoReceivervideoContent widget
  • _thermalVideoReceiverthermalVideo 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 / mp4VideoSettings.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/largeValuesWidget/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,扩展路径:

  1. FlightDisplayViewVideo.qmlQGCVideoBackground 上层叠加 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 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=falseqgcvideosinkbin 转发到 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 卡顿排查建议

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

相关推荐
小鼻子的猫5 小时前
独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生
架构
咖啡八杯5 小时前
GoF设计模式——命令模式
java·设计模式·架构
candyTong6 小时前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
doiito20 小时前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
烬羽21 小时前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构
白鲸开源1 天前
一文读懂DolphinScheduler插件机制:如何轻松扩展任务类型与数据源
java·架构·github
棒槌开发师1 天前
动态组件设计(elpis)
架构
得物技术1 天前
从表单到 Agent:得物社区活动搭建的 AI 实践之路
人工智能·架构·agent
Ausra无忧1 天前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构