3#include <QtCore/QCoreApplication>
6#include <QtCore/QFileInfo>
7#include <QtCore/QMutex>
8#include <QtCore/QStandardPaths>
9#include <QtCore/QStringList>
10#include <QtCore/QThread>
19#include <QtCore/QCoreApplication>
20#include <QtCore/QJniEnvironment>
21#include <QtCore/QJniObject>
22#include <android/asset_manager.h>
23#include <android/asset_manager_jni.h>
32static bool s_envPathsValid =
true;
33static QString s_envPathsError;
36static QMutex s_setEnvVarsMutex;
40constexpr const char* kManagedGstPathVars[] = {
41 "GST_PLUGIN_PATH",
"GST_PLUGIN_PATH_1_0",
"GST_PLUGIN_SYSTEM_PATH",
"GST_PLUGIN_SYSTEM_PATH_1_0",
42 "GST_PLUGIN_SCANNER",
"GST_PLUGIN_SCANNER_1_0",
46void _resetEnvValidation()
48 s_envPathsError.clear();
49 s_envPathsValid =
true;
52QString _cleanJoin(
const QString& base,
const QString& relative)
54 return QDir::cleanPath(QDir(base).filePath(relative));
57void _setGstEnv(
const char* name,
const QString& value)
59 qputenv(name, value.toUtf8());
60 qCDebug(GStreamerEnvLog) <<
" " << name <<
"=" << value;
65[[maybe_unused]]
void _setCaBundleIfUnset(
const char* var,
const QStringList& candidates,
bool nativePath =
false)
67 if (!qEnvironmentVariableIsEmpty(var)) {
70 for (
const QString& path : candidates) {
71 if (QFileInfo::exists(path)) {
73 const QByteArray encoded = QFile::encodeName(QDir::toNativeSeparators(path));
74 qputenv(var, encoded);
75 qCDebug(GStreamerEnvLog) <<
" " << var <<
"=" << encoded;
77 _setGstEnv(var, path);
84[[maybe_unused]]
void _setTmpDirVars(
const QString& dir)
86 _setGstEnv(
"TMP", dir);
87 _setGstEnv(
"TEMP", dir);
88 _setGstEnv(
"TMPDIR", dir);
94bool _extractApkAsset(
const char* assetPath,
const QString& destPath)
96 QJniObject ctx = QNativeInterface::QAndroidApplication::context();
98 qCWarning(GStreamerEnvLog) <<
"Cannot resolve Android Context for asset extraction";
102 QJniObject jAssetMgr = ctx.callObjectMethod(
"getAssets",
"()Landroid/content/res/AssetManager;");
103 if (!jAssetMgr.isValid()) {
104 qCWarning(GStreamerEnvLog) <<
"Context.getAssets() returned null";
109 AAssetManager* am = AAssetManager_fromJava(env.jniEnv(), jAssetMgr.object());
111 qCWarning(GStreamerEnvLog) <<
"AAssetManager_fromJava failed";
115 AAsset* asset = AAssetManager_open(am, assetPath, AASSET_MODE_BUFFER);
117 qCDebug(GStreamerEnvLog) <<
"APK asset not present:" << assetPath;
120 const off_t assetLen = AAsset_getLength(asset);
122 const QFileInfo destInfo(destPath);
123 if (destInfo.exists() && destInfo.size() == assetLen) {
128 QDir().mkpath(destInfo.absolutePath());
130 if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
131 qCWarning(GStreamerEnvLog) <<
"Cannot open" << destPath <<
"for write:" << out.errorString();
136 const void* buf = AAsset_getBuffer(asset);
138 if (buf && assetLen > 0) {
139 ok = (out.write(
static_cast<const char*
>(buf), assetLen) == assetLen);
143 ok = ok && (out.error() == QFileDevice::NoError);
146 qCWarning(GStreamerEnvLog) <<
"Failed writing asset" << assetPath <<
"to" << destPath;
147 QFile::remove(destPath);
153#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
156void _setEnvValidationError(
const QString&
error)
158 s_envPathsError =
error;
159 s_envPathsValid =
false;
160 qCCritical(GStreamerEnvLog) <<
error;
163void _unsetEnv(
const char* name)
165 if (qEnvironmentVariableIsSet(name)) {
167 qCDebug(GStreamerEnvLog) <<
" unset" << name;
171void _setGstEnvIfExists(
const char* name,
const QString& path)
173 if (QFileInfo::exists(path)) {
174 _setGstEnv(name, path);
178bool _isExecutableFile(
const QString& path)
180 const QFileInfo fileInfo(path);
181 return fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable();
184QString _firstExistingPath(
const QStringList& paths)
186 for (
const QString& path : paths) {
187 if (QFileInfo::exists(path)) {
195#if defined(Q_OS_MACOS)
196QString _joinExistingPaths(
const QStringList& paths)
198 QStringList existing;
199 existing.reserve(paths.size());
201 for (
const QString& path : paths) {
202 if (QFileInfo::exists(path) && !existing.contains(path)) {
203 existing.append(path);
207 return existing.join(QDir::listSeparator());
211void _clearManagedGstEnvVars()
213 static constexpr const char* gioPtpVars[] = {
214 "GIO_EXTRA_MODULES",
"GIO_MODULE_DIR",
"GIO_USE_VFS",
"GST_PTP_HELPER_1_0",
"GST_PTP_HELPER",
217 for (
const char* name : gioPtpVars) {
220 for (
const char* name : kManagedGstPathVars) {
225void _setGstEnvIfExecutable(
const char* name,
const QString& path)
227 if (_isExecutableFile(path)) {
228 _setGstEnv(name, path);
234void _sanitizePythonEnvForScanner()
236 static constexpr const char* varsToUnset[] = {
237 "PYTHONHOME",
"PYTHONPATH",
"VIRTUAL_ENV",
"CONDA_PREFIX",
"CONDA_DEFAULT_ENV",
"PYTHONUSERBASE",
240 for (
const char* name : varsToUnset) {
244 _setGstEnv(
"PYTHONNOUSERSITE", QStringLiteral(
"1"));
247void _applyGstEnvVars(
const QString& pluginDir,
const QString& gioModDir,
const QString& scannerPath,
248 const QString& ptpPath)
250 qCDebug(GStreamerEnvLog) <<
"Applying GStreamer environment:";
252 _sanitizePythonEnvForScanner();
253 _clearManagedGstEnvVars();
254 _setGstEnv(
"GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral(
"no"));
255 _setGstEnv(
"GST_REGISTRY_FORK", QStringLiteral(
"no"));
256 _setGstEnvIfExists(
"GIO_EXTRA_MODULES", gioModDir);
257 _setGstEnvIfExecutable(
"GST_PTP_HELPER_1_0", ptpPath);
258 _setGstEnvIfExecutable(
"GST_PTP_HELPER", ptpPath);
259 _setGstEnvIfExecutable(
"GST_PLUGIN_SCANNER_1_0", scannerPath);
260 _setGstEnvIfExecutable(
"GST_PLUGIN_SCANNER", scannerPath);
261 _setGstEnv(
"GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir);
262 _setGstEnv(
"GST_PLUGIN_SYSTEM_PATH", pluginDir);
263 _setGstEnv(
"GST_PLUGIN_PATH_1_0", pluginDir);
264 _setGstEnv(
"GST_PLUGIN_PATH", pluginDir);
267#if defined(Q_OS_LINUX)
268bool _systemGioIsNew()
272 static constexpr const char* kGioSoPaths[] = {
274 "/usr/lib/x86_64-linux-gnu/libgio-2.0.so.0",
275 "/usr/lib/aarch64-linux-gnu/libgio-2.0.so.0",
276 "/usr/lib/arm-linux-gnueabihf/libgio-2.0.so.0",
277 "/usr/lib64/libgio-2.0.so.0",
278 "/usr/lib/libgio-2.0.so.0",
281 for (
const char* path : kGioSoPaths) {
282 void* handle = dlopen(path, RTLD_LAZY | RTLD_NOLOAD);
284 handle = dlopen(path, RTLD_LAZY);
289 const bool found = (dlsym(handle,
"g_task_set_static_name") !=
nullptr);
297void _applyGioCompatOverride(
const QString& gioModDir)
299 if (gioModDir.isEmpty()) {
304 if (_systemGioIsNew()) {
305 _unsetEnv(
"GIO_EXTRA_MODULES");
306 _setGstEnv(
"GIO_MODULE_DIR", gioModDir);
307 _setGstEnv(
"GIO_USE_VFS", QStringLiteral(
"local"));
312void _warnIfScannerMissing(
const QString& platformLabel,
const QString& scannerPath)
314 if (scannerPath.isEmpty()) {
315 qCWarning(GStreamerEnvLog) <<
"GStreamer:" << platformLabel
316 <<
"bundled gst-plugin-scanner not found; GStreamer will use in-process scanning";
317 }
else if (!_isExecutableFile(scannerPath)) {
318 qCWarning(GStreamerEnvLog) <<
"GStreamer:" << platformLabel
319 <<
"gst-plugin-scanner is not executable:" << scannerPath;
323#if defined(Q_OS_MACOS)
324void _reportMissingMacFrameworkPlugins(
const QString& bundleFrameworkRoot)
326 _setEnvValidationError(
327 QStringLiteral(
"GStreamer: bundled macOS framework found but plugin directory is missing under %1")
328 .arg(bundleFrameworkRoot));
332bool _validateBundledDesktopPaths(
const QString& platformLabel,
const QString& pluginDirs,
const QString& scannerPath)
334 if (pluginDirs.isEmpty()) {
335 _setEnvValidationError(QStringLiteral(
"GStreamer: %1 bundled plugin directory is missing.").arg(platformLabel));
339 _warnIfScannerMissing(platformLabel, scannerPath);
347 _resetEnvValidation();
349 const QString appDir = QCoreApplication::applicationDirPath();
350 qCDebug(GStreamerEnvLog) <<
"App directory:" << appDir;
352#if defined(Q_OS_MACOS)
353 const QString frameworkDir = _cleanJoin(appDir,
"../Frameworks/GStreamer.framework");
354 QString rootDir = _firstExistingPath({
355 _cleanJoin(frameworkDir,
"Versions/1.0"),
356 _cleanJoin(frameworkDir,
"Versions/Current"),
359 if (rootDir.isEmpty()) {
360 rootDir = _cleanJoin(frameworkDir,
"Versions/1.0");
363#if defined(QGC_GST_MACOS_FRAMEWORK)
365 const QString pluginDirs = _joinExistingPaths({
366 _cleanJoin(rootDir,
"lib/gstreamer-1.0"),
367 _cleanJoin(appDir,
"../lib/gstreamer-1.0"),
369 const QString gioMod = _firstExistingPath({
370 _cleanJoin(rootDir,
"lib/gio/modules"),
371 _cleanJoin(appDir,
"../lib/gio/modules"),
375 const QString pluginDirs = _joinExistingPaths({
376 _cleanJoin(appDir,
"../lib/gstreamer-1.0"),
377 _cleanJoin(rootDir,
"lib/gstreamer-1.0"),
379 const QString gioMod = _firstExistingPath({
380 _cleanJoin(appDir,
"../lib/gio/modules"),
381 _cleanJoin(rootDir,
"lib/gio/modules"),
385 const QString scanner = _firstExistingPath({
386 _cleanJoin(appDir,
"../libexec/gstreamer-1.0/gst-plugin-scanner"),
387 _cleanJoin(rootDir,
"libexec/gstreamer-1.0/gst-plugin-scanner"),
389 const QString ptp = _firstExistingPath({
390 _cleanJoin(appDir,
"../libexec/gstreamer-1.0/gst-ptp-helper"),
391 _cleanJoin(rootDir,
"libexec/gstreamer-1.0/gst-ptp-helper"),
393 const bool hasBundledFramework = QFileInfo::exists(frameworkDir);
395 bool validBundlePaths =
true;
396 if (pluginDirs.isEmpty()) {
397 if (hasBundledFramework) {
398 _reportMissingMacFrameworkPlugins(rootDir);
399 validBundlePaths =
false;
402 validBundlePaths = _validateBundledDesktopPaths(QStringLiteral(
"macOS"), pluginDirs, scanner);
405 if (!pluginDirs.isEmpty() && validBundlePaths) {
406 _applyGstEnvVars(pluginDirs, gioMod, scanner, ptp);
409#if defined(QGC_GST_MACOS_FRAMEWORK)
410 if (hasBundledFramework) {
411 _setGstEnv(
"GTK_PATH", rootDir);
417 _setCaBundleIfUnset(
"SSL_CERT_FILE", {
418 _cleanJoin(rootDir,
"etc/ssl/certs/ca-certificates.crt"),
419 _cleanJoin(appDir,
"../Resources/etc/ssl/certs/ca-certificates.crt"),
422#elif defined(Q_OS_WIN)
423 const QString libDir = _cleanJoin(appDir,
"../lib");
424 const QString pluginDir = _cleanJoin(libDir,
"gstreamer-1.0");
425 const QString gioMod = _cleanJoin(libDir,
"gio/modules");
426 const QString libexecDir = _cleanJoin(appDir,
"../libexec");
427 const QString scanner = _cleanJoin(libexecDir,
"gstreamer-1.0/gst-plugin-scanner.exe");
428 const QString ptp = _cleanJoin(libexecDir,
"gstreamer-1.0/gst-ptp-helper.exe");
430 if (QFileInfo::exists(pluginDir) && _validateBundledDesktopPaths(QStringLiteral(
"Windows"), pluginDir, scanner)) {
431 _applyGstEnvVars(pluginDir, gioMod, scanner, ptp);
435 const QByteArray curPath = qgetenv(
"PATH");
436 const QByteArray binDir = QDir::toNativeSeparators(appDir).toUtf8();
437 bool alreadyOnPath =
false;
438 for (
const QByteArray &entry : curPath.split(
';')) {
440 if (entry.compare(binDir, Qt::CaseInsensitive) == 0) {
441 alreadyOnPath =
true;
445 if (!alreadyOnPath) {
446 qputenv(
"PATH", binDir +
";" + curPath);
451 _setCaBundleIfUnset(
"SSL_CERT_FILE", {_cleanJoin(appDir,
"../etc/ssl/certs/ca-certificates.crt")},
455#elif defined(Q_OS_IOS)
459 const QString resources = appDir;
460 const QString docs = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
461 const QString cache = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
462 const QString tmp = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
464 if (!tmp.isEmpty()) {
467 if (!cache.isEmpty()) {
468 _setGstEnv(
"XDG_CACHE_HOME", cache);
469 _setGstEnv(
"XDG_CONFIG_HOME", cache);
471 _setGstEnv(
"GST_REGISTRY", _cleanJoin(cache,
"registry.bin"));
472 _setGstEnv(
"GST_REGISTRY_FORK", QStringLiteral(
"no"));
475 if (!resources.isEmpty()) {
476 _setGstEnv(
"XDG_RUNTIME_DIR", resources);
477 _setGstEnv(
"XDG_DATA_DIRS", resources);
478 _setGstEnv(
"XDG_CONFIG_DIRS", resources);
479 _setGstEnv(
"XDG_DATA_HOME", resources);
481 if (!docs.isEmpty()) {
482 _setGstEnv(
"HOME", docs);
485 if (!resources.isEmpty()) {
486 _setCaBundleIfUnset(
"CA_CERTIFICATES", {_cleanJoin(resources,
"ssl/certs/ca-certificates.crt")});
490#elif defined(Q_OS_ANDROID)
494 const QString filesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
495 const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
497 if (!filesDir.isEmpty()) {
498 _setGstEnv(
"HOME", filesDir);
499 _setGstEnv(
"FONTCONFIG_PATH", _cleanJoin(filesDir,
"fontconfig"));
500 const QString caBundle = _cleanJoin(filesDir,
"ssl/certs/ca-certificates.crt");
501 if (_extractApkAsset(
"ssl/certs/ca-certificates.crt", caBundle) &&
502 qEnvironmentVariableIsEmpty(
"CA_CERTIFICATES")) {
503 _setGstEnv(
"CA_CERTIFICATES", caBundle);
505 _setGstEnv(
"XDG_DATA_DIRS", filesDir);
506 _setGstEnv(
"XDG_CONFIG_DIRS", filesDir);
507 _setGstEnv(
"XDG_CONFIG_HOME", filesDir);
508 _setGstEnv(
"XDG_DATA_HOME", filesDir);
511 if (!cacheDir.isEmpty()) {
512 _setTmpDirVars(cacheDir);
513 _setGstEnv(
"XDG_CACHE_HOME", cacheDir);
514 _setGstEnv(
"XDG_RUNTIME_DIR", cacheDir);
515 _setGstEnv(
"GST_REGISTRY", _cleanJoin(cacheDir,
"registry.bin"));
518 _setGstEnv(
"GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral(
"no"));
521#elif defined(Q_OS_LINUX)
523 if (!qEnvironmentVariableIsSet(
"GST_PLUGIN_PATH_1_0") && !qEnvironmentVariableIsSet(
"GST_PLUGIN_PATH") &&
524 !qEnvironmentVariableIsSet(
"GST_PLUGIN_SYSTEM_PATH_1_0") &&
525 !qEnvironmentVariableIsSet(
"GST_PLUGIN_SYSTEM_PATH")) {
526 const QString libDir = _cleanJoin(appDir,
"../lib");
527 const QString libexecDir = _cleanJoin(appDir,
"../libexec");
528 const QString pluginDir = _cleanJoin(libDir,
"gstreamer-1.0");
529 const QString gioMod = _cleanJoin(libDir,
"gio/modules");
530 const QString scanner = _firstExistingPath({
531 _cleanJoin(libDir,
"gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"),
532 _cleanJoin(libexecDir,
"gstreamer-1.0/gst-plugin-scanner"),
534 const QString ptp = _firstExistingPath({
535 _cleanJoin(libDir,
"gstreamer1.0/gstreamer-1.0/gst-ptp-helper"),
536 _cleanJoin(libexecDir,
"gstreamer-1.0/gst-ptp-helper"),
539 if (QFileInfo::exists(pluginDir) && _validateBundledDesktopPaths(QStringLiteral(
"Linux"), pluginDir, scanner)) {
540 _applyGstEnvVars(pluginDir, gioMod, scanner, ptp);
541 _applyGioCompatOverride(gioMod);
555 if (QCoreApplication::instance() && (QThread::currentThread() != QCoreApplication::instance()->thread())) {
556 qCWarning(GStreamerEnvLog) <<
"prepareEnvironment() called off the GUI thread; qputenv is not thread-safe";
558 const QMutexLocker setEnvLocker(&s_setEnvVarsMutex);
560 return {s_envPathsValid, s_envPathsError};
566 const QByteArray pluginPath =
567 qEnvironmentVariableIsSet(
"GST_PLUGIN_PATH_1_0") ? qgetenv(
"GST_PLUGIN_PATH_1_0") : qgetenv(
"GST_PLUGIN_PATH");
568 if (!pluginPath.isEmpty()) {
569 qCCritical(GStreamerEnvLog) <<
"Check GST_PLUGIN_PATH=" << pluginPath;
571 qCCritical(GStreamerEnvLog) <<
"GST_PLUGIN_PATH is not set";
574 qCCritical(GStreamerEnvLog) <<
"GStreamer environment diagnostics:";
575 const auto dumpVar = [](
const char* var) {
576 const QByteArray val = qgetenv(var);
577 qCCritical(GStreamerEnvLog) <<
" " << var <<
"=" << (val.isEmpty() ?
"(unset)" : val.constData());
579 for (
const char* var : kManagedGstPathVars) {
582 dumpVar(
"GST_REGISTRY_REUSE_PLUGIN_SCANNER");
#define QGC_LOGGING_CATEGORY(name, categoryStr)
ValidationResult prepareEnvironment()