Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/build-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,31 @@
"common": [
"app",
"coreelements",
"dtls",
"isomp4",
"libav",
"matroska",
"mpegtsdemux",
"multifile",
"nice",
"opengl",
"openh264",
"playback",
"rswebrtc",
"rtp",
"rtpmanager",
"rtsp",
"sdpelem",
"srtp",
"tcp",
"typefindfunctions",
"udp",
"videoparsersbad",
"vpx",
"videoconvertscale",
"videoconvert",
"videoscale"
"videoscale",
"webrtc"
],
"android": ["androidmedia", "dav1d"],
"apple": ["applemedia", "dav1d"],
Expand Down
15 changes: 15 additions & 0 deletions cmake/GStreamer/Orchestrator.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ gstreamer_current_platform_key(_qgc_gst_plat_key)
gstreamer_plugins_for(PLATFORM "${_qgc_gst_plat_key}" OUT_VAR GSTREAMER_PLUGINS)
unset(_qgc_gst_plat_key)

# Linking two or more Rust GStreamer plugins statically, such as dav1d and
# rswebrtc, is not supported out of the box because each static library
# includes duplicate Rust objects. Prune the WebRTC/WHEP stack before Android's
# mobile target snapshots GSTREAMER_PLUGINS into whole-archived static libs.
#
# https://centricular.com/devlog/2025-11/dragonfire/
# https://www.amyspark.me/blog/posts/2025/11/07/dragonfire.html
if(GStreamer_USE_STATIC_LIBS)
list(REMOVE_ITEM GSTREAMER_PLUGINS dtls nice rswebrtc srtp webrtc)
endif()

if(ANDROID)
set(GStreamer_Mobile_MODULE_NAME gstreamer_android)
set(G_IO_MODULES openssl)
Expand Down Expand Up @@ -285,6 +296,10 @@ foreach(plugin IN LISTS GSTREAMER_PLUGINS)
set(GST_PLUGIN_${plugin}_FOUND ${_gst_plugin_found})
endforeach()

if(GST_PLUGIN_rswebrtc_FOUND)
target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_WHEP)
endif()

