QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GStreamer.cc
Go to the documentation of this file.
1#include "GStreamer.h"
2#include "GStreamerHelpers.h"
3#include "GStreamerLogging.h"
4#include "AppSettings.h"
6#include "GstVideoReceiver.h"
7#include "SettingsManager.h"
8#include "VideoSettings.h"
9#include "Fact.h"
10
11#include "GstAppSinkAdapter.h"
12#if defined(QGC_HAS_GST_GLMEMORY_GPU_PATH)
14#endif
15
16#include <QtGui/QGuiApplication>
17#include <QtGui/QScreen>
18
19#include <QtCore/QCoreApplication>
20#include <QtCore/QDir>
21#include <QtCore/QFileInfo>
22#include <QtCore/QMutex>
23#include <QtCore/QSettings>
24#include <QtCore/QStandardPaths>
25#include <QtCore/QStringList>
26#include <QtQuick/QQuickItem>
27
28#include <atomic>
29
30#ifdef Q_OS_LINUX
31#include <dlfcn.h>
32#endif
33
34#include <gst/gst.h>
35
36QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer.GStreamer")
37QGC_LOGGING_CATEGORY(GStreamerDecoderRanksLog, "Video.GStreamer.GStreamer.DecoderRanks")
38
39#ifdef Q_OS_IOS
40extern "C" {
41void gst_ios_pre_init(void);
42void gst_ios_post_init(void);
43}
44#endif
45
46G_BEGIN_DECLS
47#ifdef QGC_GST_STATIC_BUILD
49GST_PLUGIN_STATIC_DECLARE(coreelements);
53GST_PLUGIN_STATIC_DECLARE(mpegtsdemux);
61GST_PLUGIN_STATIC_DECLARE(typefindfunctions);
63// gst 1.22 merged videoconvert+videoscale into videoconvertscale, but custom/embedded
64// gst builds may keep the legacy split. FindQGCGStreamer.cmake exports GST_PLUGIN_<name>_FOUND
65// so we declare against what the linker actually has, not a version assumption.
66#ifdef GST_PLUGIN_videoconvertscale_FOUND
67GST_PLUGIN_STATIC_DECLARE(videoconvertscale);
68#endif
69#ifdef GST_PLUGIN_videoconvert_FOUND
70GST_PLUGIN_STATIC_DECLARE(videoconvert);
71#endif
72#ifdef GST_PLUGIN_videoscale_FOUND
74#endif
75GST_PLUGIN_STATIC_DECLARE(videoparsersbad);
77
78#ifdef GST_PLUGIN_androidmedia_FOUND
79GST_PLUGIN_STATIC_DECLARE(androidmedia);
80#endif
81#ifdef GST_PLUGIN_applemedia_FOUND
83#endif
84#ifdef GST_PLUGIN_d3d_FOUND
86#endif
87#ifdef GST_PLUGIN_d3d11_FOUND
89#endif
90#ifdef GST_PLUGIN_d3d12_FOUND
92#endif
93#ifdef GST_PLUGIN_dav1d_FOUND
95#endif
96#ifdef GST_PLUGIN_dxva_FOUND
98#endif
99#ifdef GST_PLUGIN_nvcodec_FOUND
101#endif
102#ifdef GST_PLUGIN_qsv_FOUND
104#endif
105#ifdef GST_PLUGIN_va_FOUND
107#endif
108#ifdef GST_PLUGIN_vulkan_FOUND
110#endif
111#endif
112
114G_END_DECLS
115
116namespace GStreamer
117{
118
119namespace {
120
121static std::atomic<bool> s_envPathsValid{true};
122static QMutex s_envPathsMutex;
123static QString s_envPathsError;
124
125void _registerPlugins()
126{
127#ifdef QGC_GST_STATIC_BUILD
128 GST_PLUGIN_STATIC_REGISTER(app);
129 GST_PLUGIN_STATIC_REGISTER(coreelements);
130 GST_PLUGIN_STATIC_REGISTER(isomp4);
131 GST_PLUGIN_STATIC_REGISTER(libav);
132 GST_PLUGIN_STATIC_REGISTER(matroska);
133 GST_PLUGIN_STATIC_REGISTER(mpegtsdemux);
134 GST_PLUGIN_STATIC_REGISTER(openh264);
135 GST_PLUGIN_STATIC_REGISTER(playback);
136 GST_PLUGIN_STATIC_REGISTER(rtp);
137 GST_PLUGIN_STATIC_REGISTER(rtpmanager);
138 GST_PLUGIN_STATIC_REGISTER(rtsp);
139 GST_PLUGIN_STATIC_REGISTER(sdpelem);
140 GST_PLUGIN_STATIC_REGISTER(tcp);
141 GST_PLUGIN_STATIC_REGISTER(typefindfunctions);
142 GST_PLUGIN_STATIC_REGISTER(udp);
143#ifdef GST_PLUGIN_videoconvertscale_FOUND
144 GST_PLUGIN_STATIC_REGISTER(videoconvertscale);
145#endif
146#ifdef GST_PLUGIN_videoconvert_FOUND
147 GST_PLUGIN_STATIC_REGISTER(videoconvert);
148#endif
149#ifdef GST_PLUGIN_videoscale_FOUND
150 GST_PLUGIN_STATIC_REGISTER(videoscale);
151#endif
152 GST_PLUGIN_STATIC_REGISTER(videoparsersbad);
153 GST_PLUGIN_STATIC_REGISTER(vpx);
154
155#ifdef GST_PLUGIN_androidmedia_FOUND
156 GST_PLUGIN_STATIC_REGISTER(androidmedia);
157#endif
158#ifdef GST_PLUGIN_applemedia_FOUND
159 GST_PLUGIN_STATIC_REGISTER(applemedia);
160#endif
161#ifdef GST_PLUGIN_d3d_FOUND
162 GST_PLUGIN_STATIC_REGISTER(d3d);
163#endif
164#ifdef GST_PLUGIN_d3d11_FOUND
165 GST_PLUGIN_STATIC_REGISTER(d3d11);
166#endif
167#ifdef GST_PLUGIN_d3d12_FOUND
168 GST_PLUGIN_STATIC_REGISTER(d3d12);
169#endif
170#ifdef GST_PLUGIN_dav1d_FOUND
171 GST_PLUGIN_STATIC_REGISTER(dav1d);
172#endif
173#ifdef GST_PLUGIN_dxva_FOUND
174 GST_PLUGIN_STATIC_REGISTER(dxva);
175#endif
176#ifdef GST_PLUGIN_nvcodec_FOUND
177 GST_PLUGIN_STATIC_REGISTER(nvcodec);
178#endif
179#ifdef GST_PLUGIN_qsv_FOUND
180 GST_PLUGIN_STATIC_REGISTER(qsv);
181#endif
182#ifdef GST_PLUGIN_va_FOUND
183 GST_PLUGIN_STATIC_REGISTER(va);
184#endif
185#ifdef GST_PLUGIN_vulkan_FOUND
186 GST_PLUGIN_STATIC_REGISTER(vulkan);
187#endif
188#endif
189
190 GST_PLUGIN_STATIC_REGISTER(qgc);
191}
192
193void _resetEnvValidation()
194{
195 const QMutexLocker locker(&s_envPathsMutex);
196 s_envPathsError.clear();
197 s_envPathsValid.store(true, std::memory_order_release);
198}
199
200// Used by every platform branch except iOS.
201[[maybe_unused]] QString _cleanJoin(const QString &base, const QString &relative)
202{
203 return QDir::cleanPath(QDir(base).filePath(relative));
204}
205
206[[maybe_unused]] void _setGstEnv(const char *name, const QString &value)
207{
208 qputenv(name, value.toUtf8());
209 qCDebug(GStreamerLog) << " " << name << "=" << value;
210}
211
212#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
213
214void _setEnvValidationError(const QString &error)
215{
216 const QMutexLocker locker(&s_envPathsMutex);
217 s_envPathsError = error;
218 s_envPathsValid.store(false, std::memory_order_release);
219 qCCritical(GStreamerLog) << error;
220}
221
222void _unsetEnv(const char *name)
223{
224 if (qEnvironmentVariableIsSet(name)) {
225 qunsetenv(name);
226 qCDebug(GStreamerLog) << " unset" << name;
227 }
228}
229
230void _setGstEnvIfExists(const char *name, const QString &path)
231{
232 if (QFileInfo::exists(path)) {
233 _setGstEnv(name, path);
234 }
235}
236
237bool _isExecutableFile(const QString &path)
238{
239 const QFileInfo fileInfo(path);
240 return fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable();
241}
242
243QString _firstExistingPath(const QStringList &paths)
244{
245 for (const QString &path : paths) {
246 if (QFileInfo::exists(path)) {
247 return path;
248 }
249 }
250
251 return {};
252}
253
254#if defined(Q_OS_MACOS)
255QString _joinExistingPaths(const QStringList &paths)
256{
257 QStringList existing;
258 existing.reserve(paths.size());
259
260 for (const QString &path : paths) {
261 if (QFileInfo::exists(path) && !existing.contains(path)) {
262 existing.append(path);
263 }
264 }
265
266 return existing.join(QDir::listSeparator());
267}
268#endif
269
270void _clearManagedGstEnvVars()
271{
272 static constexpr const char *varsToUnset[] = {
273 "GIO_EXTRA_MODULES",
274 "GIO_MODULE_DIR",
275 "GIO_USE_VFS",
276 "GST_PTP_HELPER_1_0",
277 "GST_PTP_HELPER",
278 "GST_PLUGIN_SCANNER_1_0",
279 "GST_PLUGIN_SCANNER",
280 "GST_PLUGIN_SYSTEM_PATH_1_0",
281 "GST_PLUGIN_SYSTEM_PATH",
282 "GST_PLUGIN_PATH_1_0",
283 "GST_PLUGIN_PATH",
284 };
285
286 for (const char *name : varsToUnset) {
287 _unsetEnv(name);
288 }
289}
290
291void _setGstEnvIfExecutable(const char *name, const QString &path)
292{
293 if (_isExecutableFile(path)) {
294 _setGstEnv(name, path);
295 } else {
296 _unsetEnv(name);
297 }
298}
299
300
301void _sanitizePythonEnvForScanner()
302{
303 static constexpr const char *varsToUnset[] = {
304 "PYTHONHOME",
305 "PYTHONPATH",
306 "VIRTUAL_ENV",
307 "CONDA_PREFIX",
308 "CONDA_DEFAULT_ENV",
309 "PYTHONUSERBASE",
310 };
311
312 for (const char *name : varsToUnset) {
313 _unsetEnv(name);
314 }
315
316 _setGstEnv("PYTHONNOUSERSITE", QStringLiteral("1"));
317}
318
319void _applyGstEnvVars(const QString &pluginDir, const QString &gioModDir,
320 const QString &scannerPath, const QString &ptpPath)
321{
322 qCDebug(GStreamerLog) << "Applying GStreamer environment:";
323
324 _sanitizePythonEnvForScanner();
325 _clearManagedGstEnvVars();
326 _setGstEnv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral("no"));
327 _setGstEnv("GST_REGISTRY_FORK", QStringLiteral("no"));
328 _setGstEnvIfExists("GIO_EXTRA_MODULES", gioModDir);
329 _setGstEnvIfExecutable("GST_PTP_HELPER_1_0", ptpPath);
330 _setGstEnvIfExecutable("GST_PTP_HELPER", ptpPath);
331 _setGstEnvIfExecutable("GST_PLUGIN_SCANNER_1_0", scannerPath);
332 _setGstEnvIfExecutable("GST_PLUGIN_SCANNER", scannerPath);
333 _setGstEnv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir);
334 _setGstEnv("GST_PLUGIN_SYSTEM_PATH", pluginDir);
335 _setGstEnv("GST_PLUGIN_PATH_1_0", pluginDir);
336 _setGstEnv("GST_PLUGIN_PATH", pluginDir);
337}
338
339#if defined(Q_OS_LINUX)
340bool _systemGioIsNew()
341{
342 // Try the bare soname first — dlopen resolves it via ldconfig/LD_LIBRARY_PATH,
343 // which works on NixOS, Guix, and other non-FHS distros.
344 // Fall back to hardcoded paths for environments where the bare name fails.
345 static constexpr const char *kGioSoPaths[] = {
346 "libgio-2.0.so.0",
347 "/usr/lib/x86_64-linux-gnu/libgio-2.0.so.0",
348 "/usr/lib/aarch64-linux-gnu/libgio-2.0.so.0",
349 "/usr/lib/arm-linux-gnueabihf/libgio-2.0.so.0",
350 "/usr/lib64/libgio-2.0.so.0",
351 "/usr/lib/libgio-2.0.so.0",
352 };
353
354 for (const char *path : kGioSoPaths) {
355 void *handle = dlopen(path, RTLD_LAZY | RTLD_NOLOAD);
356 if (!handle) {
357 handle = dlopen(path, RTLD_LAZY);
358 }
359 if (!handle) {
360 continue;
361 }
362 const bool found = (dlsym(handle, "g_task_set_static_name") != nullptr);
363 dlclose(handle);
364 return found;
365 }
366
367 return false;
368}
369
370void _applyGioCompatOverride(const QString &gioModDir)
371{
372 if (gioModDir.isEmpty()) {
373 return;
374 }
375
376 // GIO 2.76+ requires bundled modules to be loaded via GIO_MODULE_DIR with
377 // VFS forced to local, mirroring the AppImage launcher logic.
378 if (_systemGioIsNew()) {
379 _unsetEnv("GIO_EXTRA_MODULES");
380 _setGstEnv("GIO_MODULE_DIR", gioModDir);
381 _setGstEnv("GIO_USE_VFS", QStringLiteral("local"));
382 }
383}
384#endif
385
386void _warnIfScannerMissing(const QString &platformLabel, const QString &scannerPath)
387{
388 if (scannerPath.isEmpty()) {
389 qCWarning(GStreamerLog) << "GStreamer:" << platformLabel
390 << "bundled gst-plugin-scanner not found; GStreamer will use in-process scanning";
391 } else if (!_isExecutableFile(scannerPath)) {
392 qCWarning(GStreamerLog) << "GStreamer:" << platformLabel
393 << "gst-plugin-scanner is not executable:" << scannerPath;
394 }
395}
396
397#if defined(Q_OS_MACOS)
398bool _validateMacBundlePaths(const QString &bundleFrameworkRoot,
399 const QString &pluginDirs,
400 const QString &scannerPath)
401{
402 if (pluginDirs.isEmpty()) {
403 _setEnvValidationError(QStringLiteral(
404 "GStreamer: bundled macOS framework found but plugin directory is missing under %1")
405 .arg(bundleFrameworkRoot));
406 return false;
407 }
408
409 _warnIfScannerMissing(QStringLiteral("macOS framework"), scannerPath);
410 return true;
411}
412#endif
413
414bool _validateBundledDesktopPaths(const QString &platformLabel,
415 const QString &pluginDirs,
416 const QString &scannerPath)
417{
418 if (pluginDirs.isEmpty()) {
419 _setEnvValidationError(QStringLiteral(
420 "GStreamer: %1 bundled plugin directory is missing.")
421 .arg(platformLabel));
422 return false;
423 }
424
425 _warnIfScannerMissing(platformLabel, scannerPath);
426 return true;
427}
428
429#endif // !Q_OS_ANDROID && !Q_OS_IOS
430
431void _setGstEnvVars()
432{
433 _resetEnvValidation();
434
435 const QString appDir = QCoreApplication::applicationDirPath();
436 qCDebug(GStreamerLog) << "App directory:" << appDir;
437
438#if defined(Q_OS_MACOS)
439 const QString frameworkDir = _cleanJoin(appDir, "../Frameworks/GStreamer.framework");
440 QString rootDir = _firstExistingPath({
441 _cleanJoin(frameworkDir, "Versions/1.0"),
442 _cleanJoin(frameworkDir, "Versions/Current"),
443 frameworkDir,
444 });
445 if (rootDir.isEmpty()) {
446 rootDir = _cleanJoin(frameworkDir, "Versions/1.0");
447 }
448
449#if defined(QGC_GST_MACOS_FRAMEWORK)
450 // Framework builds prefer framework paths over app-relative paths
451 const QString pluginDirs = _joinExistingPaths({
452 _cleanJoin(rootDir, "lib/gstreamer-1.0"),
453 _cleanJoin(appDir, "../lib/gstreamer-1.0"),
454 });
455 const QString gioMod = _firstExistingPath({
456 _cleanJoin(rootDir, "lib/gio/modules"),
457 _cleanJoin(appDir, "../lib/gio/modules"),
458 });
459#else
460 // Non-framework (Homebrew) builds prefer app-relative paths
461 const QString pluginDirs = _joinExistingPaths({
462 _cleanJoin(appDir, "../lib/gstreamer-1.0"),
463 _cleanJoin(rootDir, "lib/gstreamer-1.0"),
464 });
465 const QString gioMod = _firstExistingPath({
466 _cleanJoin(appDir, "../lib/gio/modules"),
467 _cleanJoin(rootDir, "lib/gio/modules"),
468 });
469#endif
470
471 const QString scanner = _firstExistingPath({
472 _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-plugin-scanner"),
473 _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-plugin-scanner"),
474 });
475 const QString ptp = _firstExistingPath({
476 _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-ptp-helper"),
477 _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-ptp-helper"),
478 });
479 const bool hasBundledFramework = QFileInfo::exists(frameworkDir);
480
481 bool validBundlePaths = true;
482 if (!pluginDirs.isEmpty()) {
483 validBundlePaths = _validateBundledDesktopPaths(QStringLiteral("macOS"), pluginDirs, scanner);
484 }
485 if (hasBundledFramework) {
486 validBundlePaths = validBundlePaths && _validateMacBundlePaths(rootDir, pluginDirs, scanner);
487 }
488
489 if (!pluginDirs.isEmpty() && validBundlePaths) {
490 _applyGstEnvVars(pluginDirs, gioMod, scanner, ptp);
491 }
492
493#if defined(QGC_GST_MACOS_FRAMEWORK)
494 if (hasBundledFramework) {
495 _setGstEnv("GTK_PATH", rootDir);
496 }
497#endif
498
499#elif defined(Q_OS_WIN)
500 const QString libDir = _cleanJoin(appDir, "../lib");
501 const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0");
502 const QString gioMod = _cleanJoin(libDir, "gio/modules");
503 const QString libexecDir = _cleanJoin(appDir, "../libexec");
504 const QString scanner = _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner.exe");
505 const QString ptp = _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper.exe");
506
507 if (QFileInfo::exists(pluginDir)
508 && _validateBundledDesktopPaths(QStringLiteral("Windows"), pluginDir, scanner)) {
509 _applyGstEnvVars(pluginDir, gioMod, scanner, ptp);
510
511 // Ensure the app's bin directory is on PATH so that child processes
512 // (gst-plugin-scanner.exe) can locate GStreamer DLLs installed
513 // alongside the main executable.
514 const QByteArray curPath = qgetenv("PATH");
515 const QByteArray binDir = QDir::toNativeSeparators(appDir).toUtf8();
516 if (!curPath.split(';').contains(binDir)) {
517 qputenv("PATH", binDir + ";" + curPath);
518 }
519 }
520
521#elif defined(Q_OS_ANDROID)
522 // Android uses static plugins — no GST_PLUGIN_PATH needed. But fontconfig
523 // and TLS need env vars pointing to the app's files/cache dirs where
524 // GStreamer.java copied fonts and certificates.
525 {
526 const QString filesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
527 const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
528
529 if (!filesDir.isEmpty()) {
530 _setGstEnv("HOME", filesDir);
531 _setGstEnv("FONTCONFIG_PATH", _cleanJoin(filesDir, "fontconfig"));
532 _setGstEnv("CA_CERTIFICATES", _cleanJoin(filesDir, "ssl/certs/ca-certificates.crt"));
533 _setGstEnv("XDG_DATA_DIRS", filesDir);
534 _setGstEnv("XDG_CONFIG_DIRS", filesDir);
535 _setGstEnv("XDG_CONFIG_HOME", filesDir);
536 _setGstEnv("XDG_DATA_HOME", filesDir);
537 }
538
539 if (!cacheDir.isEmpty()) {
540 _setGstEnv("TMP", cacheDir);
541 _setGstEnv("TEMP", cacheDir);
542 _setGstEnv("TMPDIR", cacheDir);
543 _setGstEnv("XDG_CACHE_HOME", cacheDir);
544 _setGstEnv("XDG_RUNTIME_DIR", cacheDir);
545 _setGstEnv("GST_REGISTRY", _cleanJoin(cacheDir, "registry.bin"));
546 }
547
548 _setGstEnv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral("no"));
549 }
550
551#elif defined(Q_OS_LINUX)
552 // AppRun sets GStreamer env vars before launch (including GIO compatibility
553 // logic). Only apply fallback paths when no external override is present.
554 if (!qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0")
555 && !qEnvironmentVariableIsSet("GST_PLUGIN_PATH")
556 && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH_1_0")
557 && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH")) {
558 const QString libDir = _cleanJoin(appDir, "../lib");
559 const QString libexecDir = _cleanJoin(appDir, "../libexec");
560 const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0");
561 const QString gioMod = _cleanJoin(libDir, "gio/modules");
562 const QString scanner = _firstExistingPath({
563 _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"),
564 _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner"),
565 });
566 const QString ptp = _firstExistingPath({
567 _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-ptp-helper"),
568 _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper"),
569 });
570
571 if (QFileInfo::exists(pluginDir)
572 && _validateBundledDesktopPaths(QStringLiteral("Linux"), pluginDir, scanner)) {
573 _applyGstEnvVars(pluginDir, gioMod, scanner, ptp);
574 _applyGioCompatOverride(gioMod);
575 }
576 }
577#endif
578
579}
580
581bool _verifyPlugins()
582{
583 GstRegistry *registry = gst_registry_get();
584 if (!registry) {
585 qCCritical(GStreamerLog) << "Failed to get GStreamer registry";
586 return false;
587 }
588
589 GList *plugins = gst_registry_get_plugin_list(registry);
590 if (plugins) {
591 qCDebug(GStreamerLog) << "Installed GStreamer plugins:";
592 for (GList *node = plugins; node != nullptr; node = node->next) {
593 GstPlugin *plugin = static_cast<GstPlugin*>(node->data);
594 if (plugin) {
595 qCDebug(GStreamerLog) << " " << gst_plugin_get_name(plugin)
596 << gst_plugin_get_version(plugin);
597 }
598 }
599 gst_plugin_list_free(plugins);
600 }
601
602 bool result = true;
603 // Mirror the install-time verification list so a stripped registry fails loudly here
604 // instead of waiting for first stream attempt with a misleading "no source element".
605 static constexpr const char *requiredPlugins[] = {
606 "qgc", "coreelements", "playback", "rtp", "rtpmanager", "rtsp", "tcp", "udp",
607 };
608 for (const char *name : requiredPlugins) {
609 GstPlugin *plugin = gst_registry_find_plugin(registry, name);
610 if (!plugin) {
611 qCCritical(GStreamerLog) << "Required QGC plugin not found:" << name;
612 result = false;
613 continue;
614 }
615 gst_clear_object(&plugin);
616 }
617
618 if (!result) {
619 const QByteArray pluginPath = qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0")
620 ? qgetenv("GST_PLUGIN_PATH_1_0")
621 : qgetenv("GST_PLUGIN_PATH");
622
623 if (!pluginPath.isEmpty()) {
624 qCCritical(GStreamerLog) << "Check GST_PLUGIN_PATH=" << pluginPath;
625 } else {
626 qCCritical(GStreamerLog) << "GST_PLUGIN_PATH is not set";
627 }
628
629 GList *allPlugins = gst_registry_get_plugin_list(registry);
630 for (GList *node = allPlugins; node != nullptr; node = node->next) {
631 GstPlugin *p = static_cast<GstPlugin*>(node->data);
632 if (!p) continue;
633 const gchar *desc = gst_plugin_get_description(p);
634 const gchar *filename = gst_plugin_get_filename(p);
635 if (desc && g_str_has_prefix(desc, "BLACKLIST")) {
636 qCWarning(GStreamerLog) << "Blacklisted plugin:" << gst_plugin_get_name(p)
637 << "file:" << (filename ? filename : "(null)");
638 }
639 }
640 gst_plugin_list_free(allPlugins);
641
642 static constexpr const char *envDiagnostics[] = {
643 "GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0",
644 "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0",
645 "GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0",
646 "GST_REGISTRY_REUSE_PLUGIN_SCANNER",
647 };
648 qCCritical(GStreamerLog) << "GStreamer environment diagnostics:";
649 for (const char *var : envDiagnostics) {
650 const QByteArray val = qgetenv(var);
651 qCCritical(GStreamerLog) << " " << var << "=" << (val.isEmpty() ? "(unset)" : val.constData());
652 }
653 }
654
655 return result;
656}
657
658void _logDecoderRanks()
659{
660 GList *factories = gst_element_factory_list_get_elements(
661 static_cast<GstElementFactoryListType>(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO),
662 GST_RANK_NONE);
663
664 if (!factories) {
665 qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found";
666 return;
667 }
668
669 factories = g_list_sort(factories, [](gconstpointer lhs, gconstpointer rhs) -> gint {
670 const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhs));
671 const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhs));
672 if (lhsRank != rhsRank) {
673 return (lhsRank > rhsRank) ? -1 : 1;
674 }
675 return g_strcmp0(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhs)),
676 gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhs)));
677 });
678
679 qCDebug(GStreamerDecoderRanksLog) << "Video decoder ranks:";
680 for (GList *node = factories; node != nullptr; node = node->next) {
681 GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data);
682 GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory);
683 const gchar *featureName = gst_plugin_feature_get_name(feature);
684 const guint rank = gst_plugin_feature_get_rank(feature);
685 const gchar *klass = gst_element_factory_get_klass(factory);
686 const bool isHw = GStreamer::isHardwareDecoderFactory(factory);
687
688 GstPlugin *plugin = gst_plugin_feature_get_plugin(feature);
689 const gchar *pluginName = plugin ? gst_plugin_get_name(plugin) : "?";
690
691 qCDebug(GStreamerDecoderRanksLog).noquote()
692 << QStringLiteral(" [%1] %2/%3 rank=%4 (%5)")
693 .arg(isHw ? QStringLiteral("HW") : QStringLiteral("SW"),
694 QString::fromUtf8(pluginName),
695 QString::fromUtf8(featureName))
696 .arg(rank)
697 .arg(QString::fromUtf8(klass));
698
699 if (plugin) {
700 gst_object_unref(plugin);
701 }
702 }
703
704 gst_plugin_feature_list_free(factories);
705}
706
707void _configureDebugLogging()
708{
709 gst_debug_remove_log_function(gst_debug_log_default);
710 gst_debug_add_log_function(GStreamer::qtGstLog, nullptr, nullptr);
711
712 if (!qEnvironmentVariableIsEmpty("GST_DEBUG")) {
713 return;
714 }
715
716 QSettings settings;
717 if (settings.contains(AppSettings::gstDebugLevelName)) {
718 const int level = qBound(0, settings.value(AppSettings::gstDebugLevelName).toInt(),
719 static_cast<int>(GST_LEVEL_MEMDUMP));
720 gst_debug_set_default_threshold(static_cast<GstDebugLevel>(level));
721 }
722}
723
724} // anonymous namespace
725
726void setDebugLevel(int level)
727{
728 if (!gst_is_initialized()) {
729 return;
730 }
731 const int clamped = qBound(0, level, static_cast<int>(GST_LEVEL_MEMDUMP));
732 gst_debug_set_default_threshold(static_cast<GstDebugLevel>(clamped));
733 qCDebug(GStreamerLog) << "GStreamer debug threshold set to" << clamped;
734}
735
737{
738 _setGstEnvVars();
739}
740
741namespace {
742
743bool _initGstRuntime()
744{
745 if (!s_envPathsValid.load(std::memory_order_acquire)) {
746 const QMutexLocker locker(&s_envPathsMutex);
747 qCCritical(GStreamerLog) << "Invalid GStreamer environment configuration:" << s_envPathsError;
748 return false;
749 }
750
751 // Cache arguments on the stack — QCoreApplication::arguments() is not thread-safe,
752 // but this runs early during init before concurrent access is possible.
753 const QStringList args = QCoreApplication::arguments();
754 QByteArrayList argStorage;
755 argStorage.reserve(args.size());
756 for (const QString &arg : args) {
757 argStorage.append(arg.toUtf8());
758 }
759
760 QVarLengthArray<char*, 16> argv;
761 for (QByteArray &arg : argStorage) {
762 argv.append(arg.data());
763 }
764
765 int argc = argv.size();
766 char **argvPtr = argv.data();
767 GError *error = nullptr;
768
769#ifdef Q_OS_IOS
770 gst_ios_pre_init();
771#endif
772
773 if (!gst_init_check(&argc, &argvPtr, &error)) {
774 qCCritical(GStreamerLog) << "Failed to initialize GStreamer:"
775 << (error ? error->message : "unknown error");
776 g_clear_error(&error);
777 return false;
778 }
779
780#ifdef Q_OS_IOS
781 gst_ios_post_init();
782#endif
783
784 return true;
785}
786
787} // anonymous namespace
788
790{
791 if (!gst_is_initialized()) {
792 qCCritical(GStreamerLog) << "completeInit called but gst_init() has not been called";
793 return false;
794 }
795
796 _configureDebugLogging();
797
798 guint major, minor, micro, nano;
799 gst_version(&major, &minor, &micro, &nano);
800 qCDebug(GStreamerLog) << "GStreamer initialized:" << major << "." << minor << "." << micro;
801
802#ifdef QGC_GST_BUILD_VERSION_MAJOR
803 if (major != QGC_GST_BUILD_VERSION_MAJOR || minor != QGC_GST_BUILD_VERSION_MINOR) {
804 qCWarning(GStreamerLog) << "GStreamer version mismatch: built against"
805 << QGC_GST_BUILD_VERSION_MAJOR << "." << QGC_GST_BUILD_VERSION_MINOR
806 << "but runtime is" << major << "." << minor << "." << micro;
807 }
808#endif
809
810 _registerPlugins();
811
812 if (!_verifyPlugins()) {
813 qCCritical(GStreamerLog) << "Plugin verification failed";
814 return false;
815 }
816
817 _logDecoderRanks();
818
819 GstElementFactory *appsinkFactory = gst_element_factory_find("appsink");
820 if (!appsinkFactory) {
821 qCCritical(GStreamerLog) << "appsink factory not found — videoconvert→appsink path unavailable";
822 return false;
823 }
824 qCDebug(GStreamerLog) << "appsink factory available (videoconvert → appsink → QVideoSink)";
825 gst_object_unref(appsinkFactory);
826
828 qCCritical(GStreamerLog)
829 << "GStreamer external plugin loader failed. Check GST_PLUGIN_SCANNER and bundled runtime paths.";
830 return false;
831 }
832
833 return true;
834}
835
837{
840
841 // Suppress GStreamer's default stderr debug handler before gst_init_check()
842 // to prevent raw ANSI escape codes from corrupting the terminal on macOS.
843 gst_debug_remove_log_function(gst_debug_log_default);
844
845 if (!_initGstRuntime()) {
846 return false;
847 }
848
849 return completeInit();
850}
851
852// Ownership protocol for the video sink element:
853// createVideoSink — returns a floating-ref element (refcount conceptually 1).
854// startDecoding — calls gst_object_ref (sinks float, refcount=1).
855// _ensureVideoSinkInPipeline — gst_object_ref (+1=2), gst_bin_add (+1=3).
856// _shutdownDecodingBranch — gst_bin_remove (-1=2), gst_clear_object (-1=1).
857// releaseVideoSink — gst_clear_object (-1=0, freed).
858void *createVideoSink(QQuickItem * /*widget*/, QObject * /*parent*/)
859{
860 GstElement *videoSinkBin = nullptr;
861 // All bin tunables are construct-only — properties drive behavior, no env-var indirection.
863 const QByteArray conversionElement = vs->videoConversionElement()->rawValue().toString().toUtf8();
864 const gboolean disablePar = vs->disablePixelAspectRatio()->rawValue().toBool() ? TRUE : FALSE;
865#if defined(QGC_HAS_ANY_GPU_PATH)
866 // gpu-zerocopy is construct-only on the bin; adapter reads it back from the bin so the two halves can't desync.
867 // Bin defaults to gpu-zerocopy=FALSE — every GPU-capable platform must set it explicitly here or zero-copy stays off.
868 const bool forceCpu = vs->forceCpuVideoPath()->rawValue().toBool();
869 const bool swDecoder = vs->forceVideoDecoder()->rawValue().toInt()
871 const bool gpuZeroCopy = !forceCpu && !swDecoder;
872 if (GstElementFactory *factory = gst_element_factory_find("qgcvideosinkbin")) {
873 videoSinkBin = gst_element_factory_create_full(factory,
874 "gpu-zerocopy", gpuZeroCopy ? TRUE : FALSE,
875 "conversion-element",
876 conversionElement.isEmpty() ? nullptr : conversionElement.constData(),
877 "disable-par", disablePar,
878 NULL);
879 gst_object_unref(factory);
880 }
881#else
882 if (GstElementFactory *factory = gst_element_factory_find("qgcvideosinkbin")) {
883 videoSinkBin = gst_element_factory_create_full(factory,
884 "conversion-element",
885 conversionElement.isEmpty() ? nullptr : conversionElement.constData(),
886 "disable-par", disablePar,
887 NULL);
888 gst_object_unref(factory);
889 }
890#endif
891 if (!videoSinkBin) {
892 qCCritical(GStreamerLog) << "gst_element_factory_make('qgcvideosinkbin') failed";
893 }
894 return videoSinkBin;
895}
896
897void releaseVideoSink(void *sink)
898{
899 if (!sink) return;
900 GstElement *videoSink = GST_ELEMENT(sink);
901 gst_clear_object(&videoSink);
902}
903
905{
906 return new GstVideoReceiver(parent);
907}
908
909bool setupAppSinkAdapter(void *sinkBin, QVideoSink *videoSink, QObject *adapterParent)
910{
911 if (!sinkBin || !videoSink || !adapterParent) {
912 // adapterParent owns the adapter's QObject lifetime; without it the adapter
913 // would leak on success since the caller has no handle to destroy it.
914 qCWarning(GStreamerLog) << "setupAppSinkAdapter: null sinkBin, videoSink, or adapterParent";
915 return false;
916 }
917
918 // Idempotent re-setup: tear down any previous adapter parented under this caller
919 // before creating a new one, so repeated startDecoding cycles don't accumulate
920 // dangling adapters under the same parent.
921 const auto existing = adapterParent->findChildren<GstAppSinkAdapter *>(
922 QString(), Qt::FindDirectChildrenOnly);
923 for (GstAppSinkAdapter *old : existing) {
924 // setActive(false) BEFORE teardown — teardown() nulls the sink under lock and
925 // the empty-frame push would no-op. Order matters for the ghost-frame fix to land.
926 old->setActive(false);
927 old->teardown();
928 old->deleteLater();
929 }
930
931 // Clear the GL bridge's exhausted-retry latch so a pipeline restart that occurs after Qt's
932 // globalShareContext finally appeared can prime on the next NEED_CONTEXT. No-op when the
933 // bridge is already primed (keeps cached display/context across restarts to avoid the
934 // expensive re-discovery dance).
935#if defined(QGC_HAS_GST_GLMEMORY_GPU_PATH)
936 GstGlContextBridge::rearm();
937#endif
938
939 auto *adapter = new GstAppSinkAdapter(adapterParent);
940 if (!adapter->setup(GST_ELEMENT(sinkBin), videoSink)) {
941 qCCritical(GStreamerLog) << "GstAppSinkAdapter::setup() failed";
942 adapter->deleteLater();
943 return false;
944 }
945
946 // Hand the display refresh rate to the adapter so it can keep max-time bounded by the
947 // panel's redraw budget; the adapter combines this with the negotiated stream framerate
948 // on each caps change. setRefreshRate is a no-op when QScreen is unavailable
949 // (headless/early boot) — bin's 33 ms default stays in effect.
950 const qreal refreshHz = QGuiApplication::primaryScreen()
951 ? QGuiApplication::primaryScreen()->refreshRate() : 0.0;
952 if (refreshHz >= 1.0) {
953 adapter->setRefreshRate(refreshHz);
954 }
955
956 // Opt-in OBS-style smoothing ring (default off). Read here so the streaming thread
957 // never has to dip into the SettingsManager. Pass refreshHz so the tick paces with
958 // the panel; the adapter falls back to 60 Hz when refreshHz is 0.
959 if (SettingsManager::instance()->videoSettings()->frameSmoothingEnabled()->rawValue().toBool()) {
960 adapter->setSmoothingEnabled(true, refreshHz);
961 }
962 // Connect latencyChanged so the adapter re-queries immediately on RTSP jitter-buffer reconfigures.
963 if (auto *gstReceiver = qobject_cast<GstVideoReceiver *>(adapterParent)) {
964 QObject::connect(gstReceiver, &GstVideoReceiver::latencyChanged,
966 Qt::DirectConnection);
967 }
968 return true;
969}
970
971void setAppSinkAdaptersActive(QObject *adapterParent, bool active)
972{
973 if (!adapterParent) return;
974 const auto adapters = adapterParent->findChildren<GstAppSinkAdapter *>(
975 QString(), Qt::FindDirectChildrenOnly);
976 for (GstAppSinkAdapter *a : adapters) {
977 a->setActive(active);
978 }
979}
980
981} // namespace GStreamer
G_BEGIN_DECLS GST_PLUGIN_STATIC_DECLARE(qgc)
struct _GstElement GstElement
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Bridges a GStreamer appsink to a Qt QVideoSink.
void setActive(bool active)
void requestLatencyRefresh() noexcept
Signal the adapter to refresh pipeline latency on the next streaming-thread tick.
static SettingsManager * instance()
VideoSettings * videoSettings() const
void qtGstLog(GstDebugCategory *category, GstDebugLevel level, const gchar *file, const gchar *function, gint line, GObject *object, GstDebugMessage *message, gpointer data)
VideoReceiver * createVideoReceiver(QObject *parent)
Definition GStreamer.cc:904
void setDebugLevel(int level)
Definition GStreamer.cc:726
bool didExternalPluginLoaderFail()
void releaseVideoSink(void *sink)
Definition GStreamer.cc:897
bool completeInit()
Definition GStreamer.cc:789
void redirectGLibLogging()
@ ForceVideoDecoderSoftware
Definition GStreamer.h:12
void setAppSinkAdaptersActive(QObject *adapterParent, bool active)
Definition GStreamer.cc:971
bool isHardwareDecoderFactory(GstElementFactory *factory)
bool initialize()
Definition GStreamer.cc:836
void prepareEnvironment()
Definition GStreamer.cc:736
void resetExternalPluginLoaderFailure()
bool setupAppSinkAdapter(void *sinkBin, QVideoSink *videoSink, QObject *adapterParent)
Connect the appsink inside sinkBin to videoSink. Returns true on success.
Definition GStreamer.cc:909
void * createVideoSink(QQuickItem *, QObject *)
Definition GStreamer.cc:858