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
3#include <QtCore/QCoreApplication>
4#include <QtCore/QDir>
5#include <QtCore/QFile>
6#include <QtCore/QFileInfo>
7#include <QtCore/QList>
8#include <QtCore/QMutex>
9#include <QtCore/QPointer>
10#include <QtCore/QStandardPaths>
11#include <QtCore/QStringList>
12#include <QtGui/QGuiApplication>
13#include <QtGui/QScreen>
14#include <QtGui/QWindow>
15#include <QtMultimedia/QVideoSink>
16#include <QtMultimediaQuick/private/qquickvideooutput_p.h>
17#include <QtQuick/QQuickItem>
18#include <QtQuick/QQuickWindow>
19#include <QtQuick/QSGRendererInterface>
20#ifdef Q_OS_ANDROID
21#include <QtCore/QJniEnvironment>
22#include <QtCore/QJniObject>
23#endif
24#include <array>
25#include <memory>
26#include <mutex>
27#include <utility>
28
29#include "Fact.h"
31#include "GStreamerHelpers.h"
32#include "GStreamerLogging.h"
33#include "GstScoped.h"
34#include "GstVideoReceiver.h"
36#if defined(QGC_HAS_ANY_GPU_PATH)
37#include <rhi/qrhi.h>
38
40#endif
41#include "QGCLoggingCategory.h"
45
46#ifdef Q_OS_LINUX
47#include <dlfcn.h>
48#endif
49
50#ifdef Q_OS_ANDROID
51#include <android/asset_manager.h>
52#include <android/asset_manager_jni.h>
53#endif
54
55#include <gst/gst.h>
56
57QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer.GStreamer")
58
59G_BEGIN_DECLS
60#ifdef QGC_GST_STATIC_BUILD
61// Generated from gst_static_plugins.c.in (Android/IOS.cmake on mobile, desktop static cmake):
62// registers every configured plugin, and on mobile loads gioopenssl + the bundled CA bundle.
63extern void gst_init_static_plugins(void);
64#endif
65
67G_END_DECLS
68
69namespace GStreamer {
70
71namespace {
72
73void _registerPlugins()
74{
75 // GST_PLUGIN_STATIC_REGISTER / gst_init_static_plugins() are not idempotent: a second pass
76 // re-registers GTypes ("cannot register existing type 'GstBaseQTMux'") and aborts, so run once.
77 static std::once_flag s_pluginsRegistered;
78 std::call_once(s_pluginsRegistered, [] {
79#ifdef QGC_GST_STATIC_BUILD
80 // Per-plugin registers in the generated shim are registry-guarded, so plugins the Android SDK
81 // gst_init() already pre-registered aren't re-added here.
82 gst_init_static_plugins();
83#endif
84 GST_PLUGIN_STATIC_REGISTER(qgc);
85 });
86}
87
88// plugin_init can fail silently; confirm the element factory is exposed so failures surface here
89// rather than as a misleading "create returned nullptr" later. Common cause: iOS LTO / Android R8
90// stripping the GST_ELEMENT_REGISTER side effect.
91bool requireFactory(const char* name, const char* hint)
92{
93 const GstFactoryPtr factory = adoptFactory(gst_element_factory_find(name));
94 if (!factory) {
95 qCCritical(GStreamerLog) << name << "factory not found —" << hint;
96 return false;
97 }
98 qCDebug(GStreamerLog) << name << "factory available";
99 return true;
100}
101
102bool _verifyPlugins()
103{
104 GstRegistry* registry = gst_registry_get();
105 if (!registry) {
106 qCCritical(GStreamerLog) << "Failed to get GStreamer registry";
107 return false;
108 }
109
110 qCDebug(GStreamerLog) << "Installed GStreamer plugins:";
111 GStreamer::forEachPlugin(registry, [](GstPlugin* plugin) {
112 qCDebug(GStreamerLog) << " " << gst_plugin_get_name(plugin) << gst_plugin_get_version(plugin);
113 });
114
115 bool result = true;
116 const auto hasPlugin = [registry](const char* name) {
117 const GstObjectPtr plugin(GST_OBJECT(gst_registry_find_plugin(registry, name)));
118 return plugin != nullptr;
119 };
120 // Mirrors GSTREAMER_RUNTIME_REQUIRED_PLUGINS (PluginPolicy.cmake) plus qgc,
121 // so a stripped registry fails loudly instead of at first stream attempt.
122 static constexpr std::array<const char*, 12> kRequiredPlugins = {
123 "qgc", "coreelements", "isomp4", "matroska", "multifile", "opengl",
124 "playback", "rtp", "rtpmanager", "rtsp", "tcp", "udp",
125 };
126 for (const char* name : kRequiredPlugins) {
127 if (!hasPlugin(name)) {
128 qCCritical(GStreamerLog) << "Required GStreamer plugin not found:" << name;
129 result = false;
130 }
131 }
132 // GStreamer 1.22+ fuses videoconvert+videoscale into videoconvertscale; accept either.
133 if (!hasPlugin("videoconvertscale") && !(hasPlugin("videoconvert") && hasPlugin("videoscale"))) {
134 qCCritical(GStreamerLog) << "Required GStreamer plugin not found: videoconvertscale (or videoconvert+videoscale)";
135 result = false;
136 }
137
138 if (!result) {
139 // Surface blacklisted plugins so a failure from a corrupt/incompatible plugin file
140 // shows up here instead of looking like a missing-plugin problem.
141 GStreamer::forEachPlugin(registry, [](GstPlugin* p) {
142 const gchar* desc = gst_plugin_get_description(p);
143 if (!desc || !g_str_has_prefix(desc, "BLACKLIST"))
144 return;
145 const gchar* filename = gst_plugin_get_filename(p);
146 qCWarning(GStreamerLog) << "Blacklisted plugin:" << gst_plugin_get_name(p)
147 << "file:" << (filename ? filename : "(null)");
148 });
149
150 // Path / scanner env vars belong to the environment layer that set them.
152 }
153
154 return result;
155}
156
157} // anonymous namespace
158
163
164namespace {
165
166bool _initGstRuntime(const QStringList& args, const Environment::ValidationResult& env)
167{
168 if (!env.ok) {
169 qCCritical(GStreamerLog) << "Invalid GStreamer environment configuration:" << env.error;
170 return false;
171 }
172
173 // args is snapshotted on the GUI thread: QCoreApplication::arguments() is not thread-safe
174 // and initialize() runs on a QtConcurrent pool thread.
175 QByteArrayList argStorage;
176 argStorage.reserve(args.size());
177 for (const QString& arg : args) {
178 argStorage.append(arg.toUtf8());
179 }
180
181 QVarLengthArray<char*, 16> argv;
182 for (QByteArray& arg : argStorage) {
183 argv.append(arg.data());
184 }
185
186 int argc = argv.size();
187 char** argvPtr = argv.data();
188 GError* error = nullptr;
189
190 if (!gst_init_check(&argc, &argvPtr, &error)) {
191 qCCritical(GStreamerLog) << "Failed to initialize GStreamer:" << (error ? error->message : "unknown error");
192 g_clear_error(&error);
193 return false;
194 }
195
196 return true;
197}
198
199} // anonymous namespace
200
202{
203 if (!gst_is_initialized()) {
204 qCCritical(GStreamerLog) << "completeInit called but gst_init() has not been called";
205 return false;
206 }
207
209
210 guint major, minor, micro, nano;
211 gst_version(&major, &minor, &micro, &nano);
212 qCDebug(GStreamerLog) << "GStreamer initialized:" << major << "." << minor << "." << micro;
213
214#ifdef QGC_GST_BUILD_VERSION_MAJOR
215 if (major != QGC_GST_BUILD_VERSION_MAJOR || minor != QGC_GST_BUILD_VERSION_MINOR) {
216 qCWarning(GStreamerLog) << "GStreamer version mismatch: built against" << QGC_GST_BUILD_VERSION_MAJOR << "."
217 << QGC_GST_BUILD_VERSION_MINOR << "but runtime is" << major << "." << minor << "."
218 << micro;
219 }
220#endif
221
222 _registerPlugins();
223
224#ifdef Q_OS_IOS
225 // Prefer applemedia-backed sources on iOS. Must run after _registerPlugins() (registry empty before).
226 if (GstRegistry* reg = gst_registry_get()) {
227 GStreamer::changeFeatureRank(reg, "filesrc", GST_RANK_SECONDARY);
228 GStreamer::changeFeatureRank(reg, "giosrc", GST_RANK_SECONDARY - 1);
229 }
230#endif
231
232 if (!_verifyPlugins()) {
233 qCCritical(GStreamerLog) << "Plugin verification failed";
234 return false;
235 }
236
238
239 if (!requireFactory("qgcqvideosink", "sink bin will fail to construct")) {
240 return false;
241 }
242 if (!requireFactory("qgcvideosinkbin",
243 "qgc plugin registered but element exposure failed. Likely link-time symbol "
244 "stripping (iOS LTO / Android R8) removed the GST_ELEMENT_REGISTER side effect; "
245 "add gstqgcelements.cc to a -force_load / keep rule.")) {
246 return false;
247 }
248
250 qCCritical(GStreamerLog)
251 << "GStreamer external plugin loader failed. Check GST_PLUGIN_SCANNER and bundled runtime paths.";
252 return false;
253 }
254
255 return true;
256}
257
258bool initialize(const QStringList& arguments, const Environment::ValidationResult& envResult)
259{
262
263 // Suppress GStreamer's default stderr handler before gst_init_check() — raw ANSI codes
264 // corrupt the terminal on macOS.
265 gst_debug_remove_log_function(gst_debug_log_default);
266
267 if (!_initGstRuntime(arguments, envResult)) {
268 return false;
269 }
270
271 return completeInit();
272}
273
274// Video sink refcount protocol: createVideoSink returns floating(1); ref on add to the pipeline,
275// unref on removal; releaseVideoSink drops the last ref. Keep the ref/unref sites balanced.
277{
278 // All bin tunables are construct-only — properties drive behavior, no env-var indirection.
279 const gboolean disablePar = config.disablePixelAspectRatio ? TRUE : FALSE;
280 const char* const conversion = config.conversionElement.isEmpty() ? nullptr : config.conversionElement.constData();
281
282 const GstFactoryPtr factory = adoptFactory(gst_element_factory_find("qgcvideosinkbin"));
283 if (!factory) {
284 // completeInit verified this factory at startup; absence here means the registry changed underfoot.
285 qCCritical(GStreamerLog) << "qgcvideosinkbin factory not found";
286 return nullptr;
287 }
288
289#if defined(QGC_HAS_ANY_GPU_PATH)
290 // gpu-zerocopy is construct-only on the bin; adapter reads it back from the bin so the two halves can't desync.
291 GstElement* videoSinkBin =
292 gst_element_factory_create_full(factory.get(), "gpu-zerocopy", config.gpuZeroCopy ? TRUE : FALSE,
293 "conversion-element", conversion, "disable-par", disablePar, NULL);
294#else
295 GstElement* videoSinkBin = gst_element_factory_create_full(factory.get(), "conversion-element", conversion,
296 "disable-par", disablePar, NULL);
297#endif
298 if (!videoSinkBin) {
299 qCCritical(GStreamerLog) << "qgcvideosinkbin element creation failed";
300 }
301 return videoSinkBin;
302}
303
304void releaseVideoSink(void* sink)
305{
306 if (!sink)
307 return;
308 GstElement* videoSink = GST_ELEMENT(sink);
309 gst_clear_object(&videoSink);
310}
311
313{
314 return new GstVideoReceiver(parent);
315}
316
317bool setupQVideoSinkElement(void* sinkBin, QVideoSink* videoSink, QObject* controllerParent)
318{
319 if (!sinkBin || !videoSink || !controllerParent) {
320 // controllerParent owns the controller's QObject lifetime — else it leaks (caller has no handle).
321 qCWarning(GStreamerLog) << "setupQVideoSinkElement: null sinkBin, videoSink, or controllerParent";
322 return false;
323 }
324
325 // Idempotent re-setup: tear down prior controllers so repeated startDecoding cycles
326 // don't accumulate dangling ones.
327 for (auto* c : QGCQVideoSinkController::controllersOf(controllerParent)) {
328 c->setActive(false);
329 // Stop the poll timer synchronously: deleteLater is deferred and the timer would otherwise
330 // keep binding the same element while the new controller is being installed.
331 c->prepareForRelease();
332 c->deleteLater();
333 }
334
335 // Clear the GL bridge's exhausted-retry latch so a restart after Qt's globalShareContext
336 // appears can prime on the next NEED_CONTEXT. No-op when already primed.
338
339 // Accessor returns a transfer-full ref; the guard releases it once the controller has taken
340 // its own ref for deferred QObject teardown.
341 const GstObjectPtr element(GST_OBJECT(gst_qgc_video_sink_bin_get_qvideosink(GST_QGC_VIDEO_SINK_BIN(sinkBin))));
342 if (!element) {
343 qCCritical(GStreamerLog) << "setupQVideoSinkElement: bin has no qgcqvideosink child";
344 return false;
345 }
346 GstElement* const elementRaw = GST_ELEMENT(element.get());
347
348#if defined(QGC_HAS_ANY_GPU_PATH)
349 // Resolve the GPU context on the GUI thread and push it in, else gpu-zerocopy=TRUE negotiates
350 // GPU caps but show_frame still memcpys. gpu-zerocopy is the bin's property, not the inner sink's.
351 const gboolean gpuZeroCopy = gst_qgc_video_sink_bin_get_gpu_zerocopy(GST_ELEMENT(sinkBin));
352 if (gpuZeroCopy) {
353 gst_qgc_q_video_sink_set_hw_context(GST_QGC_Q_VIDEO_SINK(elementRaw), HwBuffers::makeAdapterContext(true));
354 }
355#endif
356
357 auto* controller = new QGCQVideoSinkController(elementRaw, controllerParent);
358
359 // Route the initial install through the controller so its destroyed-sink QPointer guard
360 // covers setup, not just later swaps.
361 controller->setVideoSink(QPointer<QVideoSink>(videoSink));
362 controller->setActive(true);
363
364 return true;
365}
366
367void attachAppSink(QObject* receiver, void* sink, QQuickItem* widget)
368{
369 if (!sink || !widget || !receiver) {
370 return;
371 }
372
373 auto* videoOutput = qobject_cast<QQuickVideoOutput*>(widget);
374 if (!videoOutput) {
375 qCWarning(GStreamerLog) << "Widget is not a VideoOutput, cannot connect qgcqvideosink";
376 return;
377 }
378
379 QVideoSink* videoSink = videoOutput->videoSink();
380 if (!setupQVideoSinkElement(sink, videoSink, receiver)) {
381 qCWarning(GStreamerLog) << "setupQVideoSinkElement failed";
382 }
383
385}
386
387void bindDebugLevelFact(Fact* fact, QObject* context)
388{
389 if (!fact || !context)
390 return;
391 QObject::connect(fact, &Fact::rawValueChanged, context,
392 [](const QVariant& value) { setDebugLevel(value.toInt()); });
393}
394
395static const char* graphicsApiName(QSGRendererInterface::GraphicsApi api)
396{
397 switch (api) {
398 case QSGRendererInterface::Software:
399 return "Software";
400 case QSGRendererInterface::OpenGL:
401 return "OpenGL";
402 case QSGRendererInterface::Direct3D11:
403 return "Direct3D11";
404 case QSGRendererInterface::Direct3D12:
405 return "Direct3D12";
406 case QSGRendererInterface::Vulkan:
407 return "Vulkan";
408 case QSGRendererInterface::Metal:
409 return "Metal";
410 default:
411 return "Unknown";
412 }
413}
414
415// Zero-copy buffer family the resolved RHI backend enables, or "CPU" when no import path is compiled for it.
416static const char* zeroCopyFamilyForApi(QSGRendererInterface::GraphicsApi api)
417{
418 switch (api) {
419 case QSGRendererInterface::OpenGL:
420#if defined(QGC_HAS_GST_GLMEMORY_GPU_PATH) || defined(QGC_HAS_GST_DMABUF_GPU_PATH)
421 return "GLMemory/DMABuf";
422#else
423 return "CPU";
424#endif
425 case QSGRendererInterface::Direct3D11:
426#if defined(QGC_HAS_GST_D3D11_GPU_PATH)
427 return "D3D11";
428#else
429 return "CPU";
430#endif
431 case QSGRendererInterface::Direct3D12:
432 // GStreamer 1.28 can match adapter LUID but cannot wrap Qt's ID3D12Device; D3D12 zero-copy disabled until
433 // same-device import is possible.
434 return "CPU (D3D12 import disabled)";
435 case QSGRendererInterface::Metal:
436#if defined(QGC_HAS_GST_IOSURFACE_GPU_PATH)
437 return "IOSurface/VideoToolbox";
438#else
439 return "CPU";
440#endif
441 case QSGRendererInterface::Vulkan:
442 // Vulkan import is dormant (foreign-VkDevice guard → CPU copy), so it never delivers zero-copy today.
443 return "CPU (Vulkan import dormant)";
444 default:
445 return "CPU";
446 }
447}
448
449void onMainWindowReady(QQuickWindow* window)
450{
452 // Prefer the resolved backend (cachedRhi) over the configured API once the scene graph is up; QGCRhiCapture exists
453 // only when a GPU path is compiled, hence the guard.
454 QSGRendererInterface::GraphicsApi api = QQuickWindow::graphicsApi();
455#if defined(QGC_HAS_ANY_GPU_PATH)
456 if (QRhi* rhi = QGCRhiCapture::cachedRhi()) {
457 switch (rhi->backend()) {
458 case QRhi::OpenGLES2: api = QSGRendererInterface::OpenGL; break;
459 case QRhi::D3D11: api = QSGRendererInterface::Direct3D11; break;
460 case QRhi::D3D12: api = QSGRendererInterface::Direct3D12; break;
461 case QRhi::Metal: api = QSGRendererInterface::Metal; break;
462 case QRhi::Vulkan: api = QSGRendererInterface::Vulkan; break;
463 default: break;
464 }
465 }
466#endif
467 qCInfo(GStreamerLog) << "Resolved RHI backend:" << graphicsApiName(api) << "→ zero-copy path:"
468 << zeroCopyFamilyForApi(api);
469}
470
471QList<VideoDecoderOptions> availableDecoderFamilies()
472{
473 // One walk of the decoder factory list (mirrors prioritizeByHardwareClass) classifies each
474 // factory by name prefix into a VideoDecoderOptions family.
475 static constexpr std::array<std::pair<VideoDecoderOptions, const char*>, 5> kFamilyPrefixes = {{
478 {ForceVideoDecoderIntel, "qsv"},
480 {ForceVideoDecoderVulkan, "vulkan"},
481 }};
482
483 QList<VideoDecoderOptions> families;
484 const auto note = [&families](VideoDecoderOptions f) {
485 if (!families.contains(f))
486 families.append(f);
487 };
488
489 GList* decoderFactories = gst_element_factory_list_get_elements(
490 static_cast<GstElementFactoryListType>(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO),
491 GST_RANK_NONE);
492 for (GList* node = decoderFactories; node != nullptr; node = node->next) {
493 GstElementFactory* factory = GST_ELEMENT_FACTORY(node->data);
494 if (!factory)
495 continue;
496 const gchar* name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory));
497 if (!name)
498 continue;
499
500 if (g_str_has_prefix(name, "msdk")) {
502 continue;
503 }
504 if (g_str_has_prefix(name, "d3d11") || g_str_has_prefix(name, "d3d12") || g_str_has_prefix(name, "dxva")) {
506 continue;
507 }
508 // Legacy gstreamer-vaapi (vaapi*) also matches "va" but is demoted to RANK_NONE and never bumped by
509 // ForceVideoDecoderVAAPI (modern va* only); skip it so an unusable VAAPI option isn't kept.
510 if (g_str_has_prefix(name, "vaapi")) {
511 continue;
512 }
513 for (const auto& [family, prefix] : kFamilyPrefixes) {
514 if (g_str_has_prefix(name, prefix)) {
515 note(family);
516 break;
517 }
518 }
519 }
520 gst_plugin_feature_list_free(decoderFactories);
521
522 return families;
523}
524
525} // namespace GStreamer
G_BEGIN_DECLS GST_PLUGIN_STATIC_DECLARE(qgc)
Config config
struct _GstElement GstElement
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
A Fact is used to hold a single value within the system.
Definition Fact.h:17
void rawValueChanged(const QVariant &value)
static QList< QGCQVideoSinkController * > controllersOf(const QObject *receiver)
A receiver's owning controllers — direct children only, never a deep QObject-tree walk.
static void syncActiveToWindowVisibility(QObject *receiver, QQuickVideoOutput *videoOutput)
void gst_qgc_q_video_sink_set_hw_context(GstQgcQVideoSink *self, const HwVideoBufferContext &ctx)
GstElement * gst_qgc_video_sink_bin_get_qvideosink(GstQgcVideoSinkBin *self)
Returns the internal qgcqvideosink element, transfer-full (caller unrefs); NULL if not yet constructe...
gboolean gst_qgc_video_sink_bin_get_gpu_zerocopy(GstElement *bin)
Whether the bin built its GPU zero-copy pipeline (mirrors "gpu-zerocopy"); NULL-safe (FALSE).
ValidationResult prepareEnvironment()
void bindDebugLevelFact(Fact *fact, QObject *context)
Definition GStreamer.cc:387
void configureDebugLogging()
void onMainWindowReady(QQuickWindow *window)
Definition GStreamer.cc:449
bool changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank)
VideoReceiver * createVideoReceiver(QObject *parent)
Definition GStreamer.cc:312
void setDebugLevel(int level)
void forEachPlugin(GstRegistry *registry, const std::function< void(GstPlugin *)> &visitor)
std::unique_ptr< GstElementFactory, GstObjectDeleter > GstFactoryPtr
Definition GstScoped.h:22
bool didExternalPluginLoaderFail()
void releaseVideoSink(void *sink)
Definition GStreamer.cc:304
QList< VideoDecoderOptions > availableDecoderFamilies()
Definition GStreamer.cc:471
GstFactoryPtr adoptFactory(GstElementFactory *factory) noexcept
Definition GstScoped.h:26
bool completeInit()
Definition GStreamer.cc:201
void * createVideoSink(const VideoSinkConfig &config)
Definition GStreamer.cc:276
bool initialize(const QStringList &arguments, const Environment::ValidationResult &envResult)
Definition GStreamer.cc:258
std::unique_ptr< GstObject, GstObjectDeleter > GstObjectPtr
Definition GstScoped.h:21
void redirectGLibLogging()
static const char * zeroCopyFamilyForApi(QSGRendererInterface::GraphicsApi api)
Definition GStreamer.cc:416
void attachAppSink(QObject *receiver, void *sink, QQuickItem *widget)
Definition GStreamer.cc:367
bool setupQVideoSinkElement(void *sinkBin, QVideoSink *videoSink, QObject *controllerParent)
Definition GStreamer.cc:317
void resetExternalPluginLoaderFailure()
VideoDecoderOptions
Definition GStreamer.h:18
@ ForceVideoDecoderIntel
Definition GStreamer.h:25
@ ForceVideoDecoderVulkan
Definition GStreamer.h:26
@ ForceVideoDecoderNVIDIA
Definition GStreamer.h:21
@ ForceVideoDecoderVAAPI
Definition GStreamer.h:22
@ ForceVideoDecoderVideoToolbox
Definition GStreamer.h:24
@ ForceVideoDecoderDirectX3D
Definition GStreamer.h:23
static const char * graphicsApiName(QSGRendererInterface::GraphicsApi api)
Definition GStreamer.cc:395
Environment::ValidationResult prepareEnvironment()
Definition GStreamer.cc:159
void logDecoderRanks()
void onPipelineRestart() noexcept
Pipeline-restart hook; re-arms one-shot priming latches so a restart can prime on the next NEED_CONTE...
Definition HwBuffers.cc:180
void connectMainWindow(QQuickWindow *window) noexcept
Wire the main QQuickWindow into the RHI-capture path so snapshots follow its QRhi; no-op without GPU.
Definition HwBuffers.cc:203
QRhi * cachedRhi() noexcept
Cached QRhi* maintained by sceneGraph signals; safe from any thread via acquire ordering.