1263 lines
45 KiB
C++
1263 lines
45 KiB
C++
/*
|
|
* Copyright (c) 2014-2026 Meltytech, LLC
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "util.h"
|
|
|
|
#include "FlatpakWrapperGenerator.h"
|
|
#include "Logger.h"
|
|
#include "dialogs/transcodedialog.h"
|
|
#include "mainwindow.h"
|
|
#include "proxymanager.h"
|
|
#include "qmltypes/qmlapplication.h"
|
|
#include "settings.h"
|
|
#include "shotcut_mlt_properties.h"
|
|
#include "transcoder.h"
|
|
#include <MltChain.h>
|
|
#include <MltProducer.h>
|
|
|
|
#include <QApplication>
|
|
#include <QCamera>
|
|
#include <QCameraDevice>
|
|
#include <QCheckBox>
|
|
#include <QCryptographicHash>
|
|
#include <QDesktopServices>
|
|
#include <QDir>
|
|
#include <QDoubleSpinBox>
|
|
#include <QFileInfo>
|
|
#include <QMap>
|
|
#include <QMediaDevices>
|
|
#include <QMessageBox>
|
|
#include <QProcess>
|
|
#include <QProcessEnvironment>
|
|
#include <QRegularExpression>
|
|
#include <QStandardPaths>
|
|
#include <QStorageInfo>
|
|
#include <QStringList>
|
|
#include <QTemporaryFile>
|
|
#include <QUrl>
|
|
#include <QWidget>
|
|
#include <QtGlobal>
|
|
|
|
#include <math.h>
|
|
#include <memory>
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#ifdef Q_OS_MAC
|
|
static const unsigned int kLowMemoryThresholdPercent = 10U;
|
|
#else
|
|
static const unsigned int kLowMemoryThresholdKB = 256U * 1024U;
|
|
#endif
|
|
static const qint64 kFreeSpaceThesholdGB = 25LL * 1024 * 1024 * 1024;
|
|
|
|
QString Util::baseName(const QString &filePath, bool trimQuery)
|
|
{
|
|
QString s = filePath;
|
|
// Only if absolute path and not a URI.
|
|
if (s.startsWith('/') || s.mid(1, 2) == ":/" || s.mid(1, 2) == ":\\")
|
|
s = QFileInfo(s).fileName();
|
|
if (trimQuery) {
|
|
return removeQueryString(s);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
void Util::setColorsToHighlight(QWidget *widget, QPalette::ColorRole role)
|
|
{
|
|
if (role == QPalette::Base) {
|
|
widget->setStyleSheet("QLineEdit {"
|
|
"font-weight: bold;"
|
|
"background-color: palette(highlight);"
|
|
"color: palette(highlighted-text);"
|
|
"selection-background-color: palette(alternate-base);"
|
|
"selection-color: palette(text);"
|
|
"}"
|
|
"QLineEdit:hover {"
|
|
"border: 2px solid palette(button-text);"
|
|
"}");
|
|
} else {
|
|
QPalette palette = QApplication::palette();
|
|
palette.setColor(role, palette.color(palette.Highlight));
|
|
palette.setColor(role == QPalette::Button ? QPalette::ButtonText : QPalette::WindowText,
|
|
palette.color(palette.HighlightedText));
|
|
widget->setPalette(palette);
|
|
widget->setAutoFillBackground(true);
|
|
}
|
|
}
|
|
|
|
void Util::showInFolder(const QString &path)
|
|
{
|
|
QFileInfo info(removeQueryString(path));
|
|
#if defined(Q_OS_WIN)
|
|
QStringList args;
|
|
if (!info.isDir())
|
|
args << "/select,";
|
|
args << QDir::toNativeSeparators(path);
|
|
if (QProcess::startDetached("explorer", args))
|
|
return;
|
|
#elif defined(Q_OS_MAC)
|
|
QStringList args;
|
|
args << "-e";
|
|
args << "tell application \"Finder\"";
|
|
args << "-e";
|
|
args << "activate";
|
|
args << "-e";
|
|
args << "select POSIX file \"" + path + "\"";
|
|
args << "-e";
|
|
args << "end tell";
|
|
#if !defined(QT_DEBUG)
|
|
args << "-e";
|
|
args << "return";
|
|
#endif
|
|
if (!QProcess::execute("/usr/bin/osascript", args))
|
|
return;
|
|
#endif
|
|
Util::openUrl(QUrl::fromLocalFile(info.isDir() ? path : info.path()));
|
|
}
|
|
|
|
bool Util::warnIfNotWritable(const QString &filePath, QWidget *parent, const QString &caption)
|
|
{
|
|
// Returns true if not writable.
|
|
if (!filePath.isEmpty() && !filePath.contains("://")) {
|
|
QFileInfo info(filePath);
|
|
if (!info.isDir()) {
|
|
info = QFileInfo(info.dir().path());
|
|
}
|
|
if (!info.isWritable()) {
|
|
info = QFileInfo(filePath);
|
|
QMessageBox::warning(parent,
|
|
caption,
|
|
QObject::tr("Unable to write file %1\n"
|
|
"Perhaps you do not have permission.\n"
|
|
"Try again with a different folder.")
|
|
.arg(info.fileName()));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString Util::producerTitle(const Mlt::Producer &producer)
|
|
{
|
|
QString result;
|
|
Mlt::Producer &p = const_cast<Mlt::Producer &>(producer);
|
|
if (!p.is_valid() || p.is_blank())
|
|
return result;
|
|
if (p.get(kShotcutTransitionProperty))
|
|
return QObject::tr("Transition");
|
|
if (p.get(kTrackNameProperty))
|
|
return QObject::tr("Track: %1").arg(QString::fromUtf8(p.get(kTrackNameProperty)));
|
|
if (mlt_service_tractor_type == p.type())
|
|
return QObject::tr("Output");
|
|
if (p.get(kShotcutCaptionProperty))
|
|
return QString::fromUtf8(p.get(kShotcutCaptionProperty));
|
|
return Util::baseName(ProxyManager::resource(p));
|
|
}
|
|
|
|
QString Util::removeFileScheme(QUrl &url, bool fromPercentEncoding)
|
|
{
|
|
QString path = url.url();
|
|
if (url.scheme() == "file")
|
|
path = url.toString(QUrl::PreferLocalFile);
|
|
if (fromPercentEncoding)
|
|
return QUrl::fromPercentEncoding(path.toUtf8());
|
|
return path;
|
|
}
|
|
|
|
static inline bool isValidGoProFirstFilePrefix(const QFileInfo &info)
|
|
{
|
|
QStringList list{"GOPR", "GH01", "GL01", "GM01", "GS01", "GX01"};
|
|
return list.contains(info.baseName().left(4).toUpper());
|
|
}
|
|
|
|
static inline bool isValidGoProPrefix(const QFileInfo &info)
|
|
{
|
|
QStringList list{"GP", "GH", "GL", "GM", "GS", "GX"};
|
|
return list.contains(info.baseName().left(2).toUpper());
|
|
}
|
|
|
|
static inline bool isValidGoProSuffix(const QFileInfo &info)
|
|
{
|
|
QStringList list{"MP4", "LRV", "360", "WAV"};
|
|
return list.contains(info.suffix().toUpper());
|
|
}
|
|
|
|
const QStringList Util::sortedFileList(const QList<QUrl> &urls)
|
|
{
|
|
QStringList result;
|
|
QMap<QString, QStringList> goproFiles;
|
|
|
|
// First look for GoPro main files.
|
|
foreach (QUrl url, urls) {
|
|
QFileInfo fi(removeFileScheme(url, false));
|
|
if (fi.baseName().size() == 8 && isValidGoProSuffix(fi) && isValidGoProFirstFilePrefix(fi)) {
|
|
goproFiles[fi.baseName().mid(4)] << fi.filePath();
|
|
}
|
|
}
|
|
// Then, look for GoPro split files.
|
|
foreach (QUrl url, urls) {
|
|
QFileInfo fi(removeFileScheme(url, false));
|
|
if (fi.baseName().size() == 8 && isValidGoProSuffix(fi) && isValidGoProPrefix(fi)
|
|
&& !isValidGoProFirstFilePrefix(fi)) {
|
|
QString goproNumber = fi.baseName().mid(4);
|
|
// Only if there is a matching main GoPro file.
|
|
if (goproFiles.contains(goproNumber) && goproFiles[goproNumber].size()) {
|
|
goproFiles[goproNumber] << fi.filePath();
|
|
}
|
|
}
|
|
}
|
|
// Next, sort the GoPro files.
|
|
auto keys = goproFiles.keys();
|
|
for (auto &goproNumber : keys) {
|
|
goproFiles[goproNumber].sort(Qt::CaseSensitive);
|
|
}
|
|
// Finally, build the list of all files.
|
|
// Add all the GoPro files first.
|
|
for (auto &paths : goproFiles) {
|
|
result << paths;
|
|
}
|
|
// Add all the non-GoPro files.
|
|
for (auto url : urls) {
|
|
QFileInfo fi(removeFileScheme(url, false));
|
|
if (fi.baseName().size() == 8 && isValidGoProSuffix(fi)
|
|
&& (isValidGoProFirstFilePrefix(fi) || isValidGoProPrefix(fi))) {
|
|
QString goproNumber = fi.baseName().mid(4);
|
|
if (goproFiles.contains(goproNumber) && goproFiles[goproNumber].contains(fi.filePath()))
|
|
continue;
|
|
}
|
|
result << fi.filePath();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int Util::coerceMultiple(int value, int multiple)
|
|
{
|
|
return (value + multiple - 1) / multiple * multiple;
|
|
}
|
|
|
|
QList<QUrl> Util::expandDirectories(const QList<QUrl> &urls)
|
|
{
|
|
QList<QUrl> result;
|
|
foreach (QUrl url, urls) {
|
|
QString path = Util::removeFileScheme(url, false);
|
|
QFileInfo fi(path);
|
|
if (fi.isDir()) {
|
|
QDir dir(path);
|
|
foreach (QFileInfo fi, dir.entryInfoList(QDir::Files | QDir::Readable, QDir::Name))
|
|
result << fi.filePath();
|
|
} else {
|
|
result << url;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Util::isDecimalPoint(QChar ch)
|
|
{
|
|
// See https://en.wikipedia.org/wiki/Decimal_separator#Unicode_characters
|
|
return ch == '.' || ch == ',' || ch == '\'' || ch == ' ' || ch == QChar(0x00B7)
|
|
|| ch == QChar(0x2009) || ch == QChar(0x202F) || ch == QChar(0x02D9)
|
|
|| ch == QChar(0x066B) || ch == QChar(0x066C) || ch == QChar(0x2396);
|
|
}
|
|
|
|
bool Util::isNumeric(QString &str)
|
|
{
|
|
for (int i = 0; i < str.size(); ++i) {
|
|
auto ch = str[i];
|
|
if (ch != '+' && ch != '-' && ch.toLower() != 'e' && !isDecimalPoint(ch) && !ch.isDigit())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Util::convertNumericString(QString &str, QChar decimalPoint)
|
|
{
|
|
// Returns true if the string was changed.
|
|
bool result = false;
|
|
if (isNumeric(str)) {
|
|
for (int i = 0; i < str.size(); ++i) {
|
|
auto ch = str[i];
|
|
if (ch != decimalPoint && isDecimalPoint(ch)) {
|
|
ch = decimalPoint;
|
|
result = true;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Util::convertDecimalPoints(QString &str, QChar decimalPoint)
|
|
{
|
|
// Returns true if the string was changed.
|
|
bool result = false;
|
|
if (!str.contains(decimalPoint)) {
|
|
for (int i = 0; i < str.size(); ++i) {
|
|
auto ch = str[i];
|
|
// Space is used as a delimiter for rect fields and possibly elsewhere.
|
|
if (ch != decimalPoint && ch != ' ' && isDecimalPoint(ch)) {
|
|
ch = decimalPoint;
|
|
result = true;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Util::showFrameRateDialog(const QString &caption,
|
|
int numerator,
|
|
QDoubleSpinBox *spinner,
|
|
QWidget *parent)
|
|
{
|
|
double fps = numerator / 1001.0;
|
|
QMessageBox dialog(QMessageBox::Question,
|
|
caption,
|
|
QObject::tr("The value you entered is very similar to the common,\n"
|
|
"more standard %1 = %2/1001.\n\n"
|
|
"Do you want to use %1 = %2/1001 instead?")
|
|
.arg(fps, 0, 'f', 6)
|
|
.arg(numerator),
|
|
QMessageBox::No | QMessageBox::Yes,
|
|
parent);
|
|
dialog.setDefaultButton(QMessageBox::Yes);
|
|
dialog.setEscapeButton(QMessageBox::No);
|
|
dialog.setWindowModality(QmlApplication::dialogModality());
|
|
if (dialog.exec() == QMessageBox::Yes) {
|
|
spinner->setValue(fps);
|
|
}
|
|
}
|
|
|
|
QTemporaryFile *Util::writableTemporaryFile(const QString &filePath, const QString &templateName)
|
|
{
|
|
// filePath should already be checked writable.
|
|
QFileInfo info(filePath);
|
|
QString templateFileName
|
|
= templateName.isEmpty()
|
|
? QStringLiteral("%1.XXXXXX").arg(QCoreApplication::applicationName())
|
|
: templateName;
|
|
|
|
// First, try the system temp dir.
|
|
QString templateFilePath = QDir(QDir::tempPath()).filePath(templateFileName);
|
|
std::unique_ptr<QTemporaryFile> tmp(new QTemporaryFile(templateFilePath));
|
|
|
|
if (!tmp->open() || tmp->write("") < 0) {
|
|
// Otherwise, use the directory provided.
|
|
return new QTemporaryFile(info.dir().filePath(templateFileName));
|
|
} else {
|
|
return tmp.release();
|
|
}
|
|
}
|
|
|
|
void Util::applyCustomProperties(Mlt::Producer &destination, Mlt::Producer &source, int in, int out)
|
|
{
|
|
Mlt::Properties p(destination);
|
|
p.clear("force_progressive");
|
|
p.clear("force_tff");
|
|
p.clear("force_aspect_ratio");
|
|
p.clear("video_delay");
|
|
p.clear("color_range");
|
|
p.clear("speed");
|
|
p.clear("warp_speed");
|
|
p.clear("warp_pitch");
|
|
p.clear("rotate");
|
|
p.clear(kAspectRatioNumerator);
|
|
p.clear(kAspectRatioDenominator);
|
|
p.clear(kCommentProperty);
|
|
p.clear(kShotcutProducerProperty);
|
|
p.clear(kDefaultAudioIndexProperty);
|
|
p.clear(kOriginalInProperty);
|
|
p.clear(kOriginalOutProperty);
|
|
if (!p.get_int(kIsProxyProperty))
|
|
p.clear(kOriginalResourceProperty);
|
|
destination.pass_list(
|
|
source,
|
|
"mlt_service, audio_index, video_index, astream, vstream, force_progressive, force_tff,"
|
|
"force_aspect_ratio, video_delay, color_range, warp_speed, warp_pitch, "
|
|
"rotate," kAspectRatioNumerator "," kAspectRatioDenominator "," kCommentProperty
|
|
"," kShotcutProducerProperty "," kDefaultAudioIndexProperty "," kOriginalInProperty
|
|
"," kOriginalOutProperty "," kOriginalResourceProperty "," kDisableProxyProperty
|
|
"," kShotcutBinsProperty);
|
|
if (!destination.get("_shotcut:resource")) {
|
|
destination.set("_shotcut:resource", destination.get("resource"));
|
|
destination.set("_shotcut:length", destination.get("length"));
|
|
}
|
|
QString resource = ProxyManager::resource(destination);
|
|
if (!qstrcmp("timewarp", source.get("mlt_service"))) {
|
|
auto speed = qAbs(source.get_double("warp_speed"));
|
|
auto caption = QStringLiteral("%1 (%2x)").arg(Util::baseName(resource, true)).arg(speed);
|
|
destination.set(kShotcutCaptionProperty, caption.toUtf8().constData());
|
|
|
|
resource = destination.get("_shotcut:resource");
|
|
destination.set("warp_resource", resource.toUtf8().constData());
|
|
resource = QStringLiteral("%1:%2:%3").arg("timewarp", source.get("warp_speed"), resource);
|
|
destination.set("resource", resource.toUtf8().constData());
|
|
double speedRatio = 1.0 / speed;
|
|
int length = qRound(destination.get_length() * speedRatio);
|
|
destination.set("length", destination.frames_to_time(length, mlt_time_clock));
|
|
} else {
|
|
auto caption = Util::baseName(resource, true);
|
|
destination.set(kShotcutCaptionProperty, caption.toUtf8().constData());
|
|
|
|
p.clear("warp_resource");
|
|
destination.set("resource", destination.get("_shotcut:resource"));
|
|
destination.set("length", destination.get("_shotcut:length"));
|
|
}
|
|
destination.set_in_and_out(in, out);
|
|
}
|
|
|
|
QString Util::getFileHash(const QString &path)
|
|
{
|
|
// This routine is intentionally copied from Kdenlive.
|
|
QFile file(removeQueryString(path));
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
QByteArray fileData;
|
|
// 1 MB = 1 second per 450 files (or faster)
|
|
// 10 MB = 9 seconds per 450 files (or faster)
|
|
if (file.size() > 1000000 * 2) {
|
|
fileData = file.read(1000000);
|
|
if (file.seek(file.size() - 1000000))
|
|
fileData.append(file.readAll());
|
|
} else {
|
|
fileData = file.readAll();
|
|
}
|
|
file.close();
|
|
return QCryptographicHash::hash(fileData, QCryptographicHash::Md5).toHex();
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QString Util::getHash(Mlt::Properties &properties)
|
|
{
|
|
QString hash = properties.get(kShotcutHashProperty);
|
|
if (hash.isEmpty()) {
|
|
QString service = properties.get("mlt_service");
|
|
QString resource = QString::fromUtf8(properties.get("resource"));
|
|
|
|
if (properties.get_int(kIsProxyProperty) && properties.get(kOriginalResourceProperty))
|
|
resource = QString::fromUtf8(properties.get(kOriginalResourceProperty));
|
|
else if (service == "timewarp")
|
|
resource = QString::fromUtf8(properties.get("warp_resource"));
|
|
else if (service == "vidstab")
|
|
resource = QString::fromUtf8(properties.get("filename"));
|
|
hash = getFileHash(resource);
|
|
if (!hash.isEmpty())
|
|
properties.set(kShotcutHashProperty, hash.toLatin1().constData());
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
bool Util::hasDriveLetter(const QString &path)
|
|
{
|
|
auto driveSeparators = path.mid(1, 2);
|
|
return driveSeparators == ":/" || driveSeparators == ":\\";
|
|
}
|
|
|
|
QColorDialog::ColorDialogOptions Util::getColorDialogOptions()
|
|
{
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
return QColorDialog::DontUseNativeDialog;
|
|
#endif
|
|
return QColorDialog::ColorDialogOptions();
|
|
}
|
|
|
|
QFileDialog::Options Util::getFileDialogOptions()
|
|
{
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
if (qEnvironmentVariableIsSet("SNAP")) {
|
|
return QFileDialog::DontUseNativeDialog;
|
|
}
|
|
#endif
|
|
return QFileDialog::Options();
|
|
}
|
|
|
|
bool Util::isMemoryLow()
|
|
{
|
|
#if defined(Q_OS_WIN)
|
|
unsigned int availableKB = UINT_MAX;
|
|
MEMORYSTATUSEX memory_status;
|
|
ZeroMemory(&memory_status, sizeof(MEMORYSTATUSEX));
|
|
memory_status.dwLength = sizeof(MEMORYSTATUSEX);
|
|
if (GlobalMemoryStatusEx(&memory_status)) {
|
|
availableKB = memory_status.ullAvailPhys / 1024UL;
|
|
}
|
|
LOG_INFO() << "available RAM = " << availableKB << "KB";
|
|
return availableKB < kLowMemoryThresholdKB;
|
|
#elif defined(Q_OS_MAC)
|
|
QProcess p;
|
|
p.start("memory_pressure", QStringList());
|
|
p.waitForFinished();
|
|
auto lines = p.readAllStandardOutput();
|
|
p.close();
|
|
for (auto &line : lines.split('\n')) {
|
|
if (line.startsWith("System-wide memory free")) {
|
|
const auto fields = line.split(':');
|
|
for (auto s : fields) {
|
|
bool ok = false;
|
|
auto percentage = s.replace('%', "").toUInt(&ok);
|
|
if (ok) {
|
|
LOG_INFO() << percentage << '%';
|
|
return percentage <= kLowMemoryThresholdPercent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
|
|
QProcess p;
|
|
p.start("sysctl -n hw.usermem");
|
|
p.waitForFinished();
|
|
auto lines = p.readAllStandardOutput();
|
|
p.close();
|
|
bool ok = false;
|
|
auto availableKB = lines.toUInt(&ok);
|
|
if (ok) {
|
|
return availableKB < kLowMemoryThresholdKB;
|
|
}
|
|
|
|
return false;
|
|
#elif defined(Q_OS_LINUX)
|
|
unsigned int availableKB = UINT_MAX;
|
|
QFile meminfo("/proc/meminfo");
|
|
if (meminfo.open(QIODevice::ReadOnly)) {
|
|
for (auto line = meminfo.readLine(1024); availableKB == UINT_MAX && !line.isEmpty();
|
|
line = meminfo.readLine(1024)) {
|
|
if (line.startsWith("MemAvailable")) {
|
|
const auto &fields = line.split(' ');
|
|
for (const auto &s : fields) {
|
|
bool ok = false;
|
|
auto kB = s.toUInt(&ok);
|
|
if (ok) {
|
|
availableKB = kB;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
meminfo.close();
|
|
LOG_INFO() << "available RAM = " << availableKB << "KB";
|
|
return availableKB < kLowMemoryThresholdKB;
|
|
#endif
|
|
}
|
|
|
|
QString Util::removeQueryString(const QString &s)
|
|
{
|
|
auto i = s.lastIndexOf("\\?");
|
|
if (i < 0) {
|
|
i = s.lastIndexOf("%5C?");
|
|
}
|
|
if (i > 0) {
|
|
return s.left(i);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
int Util::greatestCommonDivisor(int m, int n)
|
|
{
|
|
int gcd, remainder;
|
|
while (n) {
|
|
remainder = m % n;
|
|
m = n;
|
|
n = remainder;
|
|
}
|
|
gcd = m;
|
|
return gcd;
|
|
}
|
|
|
|
void Util::normalizeFrameRate(double fps, int &numerator, int &denominator)
|
|
{
|
|
// Convert some common non-integer frame rates to fractions.
|
|
if (qRound(fps * 1000000.0) == 23976024) {
|
|
numerator = 24000;
|
|
denominator = 1001;
|
|
} else if (qRound(fps * 100000.0) == 2997003) {
|
|
numerator = 30000;
|
|
denominator = 1001;
|
|
} else if (qRound(fps * 1000000.0) == 47952048) {
|
|
numerator = 48000;
|
|
denominator = 1001;
|
|
} else if (qRound(fps * 100000.0) == 5994006) {
|
|
numerator = 60000;
|
|
denominator = 1001;
|
|
} else {
|
|
// Workaround storing QDoubleSpinBox::value() loses precision.
|
|
numerator = qRound(fps * 1000000.0);
|
|
denominator = 1000000;
|
|
auto gcd = greatestCommonDivisor(numerator, denominator);
|
|
numerator /= gcd;
|
|
denominator /= gcd;
|
|
}
|
|
}
|
|
|
|
QString Util::textColor(const QColor &color)
|
|
{
|
|
return (color.value() < 150) ? "white" : "black";
|
|
}
|
|
|
|
void Util::cameraFrameRateSize(const QByteArray &deviceName, qreal &frameRate, QSize &size)
|
|
{
|
|
std::unique_ptr<QCamera> camera;
|
|
for (const QCameraDevice &cameraDevice : QMediaDevices::videoInputs()) {
|
|
if (cameraDevice.id() == deviceName) {
|
|
camera.reset(new QCamera(cameraDevice));
|
|
break;
|
|
}
|
|
}
|
|
if (camera) {
|
|
auto currentFormat = camera->cameraDevice().videoFormats().first();
|
|
QList<QSize> resolutions;
|
|
for (const auto &format : camera->cameraDevice().videoFormats()) {
|
|
resolutions << format.resolution();
|
|
}
|
|
if (resolutions.size() > 0) {
|
|
LOG_INFO() << "resolutions:" << resolutions;
|
|
// Get the highest resolution
|
|
camera->setCameraFormat(currentFormat);
|
|
for (const auto &format : camera->cameraDevice().videoFormats()) {
|
|
if (format.resolution().width() > currentFormat.resolution().width()
|
|
&& format.resolution().height() > currentFormat.resolution().height()) {
|
|
camera->setCameraFormat(format);
|
|
currentFormat = format;
|
|
}
|
|
}
|
|
}
|
|
if (currentFormat.maxFrameRate() > 0) {
|
|
frameRate = currentFormat.maxFrameRate();
|
|
}
|
|
if (currentFormat.resolution().width() > 0) {
|
|
size = currentFormat.resolution();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Util::ProducerIsTimewarp(Mlt::Producer *producer)
|
|
{
|
|
return QString::fromUtf8(producer->get("mlt_service")) == "timewarp";
|
|
}
|
|
|
|
QString Util::GetFilenameFromProducer(Mlt::Producer *producer, bool useOriginal)
|
|
{
|
|
QString resource;
|
|
if (useOriginal && producer->get(kOriginalResourceProperty)) {
|
|
resource = QString::fromUtf8(producer->get(kOriginalResourceProperty));
|
|
} else if (ProducerIsTimewarp(producer)) {
|
|
resource = QString::fromUtf8(producer->get("resource"));
|
|
auto i = resource.indexOf(':');
|
|
if (producer->get_int(kIsProxyProperty) && i > 0) {
|
|
resource = resource.mid(i + 1);
|
|
} else {
|
|
resource = QString::fromUtf8(producer->get("warp_resource"));
|
|
}
|
|
} else {
|
|
resource = QString::fromUtf8(producer->get("resource"));
|
|
}
|
|
if (QFileInfo(resource).isRelative()) {
|
|
QString basePath = QFileInfo(MAIN.fileName()).canonicalPath();
|
|
QFileInfo fi(basePath, resource);
|
|
resource = fi.filePath();
|
|
}
|
|
return resource;
|
|
}
|
|
|
|
double Util::GetSpeedFromProducer(Mlt::Producer *producer)
|
|
{
|
|
double speed = 1.0;
|
|
if (ProducerIsTimewarp(producer)) {
|
|
speed = fabs(producer->get_double("warp_speed"));
|
|
}
|
|
return speed;
|
|
}
|
|
|
|
QString Util::updateCaption(Mlt::Producer *producer)
|
|
{
|
|
double warpSpeed = GetSpeedFromProducer(producer);
|
|
QString resource = GetFilenameFromProducer(producer);
|
|
QString name = Util::baseName(resource, true);
|
|
QString caption = producer->get(kShotcutCaptionProperty);
|
|
if (caption.isEmpty() || caption.startsWith(name)) {
|
|
// compute the caption
|
|
if (warpSpeed != 1.0)
|
|
caption = QStringLiteral("%1 (%2x)").arg(name).arg(warpSpeed);
|
|
else
|
|
caption = name;
|
|
producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
|
|
}
|
|
return caption;
|
|
}
|
|
|
|
void Util::passProducerProperties(Mlt::Producer *src, Mlt::Producer *dst)
|
|
{
|
|
dst->pass_list(*src,
|
|
"audio_index, video_index, astream, vstream, force_aspect_ratio,"
|
|
"video_delay, force_progressive, force_tff, force_full_range, color_range, "
|
|
"warp_pitch, rotate," kAspectRatioNumerator "," kAspectRatioDenominator
|
|
"," kShotcutHashProperty "," kPlaylistIndexProperty
|
|
"," kShotcutSkipConvertProperty "," kCommentProperty
|
|
"," kDefaultAudioIndexProperty "," kShotcutCaptionProperty
|
|
"," kOriginalResourceProperty "," kDisableProxyProperty "," kIsProxyProperty
|
|
"," kShotcutProducerProperty);
|
|
QString shotcutProducer(src->get(kShotcutProducerProperty));
|
|
QString service(src->get("mlt_service"));
|
|
if (service.startsWith("avformat") || shotcutProducer == "avformat")
|
|
dst->set(kShotcutProducerProperty, "avformat");
|
|
}
|
|
|
|
bool Util::warnIfLowDiskSpace(const QString &path)
|
|
{
|
|
// Check if the drive this file will be on is getting low on space.
|
|
if (Settings.encodeFreeSpaceCheck()) {
|
|
QStorageInfo si(QFileInfo(path).path());
|
|
LOG_DEBUG() << si.bytesAvailable() << "bytes available on" << si.displayName();
|
|
if (si.isValid() && si.bytesAvailable() < kFreeSpaceThesholdGB) {
|
|
QMessageBox dialog(QMessageBox::Question,
|
|
QApplication::applicationDisplayName(),
|
|
QObject::tr("The drive you chose only has %1 MiB of free space.\n"
|
|
"Do you still want to continue?")
|
|
.arg(si.bytesAvailable() / 1024 / 1024),
|
|
QMessageBox::No | QMessageBox::Yes);
|
|
dialog.setWindowModality(QmlApplication::dialogModality());
|
|
dialog.setDefaultButton(QMessageBox::Yes);
|
|
dialog.setEscapeButton(QMessageBox::No);
|
|
dialog.setCheckBox(new QCheckBox(
|
|
QObject::tr("Do not show this anymore.", "Export free disk space warning dialog")));
|
|
int result = dialog.exec();
|
|
if (dialog.checkBox()->isChecked())
|
|
Settings.setEncodeFreeSpaceCheck(false);
|
|
if (result == QMessageBox::No) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Util::isFpsDifferent(double a, double b)
|
|
{
|
|
return qAbs(a - b) > 0.001;
|
|
}
|
|
|
|
QString Util::getNextFile(const QString &filePath)
|
|
{
|
|
QFileInfo info(filePath);
|
|
QString basename = info.completeBaseName();
|
|
QString extension = info.suffix();
|
|
if (extension.isEmpty()) {
|
|
extension = basename;
|
|
basename = QString();
|
|
}
|
|
for (unsigned i = 1; i < std::numeric_limits<unsigned>::max(); i++) {
|
|
QString filename = QString::fromLatin1("%1%2.%3").arg(basename).arg(i).arg(extension);
|
|
if (!info.dir().exists(filename))
|
|
return info.dir().filePath(filename);
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
QString Util::trcString(int trc)
|
|
{
|
|
QString trcString = QObject::tr("unknown (%1)").arg(trc);
|
|
switch (trc) {
|
|
case 0:
|
|
trcString = QObject::tr("NA");
|
|
break;
|
|
case 1:
|
|
trcString = "ITU-R BT.709";
|
|
break;
|
|
case 6:
|
|
trcString = "ITU-R BT.601";
|
|
break;
|
|
case 7:
|
|
trcString = "SMPTE ST240";
|
|
break;
|
|
case 11:
|
|
trcString = "IEC 61966-2-4";
|
|
break;
|
|
case 13:
|
|
trcString = "sRGB";
|
|
break;
|
|
case 14:
|
|
trcString = "ITU-R BT.2020";
|
|
break;
|
|
case 15:
|
|
trcString = "ITU-R BT.2020";
|
|
break;
|
|
case 16:
|
|
trcString = "SMPTE ST2084 (PQ)";
|
|
break;
|
|
case 17:
|
|
trcString = "SMPTE ST428";
|
|
break;
|
|
case 18:
|
|
trcString = "ARIB B67 (HLG)";
|
|
break;
|
|
}
|
|
return trcString;
|
|
}
|
|
|
|
bool Util::trcIsCompatible(int trc)
|
|
{
|
|
// Transfer characteristics > SMPTE240M Probably need conversion except IEC61966-2-4 is OK
|
|
return trc <= 7 || trc == 11 || trc == 13 || trc == 14 || trc == 15 || trc == 18;
|
|
}
|
|
|
|
QString Util::getConversionAdvice(Mlt::Producer *producer)
|
|
{
|
|
QString advice;
|
|
producer->probe();
|
|
QString resource = Util::GetFilenameFromProducer(producer);
|
|
int trc = producer->get_int("meta.media.color_trc");
|
|
if (!Util::trcIsCompatible(trc)) {
|
|
QString trcString = Util::trcString(trc);
|
|
LOG_INFO() << resource << "Probable HDR" << trcString;
|
|
advice = QObject::tr("This file uses color transfer characteristics %1, which may result "
|
|
"in incorrect colors or brightness in Shotcut.")
|
|
.arg(trcString);
|
|
} else if (producer->get_int("meta.media.variable_frame_rate")) {
|
|
LOG_INFO() << resource << "is variable frame rate";
|
|
advice = QObject::tr(
|
|
"This file is variable frame rate, which is not reliable for editing.");
|
|
} else if (QFile::exists(resource) && !MLT.isSeekable(producer)) {
|
|
LOG_INFO() << resource << "is not seekable";
|
|
advice = QObject::tr("This file does not support seeking and cannot be used for editing.");
|
|
} else if (QFile::exists(resource) && resource.endsWith(".m2t")) {
|
|
LOG_INFO() << resource << "is HDV";
|
|
advice = QObject::tr("This file format (HDV) is not reliable for editing.");
|
|
}
|
|
return advice;
|
|
}
|
|
|
|
mlt_color Util::mltColorFromQColor(const QColor &color)
|
|
{
|
|
return mlt_color{static_cast<uint8_t>(color.red()),
|
|
static_cast<uint8_t>(color.green()),
|
|
static_cast<uint8_t>(color.blue()),
|
|
static_cast<uint8_t>(color.alpha())};
|
|
}
|
|
|
|
void Util::offerSingleFileConversion(QString &message, Mlt::Producer *producer, QWidget *parent)
|
|
{
|
|
TranscodeDialog
|
|
dialog(message.append(QObject::tr(
|
|
" Do you want to convert it to an edit-friendly format?\n\n"
|
|
"If yes, choose a format below and then click OK to choose a file name. "
|
|
"After choosing a file name, a job is created. "
|
|
"When it is done, it automatically replaces clips, or you can double-click the "
|
|
"job to open it.\n")),
|
|
producer->get_int("progressive"),
|
|
parent);
|
|
dialog.setWindowModality(QmlApplication::dialogModality());
|
|
dialog.showCheckBox();
|
|
dialog.set709Convert(!Util::trcIsCompatible(producer->get_int("meta.media.color_trc")));
|
|
dialog.showSubClipCheckBox();
|
|
LOG_DEBUG() << "in" << producer->get_in() << "out" << producer->get_out() << "length"
|
|
<< producer->get_length() - 1;
|
|
dialog.setSubClipChecked(producer->get_in() > 0
|
|
|| producer->get_out() < producer->get_length() - 1);
|
|
auto fps = Util::getAndroidFrameRate(producer);
|
|
if (fps > 0.0)
|
|
dialog.setFrameRate(fps);
|
|
Transcoder transcoder;
|
|
transcoder.addProducer(producer);
|
|
transcoder.convert(dialog);
|
|
}
|
|
|
|
double Util::getAndroidFrameRate(Mlt::Producer *producer)
|
|
{
|
|
auto fps = producer->get_double("meta.attr.com.android.capture.fps.markup");
|
|
if (!qIsFinite(fps))
|
|
fps = 0.0;
|
|
return fps;
|
|
}
|
|
|
|
double Util::getSuggestedFrameRate(Mlt::Producer *producer)
|
|
{
|
|
auto fps = producer->get_double("meta.attr.com.android.capture.fps.markup");
|
|
if (!qIsFinite(fps))
|
|
fps = 0.0;
|
|
if (fps <= 0.0) {
|
|
fps = producer->get_double("meta.media.frame_rate_num");
|
|
if (producer->get_double("meta.media.frame_rate_den") > 0)
|
|
fps /= producer->get_double("meta.media.frame_rate_den");
|
|
if (producer->get("force_fps"))
|
|
fps = producer->get_double("fps");
|
|
}
|
|
return fps;
|
|
}
|
|
|
|
Mlt::Producer Util::openMltVirtualClip(const QString &path)
|
|
{
|
|
Mlt::Producer xmlProducer(nullptr, "xml-clip", path.toUtf8().constData());
|
|
QScopedPointer<Mlt::Profile> testProfile(xmlProducer.profile());
|
|
if (Settings.playerGPU() && MLT.profile().is_explicit()) {
|
|
if (testProfile->width() != MLT.profile().width()
|
|
|| testProfile->height() != MLT.profile().height()
|
|
|| Util::isFpsDifferent(MLT.profile().fps(), testProfile->fps())) {
|
|
return Mlt::Producer();
|
|
}
|
|
}
|
|
if (xmlProducer.is_valid()) {
|
|
Mlt::Chain chain(MLT.profile());
|
|
chain.set_source(xmlProducer);
|
|
chain.attach_normalizers();
|
|
chain.get_length_time(mlt_time_clock);
|
|
chain.set(kShotcutVirtualClip, 1);
|
|
chain.set("resource", path.toUtf8().constData());
|
|
return chain;
|
|
}
|
|
return Mlt::Producer();
|
|
}
|
|
|
|
bool Util::hasiPhoneAmbisonic(Mlt::Producer *producer)
|
|
{
|
|
// iPhone 16 Pro has a 4 channel (spatial) audio stream with codec "apac" that causes failure.
|
|
// This is not limited to only iPhone 16 Pro, but I think most iPhones only record one usable audio track.
|
|
return producer && producer->is_valid()
|
|
&& !::qstrcmp(producer->get("meta.media.1.stream.type"), "audio")
|
|
&& QString(producer->get("meta.attr.com.apple.quicktime.model.markup")).contains("iPhone");
|
|
}
|
|
|
|
bool Util::installFlatpakWrappers(QWidget *parent)
|
|
{
|
|
if (!Settings.askFlatpakWrappers())
|
|
return false;
|
|
QMessageBox dialog(QMessageBox::Question,
|
|
qApp->applicationName(),
|
|
parent->tr(
|
|
"<p>Do you want use a Flatpak?</p>"
|
|
"<p>Click <b>Yes</b> to install/update the Flatpak wrapper scripts "
|
|
"in\n<b>Files > Home > Flatpaks</b>.</p>"
|
|
"<p>Tip: Add <b><tt>~/Flatpaks</tt></b> to your <b><tt>$PATH</tt></b> "
|
|
"to make them more "
|
|
"convenient on the command line.</p>"),
|
|
QMessageBox::No | QMessageBox::Yes,
|
|
parent);
|
|
dialog.setCheckBox(new QCheckBox(parent->tr("Do not show this anymore.")));
|
|
dialog.setWindowModality(QmlApplication::dialogModality());
|
|
dialog.setDefaultButton(QMessageBox::Yes);
|
|
dialog.setEscapeButton(QMessageBox::No);
|
|
int r = dialog.exec();
|
|
if (dialog.checkBox()->isChecked())
|
|
Settings.setAskFlatpakWrappers(false);
|
|
if (r == QMessageBox::Yes) {
|
|
FlatpakWrapperGenerator flatpaks;
|
|
const auto ls = QStandardPaths::standardLocations(QStandardPaths::HomeLocation);
|
|
auto home = QDir(ls.first());
|
|
const auto subdir = QStringLiteral("Flatpaks");
|
|
if (!home.cd(subdir)) {
|
|
if (home.mkdir(subdir))
|
|
home.cd(subdir);
|
|
else
|
|
return false;
|
|
}
|
|
flatpaks.setOutputDir(home.absolutePath());
|
|
flatpaks.setForce(true);
|
|
flatpaks.generateAllInstalled();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString Util::getExecutable(QWidget *parent)
|
|
{
|
|
QString dir;
|
|
QString filter;
|
|
#if defined(Q_OS_WIN)
|
|
dir = QStringLiteral("C:/ProgramData/Microsoft/Windows/Start Menu/Programs");
|
|
filter = parent->tr("Executable Files (*.exe);;All Files (*)");
|
|
#elif defined(Q_OS_MAC)
|
|
const auto ls = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
|
|
LOG_DEBUG() << ls;
|
|
dir = ls.last();
|
|
#elif defined(Q_OS_LINUX)
|
|
if (Util::installFlatpakWrappers(parent)) {
|
|
const auto ls = QStandardPaths::standardLocations(QStandardPaths::HomeLocation);
|
|
auto home = QDir(ls.first());
|
|
home.cd("Flatpaks");
|
|
dir = home.absolutePath();
|
|
} else {
|
|
dir = QStringLiteral("/usr/bin");
|
|
}
|
|
#endif
|
|
return QFileDialog::getOpenFileName(MAIN.window(),
|
|
parent->tr("Choose Executable"),
|
|
dir,
|
|
filter,
|
|
nullptr,
|
|
Util::getFileDialogOptions());
|
|
}
|
|
|
|
QPair<bool, bool> Util::dockerStatus(const QString &imageName)
|
|
{
|
|
// Check if docker executable is available by running 'docker --version'.
|
|
QProcess proc;
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
auto env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
proc.setProcessEnvironment(env);
|
|
#endif
|
|
proc.start(Settings.dockerPath(), {"--version"});
|
|
if (!proc.waitForStarted(1000)) {
|
|
return qMakePair(false, false);
|
|
}
|
|
// Keep the timeout short to avoid UI stall.
|
|
if (!proc.waitForFinished(2000)) {
|
|
proc.kill();
|
|
return qMakePair(false, false);
|
|
}
|
|
bool dockerOk = proc.exitStatus() == QProcess::NormalExit && proc.exitCode() == 0;
|
|
if (!dockerOk) {
|
|
return qMakePair(false, false);
|
|
}
|
|
if (imageName.isEmpty()) {
|
|
return qMakePair(true, false);
|
|
}
|
|
|
|
// Query local images for the given name (may include tag). We avoid pulling.
|
|
// Use 'docker image inspect <imageName>' which returns 0 if present.
|
|
QProcess procImage;
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
procImage.setProcessEnvironment(env);
|
|
#endif
|
|
procImage.start(Settings.dockerPath(), {"image", "inspect", imageName});
|
|
if (!procImage.waitForStarted(1000)) {
|
|
return qMakePair(true, false);
|
|
}
|
|
if (!procImage.waitForFinished(5000)) { // allow a bit more time here
|
|
procImage.kill();
|
|
return qMakePair(true, false);
|
|
}
|
|
bool imageOk = procImage.exitStatus() == QProcess::NormalExit && procImage.exitCode() == 0;
|
|
return qMakePair(true, imageOk);
|
|
}
|
|
|
|
// Helper to extract digest from 'docker manifest inspect' output.
|
|
static QString digestFromManifestInspect(const QByteArray &json)
|
|
{
|
|
auto s = QString::fromUtf8(json);
|
|
auto idx = s.indexOf("sha256:");
|
|
if (idx >= 0 && idx + 71 <= s.size()) {
|
|
return s.mid(idx, 71);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
static QSet<QString> dockerCurrentState;
|
|
|
|
bool Util::isDockerImageCurrent(const QString &imageRef)
|
|
{
|
|
if (imageRef.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: This comparison is not working outside of the machine on which I built the image.
|
|
// In the meantime, use check once per session per image ref.
|
|
if (dockerCurrentState.contains(imageRef))
|
|
return true;
|
|
dockerCurrentState << imageRef;
|
|
return false;
|
|
|
|
// Inspect (local/remote combined behavior) - this is a simplistic approach.
|
|
QProcess localProc;
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
auto env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
localProc.setProcessEnvironment(env);
|
|
#endif
|
|
localProc.start(Settings.dockerPath(), {"image", "inspect", imageRef});
|
|
if (!localProc.waitForStarted(1000) || !localProc.waitForFinished(4000)) {
|
|
if (localProc.state() != QProcess::NotRunning)
|
|
localProc.kill();
|
|
}
|
|
QString localDigest;
|
|
if (localProc.exitStatus() == QProcess::NormalExit && localProc.exitCode() == 0) {
|
|
localDigest = digestFromManifestInspect(localProc.readAllStandardOutput());
|
|
}
|
|
|
|
// Run again expecting remote freshness (will hit registry) without pulling.
|
|
QProcess remoteProc;
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
remoteProc.setProcessEnvironment(env);
|
|
#endif
|
|
remoteProc.start(Settings.dockerPath(), {"manifest", "inspect", imageRef});
|
|
if (!remoteProc.waitForStarted(1000) || !remoteProc.waitForFinished(15000)) {
|
|
if (remoteProc.state() != QProcess::NotRunning)
|
|
remoteProc.kill();
|
|
return false;
|
|
}
|
|
if (remoteProc.exitStatus() != QProcess::NormalExit || remoteProc.exitCode() != 0) {
|
|
return false;
|
|
}
|
|
auto remoteDigest = digestFromManifestInspect(remoteProc.readAllStandardOutput());
|
|
return !remoteDigest.isEmpty() && !localDigest.isEmpty() && localDigest == remoteDigest;
|
|
}
|
|
|
|
void Util::isDockerImageCurrentAsync(const QString &imageRef,
|
|
QObject *receiver,
|
|
std::function<void(bool)> callback)
|
|
{
|
|
if (!callback) {
|
|
return; // nothing to do
|
|
}
|
|
if (imageRef.isEmpty()) {
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
// TODO: This comparison is not working outside of the machine on which I built the image.
|
|
// In the meantime, use check once per session per image ref.
|
|
if (dockerCurrentState.contains(imageRef)) {
|
|
callback(true);
|
|
return;
|
|
}
|
|
dockerCurrentState << imageRef;
|
|
callback(false);
|
|
return;
|
|
|
|
auto emitResult = [callback](bool result) {
|
|
QMetaObject::invokeMethod(
|
|
qApp, [callback, result]() { callback(result); }, Qt::QueuedConnection);
|
|
};
|
|
|
|
// Start with local inspect.
|
|
auto *localProc = new QProcess(receiver);
|
|
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
auto env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
localProc->setProcessEnvironment(env);
|
|
#endif
|
|
|
|
QObject::connect(localProc,
|
|
&QProcess::errorOccurred,
|
|
localProc,
|
|
[localProc, emitResult](QProcess::ProcessError) {
|
|
localProc->deleteLater();
|
|
emitResult(false);
|
|
});
|
|
QObject::connect(
|
|
localProc,
|
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
|
localProc,
|
|
[imageRef, receiver, emitResult, localProc](int, QProcess::ExitStatus) {
|
|
QString localDigest;
|
|
if (localProc->exitStatus() == QProcess::NormalExit && localProc->exitCode() == 0) {
|
|
localDigest = digestFromManifestInspect(localProc->readAllStandardOutput());
|
|
}
|
|
localProc->deleteLater();
|
|
// Remote manifest inspect regardless; if docker missing it will fail.
|
|
auto *remoteProc = new QProcess(receiver);
|
|
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
auto env = QProcessEnvironment::systemEnvironment();
|
|
env.remove("LD_LIBRARY_PATH");
|
|
remoteProc->setProcessEnvironment(env);
|
|
#endif
|
|
|
|
QObject::connect(remoteProc,
|
|
&QProcess::errorOccurred,
|
|
remoteProc,
|
|
[remoteProc, emitResult](QProcess::ProcessError) {
|
|
remoteProc->deleteLater();
|
|
emitResult(false);
|
|
});
|
|
QObject::connect(remoteProc,
|
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
|
remoteProc,
|
|
[emitResult, remoteProc, localDigest](int, QProcess::ExitStatus) {
|
|
QString remoteDigest;
|
|
if (remoteProc->exitStatus() == QProcess::NormalExit
|
|
&& remoteProc->exitCode() == 0) {
|
|
remoteDigest = digestFromManifestInspect(
|
|
remoteProc->readAllStandardOutput());
|
|
}
|
|
remoteProc->deleteLater();
|
|
bool current = !remoteDigest.isEmpty() && !localDigest.isEmpty()
|
|
&& localDigest == remoteDigest;
|
|
emitResult(current);
|
|
});
|
|
LOG_DEBUG() << "docker manifest inspect" << imageRef;
|
|
remoteProc->start(Settings.dockerPath(), {"manifest", "inspect", imageRef});
|
|
});
|
|
LOG_DEBUG() << "docker image inspect" << imageRef;
|
|
localProc->start(Settings.dockerPath(), {"image", "inspect", imageRef});
|
|
}
|
|
|
|
bool Util::isChromiumAvailable()
|
|
{
|
|
// Check if Chromium executable file exists and is executable
|
|
QFileInfo fileInfo(Settings.chromiumPath());
|
|
return fileInfo.exists() && fileInfo.isExecutable();
|
|
}
|
|
|
|
bool Util::startDetached(const QString &program, const QStringList &arguments)
|
|
{
|
|
QProcess process;
|
|
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
// Remove parts of environment variables that our launch script has set
|
|
auto env = QProcessEnvironment::systemEnvironment();
|
|
// Get the parent of bin/shotcut
|
|
QString appDir = QFileInfo(QCoreApplication::applicationDirPath()).dir().absolutePath();
|
|
|
|
auto filterEnvVar = [&env, &appDir](const QString &varName) {
|
|
QString value = env.value(varName);
|
|
if (!value.isEmpty()) {
|
|
QStringList paths = value.split(':');
|
|
QStringList filtered;
|
|
for (QString &path : paths) {
|
|
if (!path.contains(appDir)) {
|
|
filtered << path;
|
|
} else {
|
|
LOG_DEBUG() << "removing" << path << "from env var" << varName;
|
|
}
|
|
}
|
|
env.insert(varName, filtered.join(':'));
|
|
}
|
|
};
|
|
|
|
filterEnvVar("FREI0R_PATH");
|
|
filterEnvVar("LADSPA_PATH");
|
|
filterEnvVar("LD_LIBRARY_PATH");
|
|
filterEnvVar("MANPATH");
|
|
filterEnvVar("PKG_CONFIG_PATH");
|
|
filterEnvVar("PYTHONHOME");
|
|
|
|
// These are relative paths and not cleanable with filterEnvVar()
|
|
env.remove("QML2_IMPORT_PATH");
|
|
env.remove("QT_PLUGIN_PATH");
|
|
|
|
process.setProcessEnvironment(env);
|
|
#endif
|
|
|
|
return process.startDetached(program, arguments);
|
|
}
|
|
|
|
bool Util::openUrl(const QUrl &url)
|
|
{
|
|
auto success = QDesktopServices::openUrl(url);
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
if (!success)
|
|
success = startDetached("xdg-open", {url.toString()});
|
|
#endif
|
|
return success;
|
|
}
|