QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GStreamerHelpers.cc
Go to the documentation of this file.
1#include "GStreamerHelpers.h"
2
3#include <QtCore/QByteArray>
4#include <QtCore/QDateTime>
5#include <QtCore/QDir>
6#include <QtCore/QFile>
7#include <QtCore/QFileInfo>
8#include <QtCore/QLatin1String>
9#include <QtCore/QStandardPaths>
10#include <QtCore/QString>
11#include <QtCore/QStringList>
12#include <QtQuick/QQuickWindow>
13#include <QtQuick/QSGRendererInterface>
14#include <array>
15#include <gst/rtsp/gstrtspurl.h>
16#include <span>
17
18#ifdef Q_OS_WIN
19#include <iterator>
20#if defined(QGC_HAS_ANY_GPU_PATH)
21#include <rhi/qrhi.h>
22
24#endif
25#endif
26
27#include "GStreamer.h"
28#include "GstScoped.h"
29#include "QGCLoggingCategory.h"
30
31QGC_LOGGING_CATEGORY(GStreamerHelpersLog, "Video.GStreamer.GStreamerHelpers")
32
33namespace GStreamer {
34
35bool isValidRtspUri(const gchar* uri_str)
36{
37 if (!uri_str) {
38 return false;
39 }
40
41 if (!gst_uri_is_valid(uri_str)) {
42 return false;
43 }
44
45 GstRTSPUrl* url = nullptr;
46 const GstRTSPResult res = gst_rtsp_url_parse(uri_str, &url);
47 if ((res != GST_RTSP_OK) || !url) {
48 if (url) {
49 gst_rtsp_url_free(url);
50 }
51 return false;
52 }
53
54 const bool hasHost = (url->host && url->host[0] != '\0');
55 gst_rtsp_url_free(url);
56 return hasHost;
57}
58
59QString writePipelineDot(GstElement* pipeline, const char* tag)
60{
61 // kMaxDotFiles: cap retained .dot pipeline graphs; below 5 makes retro-debugging hard.
62 constexpr int kMaxDotFiles = 10;
63
64 if (!pipeline)
65 return {};
66 if (!qgetenv("GST_DEBUG_DUMP_DOT_DIR").isEmpty()) {
67 return {};
68 }
69 const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
70 if (cacheRoot.isEmpty())
71 return {};
72 QDir dir(cacheRoot + QStringLiteral("/qgc-pipeline-dot"));
73 if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
74 qCWarning(GStreamerHelpersLog) << "Failed to create" << dir.absolutePath();
75 return {};
76 }
77
78 // Rotate: remove oldest .dot files until under cap. Sort by mtime ascending.
79 QFileInfoList existing = dir.entryInfoList(QStringList{QStringLiteral("*.dot")},
80 QDir::Files, QDir::Time | QDir::Reversed);
81 while (existing.size() >= kMaxDotFiles) {
82 QFile::remove(existing.takeFirst().absoluteFilePath());
83 }
84
85 gchar* data = gst_debug_bin_to_dot_data(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL);
86 if (!data)
87 return {};
88 const QString fileName = QStringLiteral("%1-%2.dot")
89 .arg(QString::fromLatin1(tag),
90 QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss-zzz")));
91 const QString fullPath = dir.absoluteFilePath(fileName);
92 const QByteArray dotData = QByteArray::fromRawData(data, static_cast<qsizetype>(qstrlen(data)));
93 bool wrote = false;
94 QFile out(fullPath);
95 if (out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
96 wrote = (out.write(dotData) == static_cast<qint64>(dotData.size()));
97 if (!out.flush()) {
98 wrote = false;
99 }
100 out.close();
101 if (out.error() != QFileDevice::NoError) {
102 wrote = false;
103 }
104 }
105 g_free(data);
106 return wrote ? fullPath : QString{};
107}
108
109void forEachPlugin(GstRegistry* registry, const std::function<void(GstPlugin*)>& visitor)
110{
111 if (!registry || !visitor)
112 return;
113 GList* plugins = gst_registry_get_plugin_list(registry);
114 for (GList* node = plugins; node != nullptr; node = node->next) {
115 GstPlugin* plugin = static_cast<GstPlugin*>(node->data);
116 if (plugin)
117 visitor(plugin);
118 }
119 gst_plugin_list_free(plugins);
120}
121
122bool isHardwareDecoderFactory(GstElementFactory* factory)
123{
124 if (!factory) {
125 return false;
126 }
127
128 const gchar* factoryName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory));
129 if (!factoryName) {
130 return false;
131 }
132
133 const QByteArray nameLower = QByteArray::fromRawData(factoryName, qstrlen(factoryName)).toLower();
134
135 // Android MediaCodec: exclude software wrappers, accept remaining as hardware
136 if (nameLower.startsWith("amcviddec-omxgoogle") || nameLower.startsWith("amcviddec-c2android")) {
137 return false;
138 }
139 if (nameLower.startsWith("amcviddec-")) {
140 return true;
141 }
142
143 const auto containsHardware = [](const gchar* value) {
144 if (!value)
145 return false;
146 gchar* lower = g_ascii_strdown(value, -1);
147 bool found = (g_strrstr(lower, "hardware") != nullptr);
148 g_free(lower);
149 return found;
150 };
151
152 if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS))) {
153 return true;
154 }
155
156 if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_DESCRIPTION))) {
157 return true;
158 }
159
160 static constexpr const char* kHardwareTags[] = {
161 "va", "nv", "qsv", "msdk", "vulkan", "d3d", "dxva", "vtdec", "metal",
162 };
163
164 for (const auto& tag : kHardwareTags) {
165 if (nameLower.contains(tag)) {
166 return true;
167 }
168 }
169
170 return false;
171}
172
173bool changeFeatureRank(GstRegistry* registry, const char* featureName, uint16_t rank)
174{
175 if (!registry || !featureName) {
176 return false;
177 }
178
179 const GstFeaturePtr feature = adoptFeature(gst_registry_lookup_feature(registry, featureName));
180 if (!feature) {
181 return false;
182 }
183
184 qCDebug(GStreamerHelpersLog) << " Changing feature (" << featureName << ") to use rank:" << rank;
185 gst_plugin_feature_set_rank(feature.get(), rank);
186 return true;
187}
188
189namespace {
190
191void applyRanks(GstRegistry* registry, std::span<const char* const> features, uint16_t rank)
192{
193 for (const char* name : features) {
194 changeFeatureRank(registry, name, rank);
195 }
196}
197
198// Decoder-family rank tables, shared by setCodecPriorities and the zero-copy steering below.
199constexpr std::array<const char* const, 7> kVaDecoders = {"vaav1dec", "vah264dec", "vah265dec", "vajpegdec",
200 "vampeg2dec", "vavp8dec", "vavp9dec"};
201constexpr std::array<const char* const, 9> kNvidiaDecoders = {"nvav1dec", "nvh264dec", "nvh265dec",
202 "nvjpegdec", "nvmpeg2videodec", "nvmpeg4videodec",
203 "nvmpegvideodec", "nvvp8dec", "nvvp9dec"};
204constexpr std::array<const char* const, 18> kDirectX3DDecoders = {
205 "d3d11av1dec", "d3d11h264dec", "d3d11h265dec", "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec",
206 "d3d12av1dec", "d3d12h264dec", "d3d12h265dec", "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec",
207 "dxvaav1decoder", "dxvah264decoder", "dxvah265decoder", "dxvampeg2decoder", "dxvavp8decoder", "dxvavp9decoder"};
208constexpr std::array<const char* const, 2> kVideoToolboxDecoders = {"vtdec_hw", "vtdec"};
209constexpr std::array<const char* const, 12> kIntelDecoders = {
210 "qsvh264dec", "qsvh265dec", "qsvjpegdec", "qsvvp9dec", "msdkav1dec", "msdkh264dec",
211 "msdkh265dec", "msdkmjpegdec", "msdkmpeg2dec", "msdkvc1dec", "msdkvp8dec", "msdkvp9dec"};
212constexpr std::array<const char* const, 2> kVulkanDecoders = {"vulkanh264dec", "vulkanh265dec"};
213
214void lowerDecoderRanksByClass(GstRegistry* registry, bool lowerHardware)
215{
216 static constexpr uint16_t NewRank = GST_RANK_NONE;
217 if (!registry) {
218 qCCritical(GStreamerHelpersLog) << "Invalid registry!";
219 return;
220 }
221
222 GList* decoderFactories = gst_element_factory_list_get_elements(
223 static_cast<GstElementFactoryListType>(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO),
224 GST_RANK_MARGINAL);
225
226 for (GList* node = decoderFactories; node != nullptr; node = node->next) {
227 GstElementFactory* factory = GST_ELEMENT_FACTORY(node->data);
228 if (!factory) {
229 continue;
230 }
231
232 if (GStreamer::isHardwareDecoderFactory(factory) != lowerHardware) {
233 continue;
234 }
235
236 const gchar* name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory));
237 if (!name) {
238 continue;
239 }
240
241 qCDebug(GStreamerHelpersLog) << "Lowering" << (lowerHardware ? "hardware" : "software")
242 << "decoder rank:" << name;
243 gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), NewRank);
244 }
245
246 gst_plugin_feature_list_free(decoderFactories);
247}
248
249void prioritizeByHardwareClass(GstRegistry* registry, uint16_t prioritizedRank, bool requireHardware)
250{
251 if (!registry) {
252 qCCritical(GStreamerHelpersLog) << "Failed to get gstreamer registry.";
253 return;
254 }
255
256 GList* decoderFactories = gst_element_factory_list_get_elements(
257 static_cast<GstElementFactoryListType>(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO),
258 GST_RANK_NONE);
259
260 if (!decoderFactories) {
261 qCDebug(GStreamerHelpersLog) << "No decoder factories available while prioritizing"
262 << (requireHardware ? "hardware" : "software") << "decoders";
263 return;
264 }
265
266 qCDebug(GStreamerHelpersLog) << "Prioritizing" << (requireHardware ? "hardware" : "software")
267 << "video decoders with rank:" << prioritizedRank;
268 int matchedFactories = 0;
269 for (GList* node = decoderFactories; node != nullptr; node = node->next) {
270 GstElementFactory* factory = GST_ELEMENT_FACTORY(node->data);
271 if (!factory) {
272 continue;
273 }
274
275 if (GStreamer::isHardwareDecoderFactory(factory) != requireHardware) {
276 continue;
277 }
278
279 const gchar* featureName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory));
280 if (!featureName) {
281 continue;
282 }
283
284 changeFeatureRank(registry, featureName, prioritizedRank);
285 ++matchedFactories;
286 }
287
288 if (matchedFactories == 0) {
289 qCWarning(GStreamerHelpersLog) << "No" << (requireHardware ? "hardware" : "software")
290 << "video decoder factories found to reprioritize.";
291 }
292
293 qCDebug(GStreamerHelpersLog) << "Lowering" << (requireHardware ? "software" : "hardware") << "decoder ranks.";
294 lowerDecoderRanksByClass(registry, !requireHardware);
295
296 gst_plugin_feature_list_free(decoderFactories);
297}
298
299#ifdef Q_OS_WIN
300// A decoder whose D3D family mismatches the active QRhi backend can't be sampled zero-copy and drops to a CPU copy.
301void alignD3DDecoderRanksToRhi(GstRegistry* registry, bool promoteMatchedFamily)
302{
303 // Prefer the resolved QRhi backend; ranks usually apply before the scene graph resolves, so cachedRhi() is often
304 // null here and graphicsApi() Unknown — fall back to QSG_RHI_BACKEND (Qt's D3D11 default) in that Windows case.
305 // QGCRhiCapture/qrhi.h link only when a GPU path is compiled (GuiPrivate), so the resolved-backend lookup is gated;
306 // a no-GPU Windows build relies on graphicsApi()/QSG_RHI_BACKEND alone.
307 bool wantD3D12 = false;
308 bool resolved = false;
309#if defined(QGC_HAS_ANY_GPU_PATH)
310 if (QRhi* rhi = QGCRhiCapture::cachedRhi()) {
311 wantD3D12 = (rhi->backend() == QRhi::D3D12);
312 resolved = true;
313 }
314#endif
315 if (!resolved) {
316 switch (QQuickWindow::graphicsApi()) {
317 case QSGRendererInterface::Direct3D12:
318 wantD3D12 = true;
319 break;
320 case QSGRendererInterface::Direct3D11:
321 wantD3D12 = false;
322 break;
323 default:
324 wantD3D12 =
325 qEnvironmentVariable("QSG_RHI_BACKEND").compare(QLatin1String("d3d12"), Qt::CaseInsensitive) == 0;
326 break;
327 }
328 }
329 static constexpr const char* kD3D11Decoders[] = {"d3d11av1dec", "d3d11h264dec", "d3d11h265dec",
330 "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec"};
331 static constexpr const char* kD3D12Decoders[] = {"d3d12av1dec", "d3d12h264dec", "d3d12h265dec",
332 "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec"};
333 const char* const* matched = wantD3D12 ? kD3D12Decoders : kD3D11Decoders;
334 const char* const* mismatched = wantD3D12 ? kD3D11Decoders : kD3D12Decoders;
335 qCDebug(GStreamerHelpersLog) << "Aligning D3D decoder ranks to" << (wantD3D12 ? "D3D12" : "D3D11")
336 << "RHI - demoting the mismatched decoder family";
337 static_assert(std::size(kD3D11Decoders) == std::size(kD3D12Decoders));
338 if (promoteMatchedFamily) {
339 static constexpr uint16_t ZeroCopyRank = GST_RANK_PRIMARY + 2;
340 for (size_t i = 0; i < std::size(kD3D11Decoders); ++i) {
341 changeFeatureRank(registry, matched[i], ZeroCopyRank);
342 }
343 }
344 for (size_t i = 0; i < std::size(kD3D11Decoders); ++i) {
345 changeFeatureRank(registry, mismatched[i], GST_RANK_NONE);
346 }
347}
348#endif // Q_OS_WIN
349
350#ifdef Q_OS_LINUX
351constexpr std::array<const char* const, 8> kLegacyVaapiDecoders = {"vaapiav1dec", "vaapih264dec", "vaapih265dec",
352 "vaapijpegdec", "vaapimpeg2dec", "vaapivp8dec",
353 "vaapivp9dec", "vaapidecodebin"};
354constexpr std::array<const char* const, 6> kV4l2StatelessDecoders = {
355 "v4l2slh264dec", "v4l2slh265dec", "v4l2slvp8dec", "v4l2slvp9dec", "v4l2slav1dec", "v4l2slmpeg2dec"};
356
357// Steer autoplug toward zero-copy GPU-memory decoders (DMABuf/DMA_DRM) over system-memory ones. Rank-only and
358// additive: absent factories no-op, va/v4l2 self-demote at runtime without a device, software fallback untouched.
359void preferZeroCopyDecoders(GstRegistry* registry)
360{
361 static constexpr uint16_t ZeroCopyRank = GST_RANK_PRIMARY + 2;
362
363 // Legacy gstreamer-vaapi negotiates GstVaapiMemory the qgcqvideosink/HwBuffers paths can't import
364 // zero-copy; demote it so the modern va plugin (clean DMABuf/DMA_DRM output) wins when both exist.
365 applyRanks(registry, kLegacyVaapiDecoders, GST_RANK_NONE);
366
367 // Modern va decoders (gst-va, supersedes gstreamer-vaapi) emit DMABuf/DMA_DRM, and V4L2 stateless
368 // decoders expose DMA_DRM caps on 1.26+; bump both above software so the zero-copy allocator is picked.
369 applyRanks(registry, kVaDecoders, ZeroCopyRank);
370 applyRanks(registry, kV4l2StatelessDecoders, ZeroCopyRank);
371}
372#endif // Q_OS_LINUX
373
374} // anonymous namespace
375
376void setCodecPriorities(int rawOption)
377{
378 // Validate the raw setting here so the enum switch below stays default-free (-Wswitch).
379 if ((rawOption < ForceVideoDecoderDefault) || (rawOption > ForceVideoDecoderHardware)) {
380 qCWarning(GStreamerHelpersLog) << "Ignoring invalid decode option:" << rawOption;
381 return;
382 }
383 setCodecPriorities(static_cast<VideoDecoderOptions>(rawOption));
384}
385
387{
388 GstRegistry* registry = gst_registry_get();
389
390 if (!registry) {
391 qCCritical(GStreamerHelpersLog) << "Failed to get gstreamer registry.";
392 return;
393 }
394
395 static constexpr uint16_t PrioritizedRank = GST_RANK_PRIMARY + 1;
396
397 switch (option) {
399 // HW-decoder GPU caps (GLMemory/DMABuf/VAMemory) auto-plug to system memory via gldownload/vapostproc.
400#ifdef Q_OS_LINUX
401 preferZeroCopyDecoders(registry);
402#endif
403 break;
405 prioritizeByHardwareClass(registry, PrioritizedRank, false);
406 break;
408 prioritizeByHardwareClass(registry, PrioritizedRank, true);
409 break;
411 applyRanks(registry, kVaDecoders, PrioritizedRank);
412 break;
414 applyRanks(registry, kNvidiaDecoders, PrioritizedRank);
415 break;
417 applyRanks(registry, kDirectX3DDecoders, PrioritizedRank);
418 break;
420 applyRanks(registry, kVideoToolboxDecoders, PrioritizedRank);
421 break;
423 applyRanks(registry, kIntelDecoders, PrioritizedRank);
424 break;
426 // Vulkan zero-copy import is dormant: gst-vulkan's own VkDevice fails QGC's device-match guard → CPU copy.
427 qCWarning(GStreamerHelpersLog) << "Forcing Vulkan video decoders: zero-copy import is dormant "
428 "(foreign VkDevice → CPU fallback), so decode will not be zero-copy.";
429 applyRanks(registry, kVulkanDecoders, PrioritizedRank);
430 break;
431 }
432
433#ifdef Q_OS_WIN
434 // After any option-driven rank changes, force the D3D decoder API to match the active QRhi
435 // backend so a D3D12 decoder never wins over D3D11 on Qt's default Windows RHI (and vice-versa).
436 const bool promoteMatchingD3D = (option == ForceVideoDecoderDefault) || (option == ForceVideoDecoderHardware) ||
437 (option == ForceVideoDecoderDirectX3D);
438 alignD3DDecoderRanksToRhi(registry, promoteMatchingD3D);
439#endif
440}
441
442} // namespace GStreamer
struct _GstElement GstElement
#define QGC_LOGGING_CATEGORY(name, categoryStr)
bool changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank)
void setCodecPriorities(int rawOption)
Overload taking the raw forceVideoDecoder setting value; the cast/range-check lives in the impl.
void forEachPlugin(GstRegistry *registry, const std::function< void(GstPlugin *)> &visitor)
QString writePipelineDot(GstElement *pipeline, const char *tag)
bool isHardwareDecoderFactory(GstElementFactory *factory)
GstFeaturePtr adoptFeature(GstPluginFeature *feature) noexcept
Definition GstScoped.h:31
bool isValidRtspUri(const gchar *uri_str)
VideoDecoderOptions
Definition GStreamer.h:18
@ ForceVideoDecoderDefault
Definition GStreamer.h:19
@ ForceVideoDecoderIntel
Definition GStreamer.h:25
@ ForceVideoDecoderVulkan
Definition GStreamer.h:26
@ ForceVideoDecoderSoftware
Definition GStreamer.h:20
@ ForceVideoDecoderNVIDIA
Definition GStreamer.h:21
@ ForceVideoDecoderVAAPI
Definition GStreamer.h:22
@ ForceVideoDecoderVideoToolbox
Definition GStreamer.h:24
@ ForceVideoDecoderHardware
Definition GStreamer.h:27
@ ForceVideoDecoderDirectX3D
Definition GStreamer.h:23
std::unique_ptr< GstPluginFeature, GstObjectDeleter > GstFeaturePtr
Definition GstScoped.h:23
QRhi * cachedRhi() noexcept
Cached QRhi* maintained by sceneGraph signals; safe from any thread via acquire ordering.