Skip to content

Commit

Permalink
feat: allow to remux WEBM to MP4 using FFmpeg (fix #3092)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bionus committed Jan 29, 2024
1 parent 9fa4eb7 commit 48bcda6
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 17 deletions.
18 changes: 18 additions & 0 deletions src/gui/src/settings/options-window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "backup.h"
#include "custom-buttons.h"
#include "exiftool.h"
#include "ffmpeg.h"
#include "functions.h"
#include "filename/conditional-filename.h"
#include "helpers.h"
Expand Down Expand Up @@ -138,6 +139,21 @@ OptionsWindow::OptionsWindow(Profile *profile, ThemeLoader *themeLoader, QWidget
ui->keyAcceptDialogue->setKeySequence(getKeySequence(settings, "keyAcceptDialog", Qt::CTRL | Qt::Key_Y));
ui->keyDeclineDialogue->setKeySequence(getKeySequence(settings, "keyDeclineDialog", Qt::CTRL | Qt::Key_N));

// FFmpeg format conversion
QFuture<QString> ffmpegVersion = QtConcurrent::run([=]() {
return FFmpeg::version();
});
auto *ffmpegVersionWatcher = new QFutureWatcher<QString>(this);
connect(ffmpegVersionWatcher, &QFutureWatcher<QString>::finished, [=]() {
const QString &version = ffmpegVersion.result();
ui->labelConversionFFmpegVersion->setText(version.isEmpty() ? tr("ffmpeg not found") : version);
if (version.isEmpty()) {
ui->labelConversionFFmpegVersion->setStyleSheet("color: red");
}
});
ffmpegVersionWatcher->setFuture(ffmpegVersion);
ui->checkConversionFFmpegRemuxWebmToMp4->setChecked(settings->value("Save/FFmpegRemuxWebmToMp4", false).toBool());

// Metadata using Windows Property System
#ifndef WIN_FILE_PROPS
ui->groupMetadataPropsys->setEnabled(false);
Expand Down Expand Up @@ -1257,6 +1273,8 @@ void OptionsWindow::save()
tokenSettings->save();
}

settings->setValue("FFmpegRemuxWebmToMp4", ui->checkConversionFFmpegRemuxWebmToMp4->isChecked());

settings->setValue("MetadataPropsysExtensions", ui->lineMetadataPropsysExtensions->text());
settings->beginWriteArray("MetadataPropsys");
for (int i = 0, j = 0; i < m_metadataPropsys.count(); ++i) {
Expand Down
60 changes: 56 additions & 4 deletions src/gui/src/settings/options-window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>743</width>
<height>619</height>
<width>747</width>
<height>628</height>
</rect>
</property>
<property name="windowTitle">
Expand Down Expand Up @@ -98,6 +98,11 @@
<string>Image size</string>
</property>
</item>
<item>
<property name="text">
<string>Format conversion</string>
</property>
</item>
</item>
<item>
<property name="text">
Expand Down Expand Up @@ -1176,8 +1181,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>466</height>
<width>476</width>
<height>475</height>
</rect>
</property>
<layout class="QVBoxLayout" name="scrollAreaWidgetLayout">
Expand Down Expand Up @@ -1429,6 +1434,53 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="pageFormatConversion">
<layout class="QVBoxLayout" name="verticalLayout_15">
<item>
<widget class="QGroupBox" name="groupConversionFFmpeg">
<property name="title">
<string>FFmpeg</string>
</property>
<layout class="QFormLayout" name="formLayout_27">
<item row="0" column="0">
<widget class="QLabel" name="labelConversionFFmpegVersionLabel">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="labelConversionFFmpegVersion">
<property name="text">
<string>Loading...</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkConversionFFmpegRemuxWebmToMp4">
<property name="text">
<string>Remux WEBM files to MP4</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacerFormatConversion">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="pageInterface">
<layout class="QFormLayout" name="formLayout_7">
<property name="fieldGrowthPolicy">
Expand Down
6 changes: 3 additions & 3 deletions src/lib/src/downloader/image-downloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ QList<ImageSaveResult> ImageDownloader::afterTemporarySave(Image::SaveResult sav

QList<ImageSaveResult> result;
for (const QString &file : qAsConst(m_paths)) {
const QString path = file + suffix;
QString path = file + suffix;

// Don't overwrite already existing files
if (QFile::exists(file) || (!suffix.isEmpty() && QFile::exists(path))) {
Expand Down Expand Up @@ -475,10 +475,10 @@ QList<ImageSaveResult> ImageDownloader::afterTemporarySave(Image::SaveResult sav
}
}

result.append({ path, size, saveResult });
if (m_postSave) {
m_image->postSave(path, size, saveResult, m_addMd5, m_startCommands, m_count);
path = m_image->postSave(path, size, saveResult, m_addMd5, m_startCommands, m_count);
}
result.append({ path, size, saveResult });
}

if (!moved) {
Expand Down
84 changes: 84 additions & 0 deletions src/lib/src/ffmpeg.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include "ffmpeg.h"
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include "logger.h"


QString FFmpeg::version(int msecs)
{
QProcess process;
process.start("ffmpeg", { "-version" });

if (!process.waitForStarted(msecs)) {
return "";
}
if (!process.waitForFinished(msecs)) {
process.kill();
return "";
}
if (process.exitCode() != 0) {
return "";
}

const QString output = QString::fromLocal8Bit(process.readAllStandardOutput());
QString line = output.split("\n").first().trimmed();

if (line.startsWith("ffmpeg version ")) {
line = line.mid(15);
}

const qsizetype index = line.indexOf("Copyright");
if (index != -1) {
return line.left(index).trimmed();
}

return line.trimmed();
}


QString FFmpeg::remux(const QString &file, const QString &extension, bool deleteOriginal, int msecs)
{
// Since the method takes an extension, build an absolute path to the input file with that extension
const QFileInfo info(file);
const QString destination = info.path() + QDir::separator() + info.completeBaseName() + "." + extension;

// Ensure the operation is safe to do
if (!QFile::exists(file)) {
log(QStringLiteral("Cannot remux file that does not exist: `%1`").arg(file), Logger::Error);
return file;
}
if (QFile::exists(destination)) {
log(QStringLiteral("Remuxing the file `%1` would overwrite another file: `%2`").arg(file, destination), Logger::Error);
return file;
}

QProcess process;
process.start("ffmpeg", { "-n", "-loglevel", "error", "-i", file, "-c", "copy", destination });

// Ensure the process started successfully
if (!process.waitForStarted(msecs)) {
log(QStringLiteral("Could not start FFmpeg"));
return file;
}

// Wait for FFmpeg to finish
bool ok = process.waitForFinished(msecs);

// Print stdout and stderr to the log
const QString standardOutput = QString::fromLocal8Bit(process.readAllStandardOutput()).trimmed();
if (!standardOutput.isEmpty()) {
log(QString("[Exiftool] %1").arg(standardOutput), Logger::Debug);
}
const QString standardError = QString::fromLocal8Bit(process.readAllStandardError()).trimmed();
if (!standardError.isEmpty()) {
log(QString("[Exiftool] %1").arg(standardError), Logger::Error);
}

// On success, delete the original file if requested
if (ok && deleteOriginal) {
QFile::remove(file);
}

return destination;
}
30 changes: 30 additions & 0 deletions src/lib/src/ffmpeg.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef FFMPEG_H
#define FFMPEG_H

#include <QString>


class FFmpeg
{
public:
/**
* Get the version of FFmpeg.
*
* @param msecs The duration to wait in milliseconds for the version command to run.
* @return The version number found, with basic parsing done (ex: "4.4.3").
*/
static QString version(int msecs = 30000);

/**
* Remux a file to a different format, copying the streams.
*
* @param file The file to remux.
* @param extension The target extension (ex: "mp4").
* @param deleteOriginal Whether to delete the original file on success.
* @param msecs The duration to wait in milliseconds for the command to run.
* @return The destination file path on success, the original file path on error.
*/
static QString remux(const QString &file, const QString &extension, bool deleteOriginal = true, int msecs = 30000);
};

#endif // FFMPEG_H
2 changes: 1 addition & 1 deletion src/lib/src/loader/downloadable.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Downloadable
virtual QStringList paths(const Filename &filename, const QString &folder, int count) const = 0;
const QMap<QString, Token> &tokens(Profile *profile) const;
virtual SaveResult preSave(const QString &path, Size size) = 0;
virtual void postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count, bool basic = false) = 0;
virtual QString postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count, bool basic = false) = 0;

virtual QColor color() const = 0;
virtual QString tooltip() const = 0;
Expand Down
24 changes: 17 additions & 7 deletions src/lib/src/models/image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "downloader/extension-rotator.h"
#include "exiftool.h"
#include "favorite.h"
#include "ffmpeg.h"
#include "filename/filename.h"
#include "filtering/tag-filter-list.h"
#include "functions.h"
Expand Down Expand Up @@ -701,11 +702,9 @@ QString &pathTokens(QString &filename, const QString &path)
.replace("%dir:nobackslash%", QString(dir).replace("\\", "/"))
.replace("%dir%", dir);
}
void Image::postSaving(const QString &path, Size size, bool addMd5, bool startCommands, int count, bool basic)
QString Image::postSaving(const QString &originalPath, Size size, bool addMd5, bool startCommands, int count, bool basic)
{
if (addMd5) {
m_profile->addMd5(md5(), path);
}
QString path = originalPath;

// Save info to a text file
if (!basic) {
Expand Down Expand Up @@ -767,8 +766,14 @@ void Image::postSaving(const QString &path, Size size, bool addMd5, bool startCo
commands.after();
}

// Metadata
const QString &ext = extension();

// FFmpeg
if (ext == QStringLiteral("webm") && m_settings->value("Save/FFmpegRemuxWebmToMp4", false).toBool()) {
path = FFmpeg::remux(path, "mp4");
}

// Metadata
#ifdef WIN_FILE_PROPS
const QStringList exts = m_settings->value("Save/MetadataPropsysExtensions", "jpg jpeg mp4").toString().split(' ', Qt::SkipEmptyParts);
if (exts.isEmpty() || exts.contains(ext)) {
Expand Down Expand Up @@ -799,7 +804,12 @@ void Image::postSaving(const QString &path, Size size, bool addMd5, bool startCo
}
}

if (addMd5) {
m_profile->addMd5(md5(), path);
}

setSavePath(path, size);
return path;
}


Expand Down Expand Up @@ -1218,10 +1228,10 @@ QMap<QString, Token> Image::generateTokens(Profile *profile) const
return tokens;
}

void Image::postSave(const QString &path, Size size, SaveResult res, bool addMd5, bool startCommands, int count, bool basic)
QString Image::postSave(const QString &path, Size size, SaveResult res, bool addMd5, bool startCommands, int count, bool basic)
{
static const QList<SaveResult> md5Results { SaveResult::Moved, SaveResult::Copied, SaveResult::Shortcut, SaveResult::Linked, SaveResult::Saved };
postSaving(path, size, addMd5 && md5Results.contains(res), startCommands, count, basic);
return postSaving(path, size, addMd5 && md5Results.contains(res), startCommands, count, basic);
}

bool Image::isValid() const
Expand Down
4 changes: 2 additions & 2 deletions src/lib/src/models/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class Image : public QObject, public Downloadable
QStringList paths(const Filename &filename, const QString &folder, int count) const override;
QMap<QString, Token> generateTokens(Profile *profile) const override;
SaveResult preSave(const QString &path, Size size) override;
void postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count, bool basic = false) override;
QString postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count, bool basic = false) override;

// Tokens
template <typename T>
Expand All @@ -125,7 +125,7 @@ class Image : public QObject, public Downloadable
protected:
void init();
QString md5forced() const;
void postSaving(const QString &path, Size size, bool addMd5 = true, bool startCommands = false, int count = 1, bool basic = false);
QString postSaving(const QString &path, Size size, bool addMd5 = true, bool startCommands = false, int count = 1, bool basic = false);

public slots:
void loadDetails(bool rateLimit = false);
Expand Down

0 comments on commit 48bcda6

Please # to comment.