diff --git a/.github/build-config.json b/.github/build-config.json index 967855e666f2..7abe5a4bc580 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -37,18 +37,22 @@ "common": [ "app", "coreelements", + "dtls", "isomp4", "libav", "matroska", "mpegtsdemux", "multifile", + "nice", "opengl", "openh264", "playback", + "rswebrtc", "rtp", "rtpmanager", "rtsp", "sdpelem", + "srtp", "tcp", "typefindfunctions", "udp", @@ -56,7 +60,8 @@ "vpx", "videoconvertscale", "videoconvert", - "videoscale" + "videoscale", + "webrtc" ], "android": ["androidmedia", "dav1d"], "apple": ["applemedia", "dav1d"], diff --git a/cmake/GStreamer/Orchestrator.cmake b/cmake/GStreamer/Orchestrator.cmake index a78dfe0fae4b..27e46bbd2fd0 100644 --- a/cmake/GStreamer/Orchestrator.cmake +++ b/cmake/GStreamer/Orchestrator.cmake @@ -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) @@ -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}) diff --git a/src/AppSettings/pages/Video.SettingsUI.json b/src/AppSettings/pages/Video.SettingsUI.json index f2f5837398e3..ef9ed1ad4419 100644 --- a/src/AppSettings/pages/Video.SettingsUI.json +++ b/src/AppSettings/pages/Video.SettingsUI.json @@ -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" diff --git a/src/Settings/Video.SettingsGroup.json b/src/Settings/Video.SettingsGroup.json index 9a9c0cd4c213..fc920b6a8769 100644 --- a/src/Settings/Video.SettingsGroup.json +++ b/src/Settings/Video.SettingsGroup.json @@ -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", @@ -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).", diff --git a/src/Settings/VideoSettings.cc b/src/Settings/VideoSettings.cc index ae43d03c6c1a..683b91765809 100644 --- a/src/Settings/VideoSettings.cc +++ b/src/Settings/VideoSettings.cc @@ -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); @@ -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) { @@ -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(); diff --git a/src/Settings/VideoSettings.h b/src/Settings/VideoSettings.h index 6bd21c19b1b5..3fad8df0d624 100644 --- a/src/Settings/VideoSettings.h +++ b/src/Settings/VideoSettings.h @@ -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) @@ -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) @@ -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; } @@ -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"); diff --git a/src/VideoManager/VideoManager.cc b/src/VideoManager/VideoManager.cc index 72d47e207f7d..0a758b2aadf3 100644 --- a/src/VideoManager/VideoManager.cc +++ b/src/VideoManager/VideoManager.cc @@ -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(); }); @@ -500,6 +501,7 @@ bool VideoManager::isStreamSource() const VideoSettings::videoSourceUDPH264, VideoSettings::videoSourceUDPH265, VideoSettings::videoSourceRTSP, + VideoSettings::videoSourceWHEP, VideoSettings::videoSourceTCP, VideoSettings::videoSourceMPEGTS, VideoSettings::videoSource3DRSolo, @@ -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; @@ -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; @@ -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) { diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc index 6176704bfd69..43cb861907ba 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc @@ -119,6 +119,31 @@ void forEachPlugin(GstRegistry* registry, const std::function& 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) { diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h index 568884a98985..fb6b0c3512f9 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h @@ -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. diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc b/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc index 9a61cdacb3f1..88f0ba143bda 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc @@ -1,8 +1,10 @@ #include "GstSourceFactory.h" +#include #include #include #include +#include #include #include "GStreamerHelpers.h" @@ -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) @@ -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; + } + + 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(); @@ -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. @@ -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(p); }); (void) g_signal_connect(upstream, "pad-added", G_CALLBACK(linkPad), ctx); @@ -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; } @@ -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 @@ -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; } } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.h b/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.h index 00c12ed127b5..51a1a5b4d841 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.h @@ -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 diff --git a/test/VideoManager/GStreamer/GStreamerTest.cc b/test/VideoManager/GStreamer/GStreamerTest.cc index 9f539c615f21..c3ee81b77d17 100644 --- a/test/VideoManager/GStreamer/GStreamerTest.cc +++ b/test/VideoManager/GStreamer/GStreamerTest.cc @@ -569,6 +569,7 @@ QGC_GST_SKIP_TEST(_testSourceFactoryUdpRtpJitterBuffer) QGC_GST_SKIP_TEST(_testSourceFactoryJitterBufferNone) QGC_GST_SKIP_TEST(_testSourceFactoryNoRetransmission) QGC_GST_SKIP_TEST(_testSourceFactoryRtspExcludesStaticJitterBuffer) +QGC_GST_SKIP_TEST(_testSourceFactoryWhepExcludesStaticJitterBuffer) QGC_GST_SKIP_TEST(_testSourceFactoryRejectsBadUri) QGC_GST_SKIP_TEST(_testSourceFactoryTcpMpegTs) QGC_GST_SKIP_TEST(_testSourceFactoryRejectsBadTcpUri) diff --git a/test/VideoManager/GStreamer/GStreamerTest.h b/test/VideoManager/GStreamer/GStreamerTest.h index f1658fe73461..bb0374194f2b 100644 --- a/test/VideoManager/GStreamer/GStreamerTest.h +++ b/test/VideoManager/GStreamer/GStreamerTest.h @@ -82,6 +82,7 @@ private slots: void _testSourceFactoryJitterBufferNone(); void _testSourceFactoryNoRetransmission(); void _testSourceFactoryRtspExcludesStaticJitterBuffer(); + void _testSourceFactoryWhepExcludesStaticJitterBuffer(); void _testSourceFactoryRejectsBadUri(); void _testSourceFactoryTcpMpegTs(); void _testSourceFactoryRejectsBadTcpUri(); diff --git a/test/VideoManager/GStreamer/SourceFactory/GStreamerSourceFactoryTest.cc b/test/VideoManager/GStreamer/SourceFactory/GStreamerSourceFactoryTest.cc index 35895f2a9dba..942880973c1e 100644 --- a/test/VideoManager/GStreamer/SourceFactory/GStreamerSourceFactoryTest.cc +++ b/test/VideoManager/GStreamer/SourceFactory/GStreamerSourceFactoryTest.cc @@ -132,6 +132,40 @@ void GStreamerTest::_testSourceFactoryRtspExcludesStaticJitterBuffer() "rtspsrc owns its internal jitterbuffer; the factory must not add a second one"); } +void GStreamerTest::_testSourceFactoryWhepExcludesStaticJitterBuffer() +{ +#ifndef QGC_GST_WHEP + QSKIP("QGC_GST_WHEP disabled"); +#else + if (!gst_element_factory_find("whepclientsrc")) { + QSKIP("whepclientsrc plugin unavailable"); + } + + GStreamer::SourceFactory::Config config; + config.jitterBuffer = GStreamer::SourceFactory::JitterBuffer::DropOnLatency; + + GstElement* bin = GStreamer::SourceFactory::create(QStringLiteral("http://127.0.0.1:8889/whep"), config); + QVERIFY(bin); + const auto cleanup = qScopeGuard([&] { gst_object_unref(bin); }); + + GstElement* src = findChildByFactoryName(bin, "whepclientsrc"); + QVERIFY2(src, "http:// must build a whepclientsrc when WHEP is enabled"); + + GObject* signaller = nullptr; + g_object_get(src, "signaller", &signaller, nullptr); + QVERIFY2(signaller, "whepclientsrc must provide a signaller object"); + + gchar* endpoint = nullptr; + g_object_get(signaller, "whep-endpoint", &endpoint, nullptr); + QCOMPARE(QString::fromUtf8(endpoint), QStringLiteral("http://127.0.0.1:8889/whep")); + g_free(endpoint); + g_object_unref(signaller); + + QVERIFY2(!findChildByFactoryName(bin, "rtpjitterbuffer"), + "whepclientsrc owns its internal jitter buffering; the factory must not add a second one"); +#endif +} + void GStreamerTest::_testSourceFactoryRejectsBadUri() { ignoreLogMessage("Video.GStreamer.GstSourceFactory", QtCriticalMsg,