if(NOT GStreamer_USE_STATIC_LIBS AND NOT GStreamer_USE_XCFRAMEWORK AND EXISTS "${GSTREAMER_PLUGIN_PATH}")
set(_gst_check_plugins "${GSTREAMER_PLUGINS}")
gstreamer_filter_alternates(IN_OUT_PLUGINS _gst_check_plugins AVAILABLE ${_gst_available_basenames})
Expand Down
6 changes: 5 additions & 1 deletion src/AppSettings/pages/Video.SettingsUI.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@
},
{
"heading": "Connection",
"keywords": ["rtsp", "tcp", "udp", "mpegts", "video url", "stream url"],
"keywords": ["rtsp", "whep", "tcp", "udp", "mpegts", "video url", "stream url"],
"showWhen": "!sourceDisabled && !autoStreamConfig",
"controls": [
{
"setting": "videoSettings.rtspUrl",
"showWhen": "videoSource === QGroundControl.settingsManager.videoSettings.rtspVideoSource"
},
{
"setting": "videoSettings.whepUrl",
"showWhen": "videoSource === QGroundControl.settingsManager.videoSettings.whepVideoSource"
},
{
"setting": "videoSettings.tcpUrl",
"showWhen": "videoSource === QGroundControl.settingsManager.videoSettings.tcpVideoSource"
Expand Down
13 changes: 11 additions & 2 deletions src/Settings/Video.SettingsGroup.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"QGC.MetaData.Facts": [
{
"name": "videoSource",
"shortDesc": "Source for video stream (UDP, TCP, RTSP, or connected USB camera).",
"longDesc": "Source for video. UDP, TCP, RTSP and UVC Cameras may be supported depending on Vehicle and ground station version.",
"shortDesc": "Source for video stream (UDP, TCP, RTSP, WHEP, or connected USB camera).",
"longDesc": "Source for video. UDP, TCP, RTSP, WHEP and UVC Cameras may be supported depending on Vehicle and ground station version.",
"type": "string",
"default": "",
"label": "Source",
Expand All @@ -29,6 +29,15 @@
"label": "RTSP URL",
"keywords": "rtsp,video url,stream url"
},
{
"name": "whepUrl",
"shortDesc": "HTTP(S) address for WHEP (WebRTC) video stream (e.g. http://192.168.42.1:8889/live/whep).",
"longDesc": "WHEP url address and port to bind to for video stream. Example: http://192.168.42.1:8889/live/whep",
"type": "string",
"default": "",
"label": "WHEP URL",
"keywords": "whep,webrtc,http,https,video url,stream url"
},
{
"name": "tcpUrl",
"shortDesc": "Network address and port for TCP video stream (e.g. 192.168.143.200:3001).",
Expand Down
17 changes: 17 additions & 0 deletions src/Settings/VideoSettings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ DECLARE_SETTINGGROUP(Video, "Video")
// Setup enum values for videoSource settings into meta data
QVariantList videoSourceList;
videoSourceList.append(videoSourceRTSP);
#ifdef QGC_GST_WHEP
videoSourceList.append(videoSourceWHEP);
#endif
videoSourceList.append(videoSourceUDPH264);
videoSourceList.append(videoSourceUDPH265);
videoSourceList.append(videoSourceTCP);
Expand Down Expand Up @@ -220,6 +223,15 @@ DECLARE_SETTINGSFACT_NO_FUNC(VideoSettings, rtspUrl)
return _rtspUrlFact;
}

DECLARE_SETTINGSFACT_NO_FUNC(VideoSettings, whepUrl)
{
if (!_whepUrlFact) {
_whepUrlFact = _createSettingsFact(whepUrlName);
connect(_whepUrlFact, &Fact::valueChanged, this, &VideoSettings::_configChanged);
}
return _whepUrlFact;
}

DECLARE_SETTINGSFACT_NO_FUNC(VideoSettings, tcpUrl)
{
if (!_tcpUrlFact) {
Expand Down Expand Up @@ -251,6 +263,11 @@ bool VideoSettings::streamConfigured(void)
qCDebug(VideoSettingsLog) << "Testing configuration for RTSP Stream:" << rtspUrl()->rawValue().toString();
return !rtspUrl()->rawValue().toString().isEmpty();
}
//-- If WHEP, check for URL
if(vSource == videoSourceWHEP) {
qCDebug(VideoSettingsLog) << "Testing configuration for WHEP Stream:" << whepUrl()->rawValue().toString();
return !whepUrl()->rawValue().toString().isEmpty();
}
//-- If TCP, check for URL
if(vSource == videoSourceTCP) {
qCDebug(VideoSettingsLog) << "Testing configuration for TCP Stream:" << tcpUrl()->rawValue().toString();
Expand Down
4 changes: 4 additions & 0 deletions src/Settings/VideoSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class VideoSettings : public SettingsGroup
DEFINE_SETTINGFACT(udpUrl)
DEFINE_SETTINGFACT(tcpUrl)
DEFINE_SETTINGFACT(rtspUrl)
DEFINE_SETTINGFACT(whepUrl)
DEFINE_SETTINGFACT(aspectRatio)
DEFINE_SETTINGFACT(videoFit)
DEFINE_SETTINGFACT(gridLines)
Expand All @@ -37,6 +38,7 @@ class VideoSettings : public SettingsGroup

Q_PROPERTY(bool streamConfigured READ streamConfigured NOTIFY streamConfiguredChanged)
Q_PROPERTY(QString rtspVideoSource READ rtspVideoSource CONSTANT)
Q_PROPERTY(QString whepVideoSource READ whepVideoSource CONSTANT)
Q_PROPERTY(QString udp264VideoSource READ udp264VideoSource CONSTANT)
Q_PROPERTY(QString udp265VideoSource READ udp265VideoSource CONSTANT)
Q_PROPERTY(QString tcpVideoSource READ tcpVideoSource CONSTANT)
Expand All @@ -45,6 +47,7 @@ class VideoSettings : public SettingsGroup

bool streamConfigured ();
QString rtspVideoSource () { return videoSourceRTSP; }
QString whepVideoSource () { return videoSourceWHEP; }
QString udp264VideoSource () { return videoSourceUDPH264; }
QString udp265VideoSource () { return videoSourceUDPH265; }
QString tcpVideoSource () { return videoSourceTCP; }
Expand All @@ -59,6 +62,7 @@ class VideoSettings : public SettingsGroup
static constexpr const char* videoSourceNoVideo = QT_TRANSLATE_NOOP("VideoSettings", "No Video Available");
static constexpr const char* videoDisabled = QT_TRANSLATE_NOOP("VideoSettings", "Video Stream Disabled");
static constexpr const char* videoSourceRTSP = QT_TRANSLATE_NOOP("VideoSettings", "RTSP Video Stream");
static constexpr const char* videoSourceWHEP = QT_TRANSLATE_NOOP("VideoSettings", "WHEP Video Stream");
static constexpr const char* videoSourceUDPH264 = QT_TRANSLATE_NOOP("VideoSettings", "UDP h.264 Video Stream");
static constexpr const char* videoSourceUDPH265 = QT_TRANSLATE_NOOP("VideoSettings", "UDP h.265 Video Stream");
static constexpr const char* videoSourceTCP = QT_TRANSLATE_NOOP("VideoSettings", "TCP-MPEG2 Video Stream");
Expand Down
15 changes: 12 additions & 3 deletions src/VideoManager/VideoManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ void VideoManager::init(QQuickWindow *mainWindow)
(void) connect(_videoSettings->videoSource(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
(void) connect(_videoSettings->udpUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
(void) connect(_videoSettings->rtspUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
(void) connect(_videoSettings->whepUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
(void) connect(_videoSettings->tcpUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
(void) connect(_videoSettings->aspectRatio(), &Fact::rawValueChanged, this, &VideoManager::aspectRatioChanged);
(void) connect(_videoSettings->lowLatencyMode(), &Fact::rawValueChanged, this, [this](const QVariant &value) { Q_UNUSED(value); _restartAllVideos(); });
Expand Down Expand Up @@ -500,6 +501,7 @@ bool VideoManager::isStreamSource() const
VideoSettings::videoSourceUDPH264,
VideoSettings::videoSourceUDPH265,
VideoSettings::videoSourceRTSP,
VideoSettings::videoSourceWHEP,
VideoSettings::videoSourceTCP,
VideoSettings::videoSourceMPEGTS,
VideoSettings::videoSource3DRSolo,
Expand Down Expand Up @@ -600,9 +602,7 @@ bool VideoManager::_updateAutoStream(VideoReceiver *receiver)
case VIDEO_STREAM_TYPE_RTSP:
source = VideoSettings::videoSourceRTSP;
url = pInfo->uri();
if (source == VideoSettings::videoSourceRTSP) {
_videoSettings->rtspUrl()->setRawValue(url);
}
_videoSettings->rtspUrl()->setRawValue(url);
break;
case VIDEO_STREAM_TYPE_TCP_MPEG:
source = VideoSettings::videoSourceTCP;
Expand All @@ -621,6 +621,13 @@ bool VideoManager::_updateAutoStream(VideoReceiver *receiver)
source = VideoSettings::videoSourceMPEGTS;
url = pInfo->uri().contains("mpegts://") ? pInfo->uri() : QStringLiteral("mpegts://0.0.0.0:%1").arg(pInfo->uri());
break;
#ifdef QGC_GST_WHEP
case VIDEO_STREAM_TYPE_WHEP:
source = VideoSettings::videoSourceWHEP;
url = pInfo->uri();
_videoSettings->whepUrl()->setRawValue(url);
break;
#endif
default:
qCWarning(VideoManagerLog) << "Unknown VIDEO_STREAM_TYPE";
source = VideoSettings::videoSourceNoVideo;
Expand Down Expand Up @@ -701,6 +708,8 @@ bool VideoManager::_updateSettings(VideoReceiver *receiver)
settingsChanged |= _updateVideoUri(receiver, QStringLiteral("mpegts://%1").arg(_videoSettings->udpUrl()->rawValue().toString()));
} else if (source == VideoSettings::videoSourceRTSP) {
settingsChanged |= _updateVideoUri(receiver, _videoSettings->rtspUrl()->rawValue().toString());
} else if (source == VideoSettings::videoSourceWHEP) {
settingsChanged |= _updateVideoUri(receiver, _videoSettings->whepUrl()->rawValue().toString());
} else if (source == VideoSettings::videoSourceTCP) {
settingsChanged |= _updateVideoUri(receiver, QStringLiteral("tcp://%1").arg(_videoSettings->tcpUrl()->rawValue().toString()));
} else if (source == VideoSettings::videoSource3DRSolo) {
Expand Down
25 changes: 25 additions & 0 deletions src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@ void forEachPlugin(GstRegistry* registry, const std::function<void(GstPlugin*)>&
gst_plugin_list_free(plugins);
}

bool isValidWhepUri(const gchar* uri_str)
{
if (!uri_str) {
return false;
}

if (!gst_uri_is_valid(uri_str)) {
return false;
}

if (!gst_uri_has_protocol(uri_str, "http") && !gst_uri_has_protocol(uri_str, "https")) {
return false;
}

GstUri* uri = gst_uri_from_string(uri_str);
if (!uri) {
return false;
}

const gchar* host = gst_uri_get_host(uri);
const bool hasHost = (host && host[0] != '\0');
gst_uri_unref(uri);
return hasHost;
}

bool isHardwareDecoderFactory(GstElementFactory* factory)
{
if (!factory) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace GStreamer {
bool isValidRtspUri(const gchar* uri_str);
bool isValidWhepUri(const gchar* uri_str);

/// Dump @p pipeline's graph as a rotating .dot under CacheLocation/qgc-pipeline-dot/ for field reports.
/// Returns empty (no-op) when GST_DEBUG_DUMP_DOT_DIR is set or on I/O failure.
Expand Down
51 changes: 45 additions & 6 deletions src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "GstSourceFactory.h"

#include <QtCore/QByteArray>
#include <QtCore/QFile>
#include <QtCore/QUrl>
#include <gst/gst.h>
#include <gst/gstutils.h>
#include <gst/rtsp/gstrtsptransport.h>

#include "GStreamerHelpers.h"
Expand Down Expand Up @@ -159,7 +161,7 @@ struct DynamicLinkContext
GstElement* binParser;
Config config;
guint latencyMs;
bool allowJitterBuffer; // false for RTSP (rtspsrc has its own internal jitterbuffer)
bool allowJitterBuffer; // false when the source already owns its receive-side jitter buffering
};

void linkPad(GstElement* element, GstPad* pad, gpointer data)
Expand Down Expand Up @@ -309,6 +311,32 @@ GstElement* buildRtspSource(const QString& uri, const QUrl& sourceUrl, const Con
return source;
}

#ifdef QGC_GST_WHEP
GstElement* buildWhepSource(const QString& uri, const QUrl& sourceUrl, const Config& config)
{
const QByteArray whepEndpoint = uri.toUtf8();
if (!GStreamer::isValidWhepUri(whepEndpoint.constData())) {
qCCritical(GstSourceFactoryLog) << "Invalid WHEP URI:" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
return nullptr;
Comment on lines +315 to +320
}

GstElement* source = gst_element_factory_make("whepclientsrc", "source");
if (!source) {
qCCritical(GstSourceFactoryLog) << "gst_element_factory_make('whepclientsrc') failed";
return nullptr;
}

g_object_set(source, "do-retransmission", config.doRetransmission ? TRUE : FALSE, nullptr);
gst_util_set_object_arg(G_OBJECT(source), "audio-codecs", "< >");

GObject* signaller = nullptr;
g_object_get(source, "signaller", &signaller, nullptr);
g_object_set(signaller, "whep-endpoint", whepEndpoint.constData(), nullptr);
g_object_unref(signaller);
return source;
}
#endif

GstElement* buildTcpSource(const QUrl& sourceUrl)
{
const int port = sourceUrl.port();
Expand Down Expand Up @@ -401,7 +429,7 @@ GstElement* buildUdpSource(const QUrl& sourceUrl, bool isUdpH264, bool isUdpH265
// Wire upstream → (optional rtpjitterbuffer) → binParser, topology chosen by RTP probe (MPEG-TS
// links via pad-added). Created elements join @p bin; returns false (logged) on failure.
bool linkSourceToParser(GstElement* bin, GstElement* upstream, GstElement* binParser, const Config& config,
guint latencyMs, bool isMpegTs, bool isRtsp)
guint latencyMs, bool isMpegTs, bool allowDynamicJitterBuffer)
{
// MPEG-TS exposes elementary streams via tsdemux dynamic src pads (none at NULL state) and
// isn't raw RTP, so skip the RTP probe and link via pad-added when the video pad appears.
Expand Down Expand Up @@ -438,8 +466,9 @@ bool linkSourceToParser(GstElement* bin, GstElement* upstream, GstElement* binPa
}
} else {
// linkPad applies the jitter-buffer policy itself when an application/x-rtp pad appears on a
// non-RTSP source. Context lifetime is tied to binParser (freed on its finalize).
auto* ctx = new DynamicLinkContext{binParser, config, latencyMs, !isRtsp};
// source that does not already own its receive-side jitter buffering. Context lifetime is
// tied to binParser (freed on its finalize).
auto* ctx = new DynamicLinkContext{binParser, config, latencyMs, allowDynamicJitterBuffer};
g_object_set_data_full(G_OBJECT(binParser), "qgc-dynamic-link-ctx", ctx,
[](gpointer p) { delete static_cast<DynamicLinkContext*>(p); });
(void) g_signal_connect(upstream, "pad-added", G_CALLBACK(linkPad), ctx);
Expand Down Expand Up @@ -499,12 +528,17 @@ GstElement* create(const QString& uri, const Config& config)
const QString scheme = sourceUrl.scheme().toLower();

const bool isRtsp = scheme.startsWith(QLatin1String("rtsp"));
#ifdef QGC_GST_WHEP
const bool isWhep = scheme.startsWith(QLatin1String("http"));
#else
const bool isWhep = false;
#endif
const bool isUdpH264 = (scheme == QLatin1String("udp"));
const bool isUdpH265 = (scheme == QLatin1String("udp265"));
const bool isUdpMPEGTS = (scheme == QLatin1String("mpegts"));
const bool isTcpMPEGTS = (scheme == QLatin1String("tcp"));

if (!isRtsp && !isUdpH264 && !isUdpH265 && !isUdpMPEGTS && !isTcpMPEGTS) {
if (!isRtsp && !isWhep && !isUdpH264 && !isUdpH265 && !isUdpMPEGTS && !isTcpMPEGTS) {
qCWarning(GstSourceFactoryLog) << "Unsupported URI scheme:" << scheme << "in" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
return nullptr;
}
Expand All @@ -521,6 +555,10 @@ GstElement* create(const QString& uri, const Config& config)
do {
if (isRtsp) {
source = buildRtspSource(uri, sourceUrl, config, latencyMs);
#ifdef QGC_GST_WHEP
} else if (isWhep) {
source = buildWhepSource(uri, sourceUrl, config);
#endif
} else if (isTcpMPEGTS) {
source = buildTcpSource(sourceUrl);
} else { // isUdpH264 || isUdpH265 || isUdpMPEGTS
Expand Down Expand Up @@ -617,7 +655,8 @@ GstElement* create(const QString& uri, const Config& config)
break;
}
} else {
if (!linkSourceToParser(bin, upstream, binParser, config, latencyMs, isTcpMPEGTS || isUdpMPEGTS, isRtsp)) {
if (!linkSourceToParser(bin, upstream, binParser, config, latencyMs,
isTcpMPEGTS || isUdpMPEGTS, !(isRtsp || isWhep))) {
break;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ struct Config
};

/// Build a source bin (`source` [+ `tsdemux`] [+ `rtpjitterbuffer`] + `parsebin`)
/// for `uri`. Supported schemes: rtsp/rtspt, tcp:// (MPEG-TS), udp:// (H.264 RTP),
/// udp265:// (H.265 RTP), mpegts:// (MPEG-TS over UDP).
/// for `uri`. Supported schemes: rtsp/rtspt, http(s) WHEP, tcp:// (MPEG-TS),
/// udp:// (H.264 RTP), udp265:// (H.265 RTP), mpegts:// (MPEG-TS over UDP).
///
/// Ghost pads on the returned bin are wired lazily; for `rtspsrc`/`tsdemux`/`parsebin`
/// they appear only after upstream produces pads, so callers must connect any
Expand Down
Loading
Loading