übernahme Code Shortcut

This commit is contained in:
georg0480
2026-01-31 15:28:10 +01:00
parent 6f4d6b9301
commit ef46c21291
1787 changed files with 1126465 additions and 0 deletions

266
src/jobs/abstractjob.cpp Normal file
View File

@@ -0,0 +1,266 @@
/*
* Copyright (c) 2012-2025 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 "abstractjob.h"
#include "Logger.h"
#include "postjobaction.h"
#include <QAction>
#include <QApplication>
#include <QTimer>
#ifdef Q_OS_WIN
#include <windows.h>
#else
#include <signal.h>
#endif
AbstractJob::AbstractJob(const QString &name, QThread::Priority priority)
: QProcess(0)
, m_item(0)
, m_ran(false)
, m_killed(false)
, m_label(name)
, m_startingPercent(0)
, m_priority(priority)
, m_isPaused(false)
{
setObjectName(name);
connect(this,
SIGNAL(finished(int, QProcess::ExitStatus)),
this,
SLOT(onFinished(int, QProcess::ExitStatus)));
connect(this, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(this, SIGNAL(started()), this, SLOT(onStarted()));
connect(this,
SIGNAL(progressUpdated(QStandardItem *, int)),
SLOT(onProgressUpdated(QStandardItem *, int)));
m_actionPause = new QAction(tr("Pause This Job"), this);
m_standardActions << m_actionPause;
m_actionPause->setEnabled(false);
m_actionResume = new QAction(tr("Resume This Job"), this);
m_actionResume->setEnabled(false);
m_standardActions << m_actionResume;
connect(m_actionPause, &QAction::triggered, this, &AbstractJob::pause);
connect(m_actionResume, &QAction::triggered, this, &AbstractJob::resume);
connect(this, &AbstractJob::finished, this, [this]() {
m_actionPause->setEnabled(false);
m_actionResume->setEnabled(false);
});
}
void AbstractJob::start()
{
m_killed = false;
m_ran = true;
m_estimateTime.start();
m_totalTime.start();
emit progressUpdated(m_item, 0);
}
void AbstractJob::setStandardItem(QStandardItem *item)
{
m_item = item;
}
QStandardItem *AbstractJob::standardItem()
{
return m_item;
}
bool AbstractJob::ran() const
{
return m_ran;
}
bool AbstractJob::stopped() const
{
return m_killed;
}
void AbstractJob::appendToLog(const QString &s)
{
if (m_log.size() < 100 * 1024 * 1024 /* MiB */) {
m_log.append(s);
}
}
QString AbstractJob::log() const
{
return m_log;
}
void AbstractJob::setLabel(const QString &label)
{
m_label = label;
}
QTime AbstractJob::estimateRemaining(int percent)
{
QTime result;
if (percent) {
int averageMs = m_estimateTime.elapsed() / qMax(1, percent - qMax(0, m_startingPercent));
result = QTime::fromMSecsSinceStartOfDay(averageMs * (100 - percent));
}
return result;
}
void AbstractJob::setPostJobAction(PostJobAction *action)
{
m_postJobAction.reset(action);
}
bool AbstractJob::paused() const
{
return m_isPaused;
}
void AbstractJob::start(const QString &program, const QStringList &arguments)
{
QString prog = program;
QStringList args = arguments;
#ifndef Q_OS_WIN
if (m_priority == QThread::LowPriority || m_priority == QThread::HighPriority) {
args.prepend(program);
args.prepend(m_priority == QThread::LowPriority ? "3" : "-3");
args.prepend("-n");
prog = "nice";
}
#endif
QProcess::start(prog, args);
AbstractJob::start();
m_actionPause->setEnabled(true);
m_actionResume->setEnabled(false);
m_isPaused = false;
}
void AbstractJob::stop()
{
if (paused()) {
#ifdef Q_OS_WIN
::DebugActiveProcessStop(QProcess::processId());
#else
::kill(QProcess::processId(), SIGCONT);
#endif
}
closeWriteChannel();
terminate();
QTimer::singleShot(2000, this, SLOT(kill()));
m_killed = true;
m_actionPause->setEnabled(false);
m_actionResume->setEnabled(false);
}
void AbstractJob::pause()
{
m_isPaused = true;
m_actionPause->setEnabled(false);
m_actionResume->setEnabled(true);
#ifdef Q_OS_WIN
::DebugActiveProcess(QProcess::processId());
#else
::kill(QProcess::processId(), SIGSTOP);
#endif
emit progressUpdated(m_item, -1);
}
void AbstractJob::resume()
{
m_actionPause->setEnabled(true);
m_actionResume->setEnabled(false);
m_startingPercent = -1;
#ifdef Q_OS_WIN
::DebugActiveProcessStop(QProcess::processId());
#else
::kill(QProcess::processId(), SIGCONT);
#endif
m_isPaused = false;
emit progressUpdated(m_item, 0);
}
void AbstractJob::setKilled(bool killed)
{
m_killed = killed;
}
void AbstractJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
const QTime &time = QTime::fromMSecsSinceStartOfDay(m_totalTime.elapsed());
if (isOpen()) {
m_log.append(readAll());
}
if (exitStatus == QProcess::NormalExit && exitCode == 0 && !m_killed) {
if (m_postJobAction) {
m_postJobAction->doAction();
}
LOG_INFO() << "job succeeeded";
m_log.append(QStringLiteral("Completed successfully in %1\n").arg(time.toString()));
emit progressUpdated(m_item, 100);
emit finished(this, true);
} else if (m_killed) {
LOG_INFO() << "job stopped";
m_log.append(QStringLiteral("Stopped by user at %1\n").arg(time.toString()));
emit finished(this, false);
} else {
LOG_INFO() << "job failed with" << exitCode;
m_log.append(QStringLiteral("Failed with exit code %1\n").arg(exitCode));
emit finished(this, false);
}
m_isPaused = false;
}
void AbstractJob::onReadyRead()
{
QString msg;
do {
msg = readLine();
appendToLog(msg);
} while (!msg.isEmpty());
}
void AbstractJob::onStarted()
{
#ifdef Q_OS_WIN
qint64 processId = QProcess::processId();
HANDLE processHandle = OpenProcess(PROCESS_SET_INFORMATION, FALSE, processId);
if (processHandle) {
switch (m_priority) {
case QThread::LowPriority:
SetPriorityClass(processHandle, BELOW_NORMAL_PRIORITY_CLASS);
break;
case QThread::HighPriority:
SetPriorityClass(processHandle, ABOVE_NORMAL_PRIORITY_CLASS);
break;
default:
SetPriorityClass(processHandle, NORMAL_PRIORITY_CLASS);
}
CloseHandle(processHandle);
}
#endif
}
void AbstractJob::onProgressUpdated(QStandardItem *, int percent)
{
// Start timer on first reported percentage > 0.
if (percent == 1 || m_startingPercent < 0) {
m_estimateTime.restart();
m_startingPercent = percent;
}
}

100
src/jobs/abstractjob.h Normal file
View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2012-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/>.
*/
#ifndef ABSTRACTJOB_H
#define ABSTRACTJOB_H
#include "postjobaction.h"
#include "settings.h"
#include <QElapsedTimer>
#include <QList>
#include <QModelIndex>
#include <QProcess>
#include <QThread>
class QAction;
class QStandardItem;
class AbstractJob : public QProcess
{
Q_OBJECT
public:
explicit AbstractJob(const QString &name, QThread::Priority priority = Settings.jobPriority());
virtual ~AbstractJob() {}
void setStandardItem(QStandardItem *item);
QStandardItem *standardItem();
bool ran() const;
bool stopped() const;
bool isFinished() const { return (ran() && state() != QProcess::Running); }
void appendToLog(const QString &);
QString log() const;
QString label() const { return m_label; }
void setLabel(const QString &label);
QList<QAction *> standardActions() const { return m_standardActions; }
QList<QAction *> successActions() const { return m_successActions; }
QTime estimateRemaining(int percent);
QElapsedTimer time() const { return m_totalTime; }
void setPostJobAction(PostJobAction *action);
bool paused() const;
void setTarget(const QString &target) { m_target = target; }
QString target() { return m_target; }
bool hasPostJobAction() const { return !m_postJobAction.isNull(); }
public slots:
void start(const QString &program, const QStringList &arguments);
virtual void start();
virtual void stop();
void pause();
void resume();
signals:
void progressUpdated(QStandardItem *item, int percent);
void finished(AbstractJob *job, bool isSuccess, QString failureTime = QString());
protected:
void setKilled(bool = true);
QList<QAction *> m_standardActions;
QList<QAction *> m_successActions;
QStandardItem *m_item;
protected slots:
virtual void onFinished(int exitCode, QProcess::ExitStatus exitStatus = QProcess::NormalExit);
virtual void onReadyRead();
virtual void onStarted();
private slots:
void onProgressUpdated(QStandardItem *, int percent);
private:
bool m_ran;
bool m_killed;
QString m_log;
QString m_label;
QElapsedTimer m_estimateTime;
int m_startingPercent;
QElapsedTimer m_totalTime;
QScopedPointer<PostJobAction> m_postJobAction;
QThread::Priority m_priority;
QAction *m_actionPause;
QAction *m_actionResume;
bool m_isPaused;
QString m_target;
};
#endif // ABSTRACTJOB_H

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2023 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 "bitrateviewerjob.h"
#include "Logger.h"
#include "dialogs/bitratedialog.h"
#include "mainwindow.h"
#include "util.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
BitrateViewerJob::BitrateViewerJob(const QString &name, const QStringList &args, double fps)
: FfprobeJob(name, args)
, m_resource(args.last())
, m_fps(fps)
{
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
connect(action, &QAction::triggered, this, &BitrateViewerJob::onOpenTriggered);
m_successActions << action;
}
BitrateViewerJob::~BitrateViewerJob() {}
void BitrateViewerJob::onFinished(int exitCode, ExitStatus exitStatus)
{
AbstractJob::onFinished(exitCode, exitStatus);
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
QJsonParseError error;
auto s = log();
s = s.left(s.lastIndexOf('}') + 1);
auto doc = QJsonDocument::fromJson(s.toUtf8(), &error);
if (QJsonParseError::NoError == error.error && doc.isObject()) {
auto v = doc.object().value("packets");
if (v.isArray()) {
m_data = v.toArray();
onOpenTriggered();
}
} else {
LOG_ERROR() << "JSON parsing error:" << error.errorString();
}
}
}
void BitrateViewerJob::onOpenTriggered()
{
BitrateDialog dialog(Util::baseName(m_resource), m_fps, m_data, &MAIN);
dialog.exec();
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 202-2025 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/>.
*/
#ifndef BITRATEVIEWERJOB_H
#define BITRATEVIEWERJOB_H
#include "ffprobejob.h"
#include <QJsonArray>
class BitrateViewerJob : public FfprobeJob
{
Q_OBJECT
public:
BitrateViewerJob(const QString &name, const QStringList &args, double fps);
virtual ~BitrateViewerJob();
private slots:
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
void onOpenTriggered();
private:
QString m_resource;
double m_fps{0.0};
QJsonArray m_data;
};
#endif // BITRATEVIEWERJOB_H

View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2025 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 "dockerpulljob.h"
#include "Logger.h"
#include "settings.h"
#include <QRegularExpression>
DockerPullJob::DockerPullJob(const QString &imageRef, QThread::Priority priority)
: AbstractJob(QObject::tr("docker pull %1").arg(imageRef), priority)
, m_imageRef(imageRef)
{
setTarget(imageRef);
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
auto env = QProcessEnvironment::systemEnvironment();
env.remove("LD_LIBRARY_PATH");
setProcessEnvironment(env);
#endif
}
DockerPullJob::~DockerPullJob()
{
LOG_DEBUG() << "DockerPullJob destroyed";
}
void DockerPullJob::start()
{
QString docker = Settings.dockerPath();
QStringList args{QStringLiteral("pull"), m_imageRef};
setProcessChannelMode(QProcess::MergedChannels);
LOG_DEBUG() << docker + " " + args.join(' ');
AbstractJob::start(docker, args);
emit progressUpdated(m_item, 0);
}
void DockerPullJob::onReadyRead()
{
QString msg;
do {
msg = readLine();
if (!msg.isEmpty()) {
appendToLog(msg);
// Typical docker pull progress lines contain something like:
// Digest: sha256:...
// Status: Downloaded newer image for repository:tag
// or layer progress lines: <id>: Pull complete
// There's no simple percentage, so we approximate by counting 'Pull complete'.
static int totalComplete = 0;
if (msg.contains("Pull complete")) {
++totalComplete;
int percent = qMin(99, totalComplete * 5); // heuristic
if (percent != m_previousPercent) {
emit progressUpdated(m_item, percent);
m_previousPercent = percent;
}
} else if (msg.startsWith("Status: ")) {
emit progressUpdated(m_item, 100);
}
}
} while (!msg.isEmpty());
}

43
src/jobs/dockerpulljob.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 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/>.
*/
#ifndef DOCKERPULLJOB_H
#define DOCKERPULLJOB_H
#include "abstractjob.h"
#include <QString>
class DockerPullJob : public AbstractJob
{
Q_OBJECT
public:
DockerPullJob(const QString &imageRef, QThread::Priority priority = Settings.jobPriority());
virtual ~DockerPullJob();
public slots:
void start() override;
protected slots:
void onReadyRead() override;
private:
QString m_imageRef;
int m_previousPercent{-1};
};
#endif // DOCKERPULLJOB_H

319
src/jobs/encodejob.cpp Normal file
View File

@@ -0,0 +1,319 @@
/*
* Copyright (c) 2012-2025 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 "encodejob.h"
#include "dialogs/listselectiondialog.h"
#include "docks/timelinedock.h"
#include "jobqueue.h"
#include "jobs/videoqualityjob.h"
#include "mainwindow.h"
#include "qmltypes/qmlapplication.h"
#include "qmltypes/qmlutilities.h"
#include "settings.h"
#include "spatialmedia/spatialmedia.h"
#include "util.h"
#include <QAction>
#include <QDesktopServices>
#include <QDir>
#include <QDomDocument>
#include <QFileDialog>
#include <QFileInfo>
#include <QJSEngine>
#include <QTemporaryFile>
#include <QTextStream>
#include <QUrl>
#include "Logger.h"
EncodeJob::EncodeJob(const QString &name,
const QString &xml,
int frameRateNum,
int frameRateDen,
QThread::Priority priority)
: MeltJob(name, xml, frameRateNum, frameRateDen, priority)
{
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
action->setToolTip(tr("Open the output file in the Shotcut player"));
connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered()));
m_successActions << action;
action = new QAction(tr("Show In Files"), this);
action->setToolTip(tr("Show In Files"));
connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered()));
m_successActions << action;
action = new QAction(tr("Show In Folder"), this);
action->setToolTip(tr("Show In Folder"));
connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered()));
m_successActions << action;
action = new QAction(tr("Measure Video Quality..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(onVideoQualityTriggered()));
m_successActions << action;
action = new QAction(tr("Set Equirectangular..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(onSpatialMediaTriggered()));
m_successActions << action;
action = new QAction(tr("Embed Markers as Chapters..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(onEmbedChapters()));
m_successActions << action;
}
void EncodeJob::onVideoQualityTriggered()
{
// Get the location and file name for the report.
QString directory = Settings.encodePath();
QString caption = tr("Video Quality Report");
QString nameFilter = tr("Text Documents (*.txt);;All Files (*)");
QString reportPath = QFileDialog::getSaveFileName(&MAIN,
caption,
directory,
nameFilter,
nullptr,
Util::getFileDialogOptions());
if (!reportPath.isEmpty()) {
QFileInfo fi(reportPath);
if (fi.suffix().isEmpty())
reportPath += ".txt";
if (Util::warnIfNotWritable(reportPath, &MAIN, caption))
return;
// Get temp file for the new XML.
QScopedPointer<QTemporaryFile> tmp(Util::writableTemporaryFile(reportPath));
tmp->open();
// Generate the XML for the comparison.
Mlt::Tractor tractor(MLT.profile());
Mlt::Producer original(MLT.profile(), xmlPath().toUtf8().constData());
Mlt::Producer encoded(MLT.profile(), objectName().toUtf8().constData());
Mlt::Transition vqm(MLT.profile(), "vqm");
if (original.is_valid() && encoded.is_valid() && vqm.is_valid()) {
tractor.set_track(original, 0);
tractor.set_track(encoded, 1);
tractor.plant_transition(vqm);
vqm.set("render", 0);
MLT.saveXML(tmp->fileName(), &tractor, false /* without relative paths */, tmp.data());
tmp->close();
// Add consumer element to XML.
QFile f1(tmp->fileName());
f1.open(QIODevice::ReadOnly);
QDomDocument dom(tmp->fileName());
dom.setContent(&f1);
f1.close();
QDomElement consumerNode = dom.createElement("consumer");
QDomNodeList profiles = dom.elementsByTagName("profile");
if (profiles.isEmpty())
dom.documentElement().insertAfter(consumerNode, dom.documentElement());
else
dom.documentElement().insertAfter(consumerNode, profiles.at(profiles.length() - 1));
consumerNode.setAttribute("mlt_service", "null");
consumerNode.setAttribute("real_time", -1);
consumerNode.setAttribute("terminate_on_pause", 1);
// Create job and add it to the queue.
JOBS.add(new VideoQualityJob(objectName(),
dom.toString(2),
reportPath,
MLT.profile().frame_rate_num(),
MLT.profile().frame_rate_den()));
}
}
}
void EncodeJob::onSpatialMediaTriggered()
{
// Get the location and file name for the output file.
QString caption = tr("Set Equirectangular Projection");
QFileInfo info(objectName());
QString directory = QStringLiteral("%1/%2 - ERP.%3")
.arg(Settings.encodePath())
.arg(info.completeBaseName())
.arg(info.suffix());
QString filePath = QFileDialog::getSaveFileName(&MAIN,
caption,
directory,
QString(),
nullptr,
Util::getFileDialogOptions());
if (!filePath.isEmpty()) {
if (SpatialMedia::injectSpherical(objectName().toStdString(), filePath.toStdString())) {
MAIN.showStatusMessage(tr("Successfully wrote %1").arg(QFileInfo(filePath).fileName()));
} else {
MAIN.showStatusMessage(tr("An error occurred saving the projection."));
}
}
}
void EncodeJob::onEmbedChapters()
{
// Options dialog
auto uniqueColors = MAIN.timelineDock()->markersModel()->allColors();
if (uniqueColors.isEmpty()) {
return;
}
std::sort(uniqueColors.begin(), uniqueColors.end(), [=](const QColor &a, const QColor &b) {
if (a.hue() == b.hue()) {
if (a.saturation() == b.saturation()) {
return a.value() <= b.value();
}
return a.saturation() <= b.saturation();
}
return a.hue() <= b.hue();
});
QStringList colors;
for (auto &color : uniqueColors) {
colors << color.name();
}
const auto rangesOption = tr("Include ranges (Duration > 1 frame)?");
QStringList initialOptions;
for (auto &m : MAIN.timelineDock()->markersModel()->getMarkers()) {
if (m.end != m.start) {
initialOptions << rangesOption;
break;
}
}
ListSelectionDialog dialog(initialOptions, &MAIN);
dialog.setWindowModality(QmlApplication::dialogModality());
dialog.setWindowTitle(tr("Choose Markers"));
if (Settings.exportRangeMarkers()) {
dialog.setSelection({rangesOption});
}
dialog.setColors(colors);
if (dialog.exec() != QDialog::Accepted) {
return;
}
auto selection = dialog.selection();
Settings.setExportRangeMarkers(selection.contains(rangesOption));
// Get the location and file name for the output file.
QString caption = tr("Embed Chapters");
QFileInfo info(objectName());
QString directory = QStringLiteral("%1/%2 - Chapters.%3")
.arg(Settings.encodePath(), info.completeBaseName(), info.suffix());
QString filePath = QFileDialog::getSaveFileName(&MAIN,
caption,
directory,
QString(),
nullptr,
Util::getFileDialogOptions());
if (!filePath.isEmpty()) {
if (Util::warnIfNotWritable(filePath, &MAIN, caption))
return;
// Locate the JavaScript file in the filesystem.
QDir qmlDir = QmlUtilities::qmlDir();
qmlDir.cd("export-chapters");
auto jsFileName = qmlDir.absoluteFilePath("export-chapters.js");
QFile scriptFile(jsFileName);
if (scriptFile.open(QIODevice::ReadOnly)) {
// Read JavaScript into a string.
QTextStream stream(&scriptFile);
stream.setEncoding(QStringConverter::Utf8);
stream.setAutoDetectUnicode(true);
QString contents = stream.readAll();
scriptFile.close();
// Evaluate JavaScript.
QJSEngine jsEngine;
QJSValue result = jsEngine.evaluate(contents, jsFileName);
if (!result.isError()) {
// Call the JavaScript main function.
QJSValue options = jsEngine.newObject();
options.setProperty("ffmetadata", true);
if (selection.contains(rangesOption)) {
options.setProperty("includeRanges", true);
selection.removeOne(rangesOption);
}
QJSValue array = jsEngine.newArray(selection.size());
for (int i = 0; i < selection.size(); ++i)
array.setProperty(i, selection[i].toUpper());
options.setProperty("colors", array);
QJSValueList args;
args << MLT.XML(0, true, true) << options;
result = result.call(args);
if (!result.isError()) {
// Save the result with the export file name.
auto tempFile = Util::writableTemporaryFile(filePath, "shotcut-XXXXXX.txt");
tempFile->write(result.toString().toUtf8());
tempFile->close();
QStringList args;
args << "-i" << objectName() << "-i" << tempFile->fileName() << "-map_metadata"
<< "1"
<< "-c"
<< "copy"
<< "-y" << filePath;
auto job = new FfmpegJob(filePath, args, false);
job->setLabel(filePath);
tempFile->setParent(job);
JOBS.add(job);
}
} else {
LOG_ERROR() << "Uncaught exception at line" << result.property("lineNumber").toInt()
<< ":" << result.toString();
MAIN.showStatusMessage(tr("A JavaScript error occurred during export."));
}
} else {
MAIN.showStatusMessage(tr("Failed to open export-chapters.js"));
}
}
}
void EncodeJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
if (exitStatus != QProcess::NormalExit && exitCode != 0 && !stopped()) {
LOG_INFO() << "job failed with" << exitCode;
appendToLog(QStringLiteral("Failed with exit code %1\n").arg(exitCode));
bool isParallel = false;
// Parse the XML.
m_xml->open();
QDomDocument dom(xmlPath());
dom.setContent(m_xml.data());
m_xml->close();
// Locate the consumer element.
QDomNodeList consumers = dom.elementsByTagName("consumer");
for (int i = 0; i < consumers.length(); i++) {
QDomElement consumer = consumers.at(i).toElement();
// If real_time is set for parallel.
if (consumer.attribute("real_time").toInt() < -1) {
isParallel = true;
consumer.setAttribute("real_time", "-1");
}
}
if (isParallel) {
QString message(tr("Export job failed; trying again without Parallel processing."));
MAIN.showStatusMessage(message);
appendToLog(message.append("\n"));
m_xml->open();
QTextStream textStream(m_xml.data());
dom.save(textStream, 2);
m_xml->close();
MeltJob::start();
return;
}
}
MeltJob::onFinished(exitCode, exitStatus);
}

44
src/jobs/encodejob.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2012-2025 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/>.
*/
#ifndef ENCODEJOB_H
#define ENCODEJOB_H
#include "meltjob.h"
#include <QThread>
class EncodeJob : public MeltJob
{
Q_OBJECT
public:
EncodeJob(const QString &name,
const QString &xml,
int frameRateNum,
int frameRateDen,
const QThread::Priority priority);
private slots:
void onVideoQualityTriggered();
void onSpatialMediaTriggered();
void onEmbedChapters();
protected slots:
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
};
#endif // ENCODEJOB_H

121
src/jobs/ffmpegjob.cpp Normal file
View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2016-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 "ffmpegjob.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include "util.h"
#include <MltProperties.h>
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <cstdio>
FfmpegJob::FfmpegJob(const QString &name,
const QStringList &args,
bool isOpenLog,
QThread::Priority priority)
: AbstractJob(name, priority)
, m_duration(0.0)
, m_previousPercent(0)
, m_isOpenLog(isOpenLog)
{
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
connect(action, SIGNAL(triggered()), this, SLOT(onOpenTriggered()));
m_successActions << action;
m_args.append(args);
setLabel(tr("Check %1").arg(Util::baseName(name)));
}
FfmpegJob::~FfmpegJob()
{
if (objectName().contains("proxies") && objectName().contains(".pending.")) {
QFile::remove(objectName());
}
}
void FfmpegJob::start()
{
QString shotcutPath = qApp->applicationDirPath();
QFileInfo ffmpegPath(shotcutPath, "ffmpeg");
setReadChannel(QProcess::StandardError);
LOG_DEBUG() << ffmpegPath.absoluteFilePath() + " " + m_args.join(' ');
AbstractJob::start(ffmpegPath.absoluteFilePath(), m_args);
}
void FfmpegJob::stop()
{
setKilled(hasPostJobAction());
write("q");
QTimer::singleShot(3000, this, [this]() { AbstractJob::stop(); });
}
void FfmpegJob::onOpenTriggered()
{
if (m_isOpenLog) {
TextViewerDialog dialog(&MAIN);
dialog.setWindowTitle(tr("FFmpeg Log"));
dialog.setText(log());
dialog.exec();
} else {
MAIN.open(objectName().toUtf8().constData());
}
}
static double timeToSeconds(QString time)
{
int h, m, s, mil;
const int ret = std::sscanf(time.toLatin1().constData(), "%d:%d:%d.%d", &h, &m, &s, &mil);
if (ret != 4) {
LOG_ERROR() << "unable to parse time:" << time;
return -1.0;
}
return (h * 60.0 * 60.0) + (m * 60.0) + s + (mil / 100.0);
}
void FfmpegJob::onReadyRead()
{
QString msg;
do {
msg = readLine();
if (!msg.startsWith("frame=") && (!msg.trimmed().isEmpty())) {
appendToLog(msg);
}
if (m_duration == 0 && msg.contains("Duration:")) {
msg = msg.mid(msg.indexOf("Duration:") + 9);
msg = msg.left(msg.indexOf(','));
m_duration = timeToSeconds(msg);
emit progressUpdated(m_item, 0);
} else if (m_duration != 0 && msg.startsWith("frame=")) {
msg = msg.mid(msg.indexOf("time=") + 6);
msg = msg.left(msg.indexOf(" bitrate"));
double time = timeToSeconds(msg);
int percent = qRound(time * 100.0 / m_duration);
if (percent != m_previousPercent) {
emit progressUpdated(m_item, percent);
m_previousPercent = percent;
}
}
} while (!msg.isEmpty());
}

50
src/jobs/ffmpegjob.h Normal file
View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2016-2022 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/>.
*/
#ifndef FFMPEGJOB_H
#define FFMPEGJOB_H
#include "abstractjob.h"
#include <QStringList>
class FfmpegJob : public AbstractJob
{
Q_OBJECT
public:
FfmpegJob(const QString &name,
const QStringList &args,
bool isOpenLog = true,
QThread::Priority priority = Settings.jobPriority());
virtual ~FfmpegJob();
void start();
public slots:
virtual void stop();
private slots:
void onOpenTriggered();
void onReadyRead();
private:
QStringList m_args;
double m_duration;
int m_previousPercent;
bool m_isOpenLog;
};
#endif // FFMPEGJOB_H

57
src/jobs/ffprobejob.cpp Normal file
View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2016 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 "ffprobejob.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include "util.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
FfprobeJob::FfprobeJob(const QString &name, const QStringList &args)
: AbstractJob(name)
{
m_args.append(args);
}
FfprobeJob::~FfprobeJob() {}
void FfprobeJob::start()
{
QString shotcutPath = qApp->applicationDirPath();
QFileInfo ffprobePath(shotcutPath, "ffprobe");
setReadChannel(QProcess::StandardOutput);
LOG_DEBUG() << ffprobePath.absoluteFilePath() + " " + m_args.join(' ');
AbstractJob::start(ffprobePath.absoluteFilePath(), m_args);
}
void FfprobeJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
AbstractJob::onFinished(exitCode, exitStatus);
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
TextViewerDialog dialog(&MAIN);
dialog.setWindowTitle(tr("More Information"));
dialog.setText(log().replace("\\:", ":"));
dialog.exec();
}
deleteLater();
}

40
src/jobs/ffprobejob.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2016-2025 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/>.
*/
#ifndef FFPROBEJOB_H
#define FFPROBEJOB_H
#include "abstractjob.h"
#include <QStringList>
class FfprobeJob : public AbstractJob
{
Q_OBJECT
public:
FfprobeJob(const QString &name, const QStringList &args);
virtual ~FfprobeJob();
void start() override;
protected slots:
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
private:
QStringList m_args;
};
#endif // FFPROBEJOB_H

44
src/jobs/gopro2gpxjob.cpp Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 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 "gopro2gpxjob.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include "util.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
GoPro2GpxJob::GoPro2GpxJob(const QString &name, const QStringList &args)
: AbstractJob(name)
{
m_args.append(args);
setLabel(QStringLiteral("%1 %2").arg(tr("Export GPX"), Util::baseName(name)));
}
void GoPro2GpxJob::start()
{
QString shotcutPath = qApp->applicationDirPath();
QFileInfo gopro2gpxPath(shotcutPath, "gopro2gpx");
setReadChannel(QProcess::StandardOutput);
LOG_DEBUG() << gopro2gpxPath.absoluteFilePath() + " " + m_args.join(' ');
AbstractJob::start(gopro2gpxPath.absoluteFilePath(), m_args);
}

37
src/jobs/gopro2gpxjob.h Normal file
View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 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/>.
*/
#ifndef GOPRO2GPXJOB_H
#define GOPRO2GPXJOB_H
#include "abstractjob.h"
#include <QStringList>
class GoPro2GpxJob : public AbstractJob
{
Q_OBJECT
public:
GoPro2GpxJob(const QString &name, const QStringList &args);
virtual ~GoPro2GpxJob(){};
void start();
private:
QStringList m_args;
};
#endif // GOPRO2GPXJOB_H

View File

@@ -0,0 +1,215 @@
/*
* Copyright (c) 2025 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 "htmlgeneratorjob.h"
#include "Logger.h"
#include "mainwindow.h"
#include "mltcontroller.h"
#include "settings.h"
#include "shotcut_mlt_properties.h"
#include "widgets/htmlgeneratorwidget.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTemporaryFile>
static double fps()
{
return std::min(15.0, MLT.profile().fps());
}
HtmlGeneratorJob::HtmlGeneratorJob(const QString &name,
const QString &html,
const QString &outputPath,
int duration,
QThread::Priority priority)
: AbstractJob(name, priority)
, m_html(html)
, m_outputPath(outputPath)
, m_duration(duration)
, m_generator(nullptr)
, m_isGeneratingFrames(true)
, m_previousPercent(0)
{
setTarget(outputPath);
// Create temporary directory for animation frames
const auto outDir = QFileInfo(m_outputPath).dir();
m_tempDir.reset(new QTemporaryDir(outDir.filePath("shotcut-htmlgen-XXXXXX")));
if (!m_tempDir->isValid()) {
LOG_ERROR() << "Failed to create temp directory for HTML animation frames:"
<< m_tempDir->path();
}
auto *action = new QAction(tr("Open"), this);
action->setData("Open");
connect(action, &QAction::triggered, this, &HtmlGeneratorJob::onOpenTriggered);
m_successActions << action;
}
void HtmlGeneratorJob::start()
{
if (!m_tempDir || !m_tempDir->isValid()) {
LOG_ERROR() << "Invalid temp directory";
return;
}
// Create HTML file
m_htmlFilePath = m_tempDir->path() + "/animation.html";
QFile htmlFile(m_htmlFilePath);
if (!htmlFile.open(QIODevice::WriteOnly)) {
LOG_ERROR() << "Failed to create HTML file:" << htmlFile.errorString();
return;
}
htmlFile.write(m_html.toUtf8());
htmlFile.flush();
htmlFile.close();
// Create HTML generator for animation
m_generator = new HtmlGenerator(this);
// Set animation parameters: fps, duration in milliseconds
m_generator->setAnimationParameters(fps(), m_duration);
connect(m_generator,
&HtmlGenerator::imageReady,
this,
&HtmlGeneratorJob::onAnimationFramesReady);
connect(m_generator,
&HtmlGenerator::progressUpdate,
this,
&HtmlGeneratorJob::onHtmlGeneratorProgress);
const QString url("file://" + m_htmlFilePath);
const QSize size(qRound(MLT.profile().width() * MLT.profile().sar()), MLT.profile().height());
// Start the animation capture
AbstractJob::start();
m_isGeneratingFrames = true;
m_generator->launchBrowser(Settings.chromiumPath(), url, size, m_tempDir->path());
LOG_DEBUG() << "Started HTML animation generation:" << url;
}
void HtmlGeneratorJob::onAnimationFramesReady()
{
LOG_DEBUG() << "Animation frames ready, starting FFmpeg conversion";
m_isGeneratingFrames = false;
emit progressUpdated(m_item, 80); // Frames generation is done
// Clean up the generator
if (m_generator) {
m_generator->deleteLater();
m_generator = nullptr;
}
// Now start FFmpeg process
const auto shotcutPath = qApp->applicationDirPath();
const QFileInfo ffmpegPath(shotcutPath, "ffmpeg");
QStringList args;
args << "-r" << QString::number(fps()) << "-i" << m_tempDir->path() + "/frame_%04d.png"
<< "-codec:v"
<< "utvideo"
<< "-pix_fmt"
<< "gbrap"
<< "-y" << m_outputPath;
setReadChannel(QProcess::StandardError);
LOG_DEBUG() << ffmpegPath.absoluteFilePath() + " " + args.join(' ');
// Start FFmpeg process (this will call AbstractJob::start with program and args)
AbstractJob::start(ffmpegPath.absoluteFilePath(), args);
}
void HtmlGeneratorJob::onReadyRead()
{
if (m_isGeneratingFrames) {
return; // Don't process FFmpeg output during frame generation
}
// Process FFmpeg output for progress reporting
QString msg;
do {
msg = readLine();
if (!msg.startsWith("frame=") && (!msg.trimmed().isEmpty())) {
appendToLog(msg);
}
// Look for progress information in FFmpeg output
if (msg.contains("frame=")) {
// Extract frame number from FFmpeg output
static QRegularExpression frameRegex("frame=\\s*(\\d+)");
const auto match = frameRegex.match(msg);
if (match.hasMatch()) {
int currentFrame = match.captured(1).toInt();
int totalFrames = qRound((m_duration / 1000.0) * fps());
int percent = 80 + qRound((currentFrame * 80.0) / totalFrames); // 80-100%
if (percent != m_previousPercent) {
emit progressUpdated(m_item, percent);
m_previousPercent = percent;
}
}
}
} while (!msg.isEmpty());
}
void HtmlGeneratorJob::onHtmlGeneratorProgress(float progress)
{
if (m_isGeneratingFrames) {
// Map 0.0-1.0 to 0-80%
const auto percent = qRound(progress * 80.0);
emit progressUpdated(m_item, percent);
}
}
void HtmlGeneratorJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
AbstractJob::onFinished(exitCode, exitStatus);
if (exitCode == 0 && QFileInfo::exists(m_outputPath)) {
// Automatically open the captured file
QTimer::singleShot(0, this, SLOT(onOpenTriggered()));
}
}
void HtmlGeneratorJob::onOpenTriggered()
{
auto p = new Mlt::Producer(MLT.profile(), m_outputPath.toUtf8().constData());
p->set(kPrivateProducerProperty, property(kPrivateProducerProperty).toString().toLatin1());
p->set(HtmlGeneratorWidget::kColorProperty,
property(HtmlGeneratorWidget::kColorProperty).toByteArray());
p->set(HtmlGeneratorWidget::kCssProperty,
property(HtmlGeneratorWidget::kCssProperty).toByteArray());
p->set(HtmlGeneratorWidget::kBodyProperty,
property(HtmlGeneratorWidget::kBodyProperty).toByteArray());
p->set(HtmlGeneratorWidget::kJavaScriptProperty,
property(HtmlGeneratorWidget::kJavaScriptProperty).toByteArray());
p->set(HtmlGeneratorWidget::kLine1Property,
property(HtmlGeneratorWidget::kLine1Property).toByteArray());
p->set(HtmlGeneratorWidget::kLine2Property,
property(HtmlGeneratorWidget::kLine2Property).toByteArray());
p->set(HtmlGeneratorWidget::kLine3Property,
property(HtmlGeneratorWidget::kLine3Property).toByteArray());
MAIN.open(p, false);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 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/>.
*/
#ifndef HTMLGENERATORJOB_H
#define HTMLGENERATORJOB_H
#include "abstractjob.h"
#include "htmlgenerator.h"
#include <QDir>
#include <QTemporaryDir>
class HtmlGeneratorJob : public AbstractJob
{
Q_OBJECT
public:
HtmlGeneratorJob(const QString &name,
const QString &html,
const QString &outputPath,
int duration,
QThread::Priority priority = QThread::HighPriority);
void start() override;
protected slots:
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
void onReadyRead() override;
private slots:
void onAnimationFramesReady();
void onHtmlGeneratorProgress(float progress);
void onOpenTriggered();
private:
QString m_html;
QString m_outputPath;
int m_duration;
HtmlGenerator *m_generator; // owned via parent QObject
std::unique_ptr<QTemporaryDir> m_tempDir;
QString m_htmlFilePath;
bool m_isGeneratingFrames;
int m_previousPercent;
};
#endif // HTMLGENERATORJOB_H

227
src/jobs/kokorodokijob.cpp Normal file
View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2025 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 "kokorodokijob.h"
#include "Logger.h"
#include "jobqueue.h"
#include "jobs/dockerpulljob.h"
#include "mainwindow.h"
#include "qmltypes/qmlapplication.h"
#include "settings.h"
#include "util.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QRegularExpression>
static const auto kDockerImageRef = QStringLiteral("mltframework/kokorodoki");
KokorodokiJob::KokorodokiJob(const QString &inputFile,
const QString &outputFile,
const QString &language,
const QString &voice,
double speed,
QThread::Priority priority)
: AbstractJob(QObject::tr("Text to speech: %1").arg(QFileInfo(outputFile).fileName()), priority)
, m_inputFile(inputFile)
, m_outputFile(outputFile)
, m_language(language)
, m_voice(voice)
, m_speed(speed)
{
setTarget(outputFile);
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
connect(action, &QAction::triggered, this, [this]() { onOpenTriggered(); });
m_successActions << action;
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
auto env = QProcessEnvironment::systemEnvironment();
env.remove("LD_LIBRARY_PATH");
setProcessEnvironment(env);
#endif
}
KokorodokiJob::~KokorodokiJob()
{
LOG_DEBUG() << "KokorodokiJob destroyed";
}
bool KokorodokiJob::checkDockerImage(QWidget *parent)
{
auto dockerKokorodokiExists = Util::dockerStatus("mltframework/kokorodoki");
if (!dockerKokorodokiExists.first) {
QMessageBox
dialog(QMessageBox::Warning,
QApplication::applicationName(),
parent
->tr("<p>This feature requires <b><a "
"href=\"https://www.docker.com/\">Docker</a></b>, which provides an "
"installer, and the automatic download of an <b><big>13.2 GB</big></b> "
"Docker image.</p><p>If you already installed Docker it could not be "
"found at the expected location: <tt>%1</tt></p><p>Click <b>OK</b> to "
"continue and locate the <tt>docker</tt> program on your system.</p>")
.arg(Settings.dockerPath()),
QMessageBox::Cancel | QMessageBox::Ok,
parent);
dialog.setWindowModality(QmlApplication::dialogModality());
dialog.setDefaultButton(QMessageBox::Ok);
dialog.setEscapeButton(QMessageBox::Cancel);
if (QMessageBox::Cancel == dialog.exec())
return false;
const auto docker = Util::getExecutable(parent);
if (docker.isEmpty())
return false;
Settings.setDockerPath(docker);
} else if (!dockerKokorodokiExists.second) {
QMessageBox
dialog(QMessageBox::Question,
QApplication::applicationName(),
parent
->tr("<p>This feature requires the automatic download of an <b><big>13.2 "
"GB</big></b> Docker image.</p><p>Do you want to continue?</p>")
.arg(Settings.dockerPath()),
QMessageBox::No | QMessageBox::Yes,
parent);
dialog.setWindowModality(QmlApplication::dialogModality());
dialog.setDefaultButton(QMessageBox::Yes);
dialog.setEscapeButton(QMessageBox::No);
if (QMessageBox::No == dialog.exec())
return false;
}
return true;
}
// static
void KokorodokiJob::prepareAndRun(QWidget *parent, std::function<void()> callback)
{
if (!callback)
return;
Util::isDockerImageCurrentAsync(kDockerImageRef, parent, [callback](bool current) {
LOG_DEBUG() << "dockerImageIsCurrent" << current;
if (current) {
callback();
} else {
auto pullJob = new DockerPullJob(kDockerImageRef);
QObject::connect(pullJob,
&AbstractJob::finished,
pullJob,
[callback](AbstractJob *, bool success) {
if (success)
callback();
});
JOBS.add(pullJob);
}
});
}
void KokorodokiJob::start()
{
QFileInfo inFi(m_inputFile);
QFileInfo outFi(m_outputFile);
if (inFi.dir() != outFi.dir()) {
// For safety ensure same directory
LOG_INFO() << "Input and output not in same directory; aborting.";
emit progressUpdated(m_item, 0);
kill();
return;
}
auto docker = Settings.dockerPath();
auto dirPath = outFi.dir().absolutePath();
auto baseIn = inFi.fileName();
auto baseOut = outFi.fileName();
QStringList args;
args << "run"
<< "--cpus" << QString::number(std::max(1, QThread::idealThreadCount() - 1)) << "-t"
<< "-e"
<< "SKIP_STARTUP_MSG=1";
args << "-v" << QStringLiteral("%1:/mnt:Z").arg(dirPath);
args << "-w"
<< "/mnt";
args << "--rm";
args << "mltframework/kokorodoki";
args << "-l" << m_language;
args << "-v" << m_voice;
args << "-s" << QString::number(m_speed, 'f', 2);
args << "-f" << baseIn;
args << "-o" << baseOut;
setReadChannel(QProcess::StandardOutput);
setProcessChannelMode(QProcess::MergedChannels);
LOG_DEBUG() << docker + " " + args.join(' ');
AbstractJob::start(docker, args);
emit progressUpdated(m_item, 0);
}
void KokorodokiJob::onReadyRead()
{
QString line;
static QRegularExpression timeRe("(\\d{1,2}):(\\d{2}):(\\d{2})\\b");
static QRegularExpression ansiRe("\x1B\[[0-9;?]*[A-Za-z]");
// Any Braille pattern character (U+2800U+28FF) often used by rich.console spinners.
static QRegularExpression brailleRe("[\u2800-\u28FF]");
do {
line = readLine();
if (!line.isEmpty()) {
// Remove ANSI escape sequences (color/spinner control, etc.).
line.replace(ansiRe, "");
// Remove braille spinner characters.
line.replace(brailleRe, "");
// Trim space left after spinner removal.
line = line.trimmed();
// Suppress duplicate consecutive lines.
if (line == m_lastLogLine) {
continue;
}
m_lastLogLine = line;
appendToLog(line.append('\n'));
// Very rough progress: update percent based on elapsed time relative to last seen.
const auto match = timeRe.match(line);
if (match.hasMatch()) {
const auto h = match.captured(1);
const auto m = match.captured(2);
const auto s = match.captured(3);
const auto tc = h + ":" + m + ":" + s;
if (tc != m_lastTimecode) {
m_lastTimecode = tc;
// Convert to seconds; no known total so just map seconds to a capped percent.
auto seconds = h.toInt() * 3600 + m.toInt() * 60 + s.toInt();
emit progressUpdated(m_item, std::min(99, 3 + seconds));
}
}
if (line.contains("Kokoro initialized"))
emit progressUpdated(m_item, 2);
else if (line.contains("pipeline initialized"))
emit progressUpdated(m_item, 3);
else if (line.contains("Saved to "))
emit progressUpdated(m_item, 100);
}
} while (!line.isEmpty());
}
void KokorodokiJob::onOpenTriggered()
{
MAIN.open(m_outputFile);
}

55
src/jobs/kokorodokijob.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2025 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/>.
*/
#ifndef KOKORODOKIJOB_H
#define KOKORODOKIJOB_H
#include "abstractjob.h"
class KokorodokiJob : public AbstractJob
{
Q_OBJECT
public:
KokorodokiJob(const QString &inputFile,
const QString &outputFile,
const QString &language,
const QString &voice,
double speed,
QThread::Priority priority = Settings.jobPriority());
~KokorodokiJob() override;
static bool checkDockerImage(QWidget *parent);
// Calls 'callback' once the image is ready (or immediately if already current).
// 'parent' is used for dialogs.
static void prepareAndRun(QWidget *parent, std::function<void()> callback);
public slots:
void start() override;
protected slots:
void onReadyRead() override;
private:
void onOpenTriggered();
QString m_inputFile;
QString m_outputFile;
QString m_language;
QString m_voice;
double m_speed;
QString m_lastTimecode;
QString m_lastLogLine;
};
#endif // KOKORODOKIJOB_H

239
src/jobs/meltjob.cpp Normal file
View File

@@ -0,0 +1,239 @@
/*
* Copyright (c) 2012-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 "meltjob.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include "settings.h"
#include "util.h"
#include <QAction>
#include <QApplication>
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QTimer>
MeltJob::MeltJob(const QString &name,
const QString &xml,
int frameRateNum,
int frameRateDen,
QThread::Priority priority)
: AbstractJob(name, priority)
, m_isStreaming(false)
, m_previousPercent(0)
, m_currentFrame(0)
, m_useMultiConsumer(false)
{
setTarget(name);
if (!xml.isEmpty()) {
QAction *action = new QAction(tr("View XML"), this);
action->setToolTip(tr("View the MLT XML for this job"));
connect(action, SIGNAL(triggered()), this, SLOT(onViewXmlTriggered()));
m_standardActions << action;
m_xml.reset(Util::writableTemporaryFile(name, "shotcut-XXXXXX.mlt"));
if (m_xml->open()) {
m_xml->write(xml.toUtf8());
m_xml->close();
}
} else {
// Not an EncodeJob
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
action->setToolTip(tr("Open the output file in the Shotcut player"));
connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered()));
m_successActions << action;
action = new QAction(tr("Show In Folder"), this);
action->setToolTip(tr("Show In Files"));
connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered()));
m_successActions << action;
action = new QAction(tr("Show In Folder"), this);
action->setToolTip(tr("Show In Folder"));
connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered()));
m_successActions << action;
}
if (frameRateNum > 0 && frameRateDen > 0)
m_profile.set_frame_rate(frameRateNum, frameRateDen);
}
void MeltJob::onOpenTiggered()
{
MAIN.open(objectName().toUtf8().constData());
}
void MeltJob::onShowFolderTriggered()
{
Util::showInFolder(objectName());
}
void MeltJob::onShowInFilesTriggered()
{
MAIN.showInFiles(objectName());
}
MeltJob::MeltJob(const QString &name,
const QString &xml,
const QStringList &args,
int frameRateNum,
int frameRateDen)
: MeltJob(name, xml, frameRateNum, frameRateDen)
{
m_args = args;
}
MeltJob::MeltJob(const QString &name, const QStringList &args, int frameRateNum, int frameRateDen)
: MeltJob(name, QString(), frameRateNum, frameRateDen)
{
m_args = args;
}
MeltJob::~MeltJob()
{
LOG_DEBUG() << "begin";
}
void MeltJob::start()
{
if (m_args.isEmpty() && !m_xml) {
AbstractJob::start();
LOG_ERROR() << "the job XML is empty!";
appendToLog("Error: the job XML is empty!\n");
QTimer::singleShot(0, this, [=]() { emit finished(this, false); });
return;
}
QString shotcutPath = qApp->applicationDirPath();
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
QFileInfo meltPath(shotcutPath, "melt-7");
#else
QFileInfo meltPath(shotcutPath, "melt");
#endif
setReadChannel(QProcess::StandardError);
QStringList args;
args << "-verbose";
args << "-progress2";
args << "-abort";
if (!m_xml.isNull()) {
if (m_useMultiConsumer) {
args << "xml:" + QUrl::toPercentEncoding(xmlPath()) + "?multi:1";
} else {
args << "xml:" + QUrl::toPercentEncoding(xmlPath());
}
}
if (m_args.size() > 0) {
args.append(m_args);
}
if (m_in > -1) {
args << QStringLiteral("in=%1").arg(m_in);
}
if (m_out > -1) {
args << QStringLiteral("out=%1").arg(m_out);
}
LOG_DEBUG() << meltPath.absoluteFilePath() + " " + args.join(' ');
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
#ifndef Q_OS_MAC
// These environment variables fix rich text rendering for high DPI
// fractional or otherwise.
env.insert("QT_AUTO_SCREEN_SCALE_FACTOR", "1");
env.insert("QT_SCALE_FACTOR_ROUNDING_POLICY", "PassThrough");
#endif
if (!Settings.encodeHardwareDecoder()) {
env.remove("MLT_AVFORMAT_HWACCEL");
}
env.remove("MLT_AVFORMAT_HWACCEL_PPS");
setProcessEnvironment(env);
#ifdef Q_OS_WIN
if (m_isStreaming)
args << "-getc";
#endif
AbstractJob::start(meltPath.absoluteFilePath(), args);
}
QString MeltJob::xml()
{
if (m_xml->open()) {
QString s(m_xml->readAll());
m_xml->close();
return s;
} else {
return QString();
}
}
void MeltJob::setIsStreaming(bool streaming)
{
m_isStreaming = streaming;
}
void MeltJob::setUseMultiConsumer(bool multi)
{
m_useMultiConsumer = multi;
}
void MeltJob::setInAndOut(int in, int out)
{
m_in = in;
m_out = out;
}
void MeltJob::onViewXmlTriggered()
{
TextViewerDialog dialog(&MAIN, true);
dialog.setWindowTitle(tr("MLT XML"));
dialog.setText(xml());
dialog.exec();
}
void MeltJob::onReadyRead()
{
QString msg;
do {
msg = readLine();
int index = msg.indexOf("Frame:");
if (index > -1) {
index += 6;
int comma = msg.indexOf(',', index);
m_currentFrame = msg.mid(index, comma - index).toInt();
}
index = msg.indexOf("percentage:");
if (index > -1) {
int percent = msg.mid(index + 11).toInt();
if (percent > m_previousPercent) {
emit progressUpdated(m_item, percent);
QCoreApplication::processEvents();
m_previousPercent = percent;
}
} else {
appendToLog(msg);
}
} while (!msg.isEmpty());
}
void MeltJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
AbstractJob::onFinished(exitCode, exitStatus);
if (exitStatus != QProcess::NormalExit && exitCode != 0 && !stopped()) {
Mlt::Producer producer(m_profile, "colour:");
QString time = QString::fromLatin1(producer.frames_to_time(m_currentFrame));
emit finished(this, false, time);
}
}

73
src/jobs/meltjob.h Normal file
View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2012-2025 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/>.
*/
#ifndef MELTJOB_H
#define MELTJOB_H
#include "abstractjob.h"
#include <MltProfile.h>
#include <QTemporaryFile>
class MeltJob : public AbstractJob
{
Q_OBJECT
public:
MeltJob(const QString &name,
const QString &xml,
int frameRateNum,
int frameRateDen,
QThread::Priority priority = Settings.jobPriority());
MeltJob(const QString &name, const QStringList &args, int frameRateNum, int frameRateDen);
MeltJob(const QString &name,
const QString &xml,
const QStringList &args,
int frameRateNum,
int frameRateDen);
virtual ~MeltJob();
QString xml();
QString xmlPath() const { return m_xml->fileName(); }
void setIsStreaming(bool streaming);
void setUseMultiConsumer(bool multi = true);
void setInAndOut(int in, int out);
public slots:
void start() override;
void onViewXmlTriggered();
protected slots:
virtual void onOpenTiggered();
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
void onShowFolderTriggered();
void onShowInFilesTriggered();
void onReadyRead() override;
protected:
QScopedPointer<QTemporaryFile> m_xml;
private:
bool m_isStreaming;
int m_previousPercent;
QStringList m_args;
int m_currentFrame;
Mlt::Profile m_profile;
bool m_useMultiConsumer;
int m_in{-1};
int m_out{-1};
};
#endif // MELTJOB_H

126
src/jobs/postjobaction.cpp Normal file
View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2018-2024 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 "postjobaction.h"
#include "Logger.h"
#include "docks/playlistdock.h"
#include "docks/subtitlesdock.h"
#include "mainwindow.h"
#include "shotcut_mlt_properties.h"
#include <QFile>
// For file time functions in FilePropertiesPostJobAction::doAction();
#include <sys/stat.h>
#include <utime.h>
void FilePropertiesPostJobAction::doAction()
{
// TODO: When QT 5.10 is available, use QFileDevice functions
#ifdef Q_OS_WIN
struct _stat srcTime;
struct _utimbuf dstTime;
_stat(m_srcFile.toUtf8().constData(), &srcTime);
dstTime.actime = srcTime.st_atime;
dstTime.modtime = srcTime.st_mtime;
_utime(m_dstFile.toUtf8().constData(), &dstTime);
#else
struct stat srcTime;
struct utimbuf dstTime;
stat(m_srcFile.toUtf8().constData(), &srcTime);
dstTime.actime = srcTime.st_atime;
dstTime.modtime = srcTime.st_mtime;
utime(m_dstFile.toUtf8().constData(), &dstTime);
#endif
}
void OpenPostJobAction::doAction()
{
FilePropertiesPostJobAction::doAction();
if (!m_fileNameToRemove.isEmpty()) {
QFile::remove(m_fileNameToRemove);
}
MAIN.open(m_dstFile);
MAIN.playlistDock()->onAppendCutActionTriggered();
}
void ReplaceOnePostJobAction::doAction()
{
FilePropertiesPostJobAction::doAction();
if (!m_fileNameToRemove.isEmpty()) {
QFile::remove(m_fileNameToRemove);
}
Mlt::Producer newProducer(MLT.profile(), m_dstFile.toUtf8().constData());
if (newProducer.is_valid()) {
Mlt::Producer *producer = MLT.setupNewProducer(&newProducer);
producer->set_in_and_out(m_in, -1);
MAIN.replaceInTimeline(m_uuid, *producer);
delete producer;
}
}
void ReplaceAllPostJobAction::doAction()
{
FilePropertiesPostJobAction::doAction();
Mlt::Producer newProducer(MLT.profile(), m_dstFile.toUtf8().constData());
if (newProducer.is_valid()) {
Mlt::Producer *producer = MLT.setupNewProducer(&newProducer);
MAIN.replaceAllByHash(m_hash, *producer);
delete producer;
}
}
void ProxyReplacePostJobAction::doAction()
{
FilePropertiesPostJobAction::doAction();
QFileInfo info(m_dstFile);
QString newFileName = info.path() + "/" + info.baseName() + "." + info.suffix();
QFile::remove(newFileName);
if (QFile::rename(m_dstFile, newFileName)) {
Mlt::Producer newProducer(MLT.profile(), newFileName.toUtf8().constData());
if (newProducer.is_valid()) {
Mlt::Producer *producer = MLT.setupNewProducer(&newProducer);
producer->set(kIsProxyProperty, 1);
producer->set(kOriginalResourceProperty, m_srcFile.toUtf8().constData());
MAIN.replaceAllByHash(m_hash, *producer, true);
delete producer;
} else {
LOG_WARNING() << "proxy file is invalid" << newFileName;
QFile::remove(m_dstFile);
}
} else {
LOG_WARNING() << "failed to rename" << m_dstFile << "as" << newFileName;
QFile::remove(m_dstFile);
}
}
void ProxyFinalizePostJobAction::doAction()
{
FilePropertiesPostJobAction::doAction();
QFileInfo info(m_dstFile);
QString newFileName = info.path() + "/" + info.baseName() + "." + info.suffix();
if (!QFile::rename(m_dstFile, newFileName)) {
LOG_WARNING() << "failed to rename" << m_dstFile << "as" << newFileName;
QFile::remove(m_dstFile);
}
}
void ImportSrtPostJobAction::doAction()
{
m_dock->importSrtFromFile(m_srtFile, m_trackName, m_lang, m_includeNonspoken);
}

152
src/jobs/postjobaction.h Normal file
View File

@@ -0,0 +1,152 @@
/*
* Copyright (c) 2018-2024 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/>.
*/
#ifndef POSTJOBACTION_H
#define POSTJOBACTION_H
#include <QString>
#include <QUuid>
class PostJobAction
{
public:
virtual ~PostJobAction() {}
virtual void doAction() = 0;
};
class FilePropertiesPostJobAction : public PostJobAction
{
public:
FilePropertiesPostJobAction(const QString &srcFile, const QString &dstFile)
: m_srcFile(srcFile)
, m_dstFile(dstFile)
{}
virtual ~FilePropertiesPostJobAction() {}
virtual void doAction();
protected:
QString m_srcFile;
QString m_dstFile;
};
class OpenPostJobAction : public FilePropertiesPostJobAction
{
public:
OpenPostJobAction(const QString &srcFile,
const QString &dstFile,
const QString &fileNameToRemove)
: FilePropertiesPostJobAction(srcFile, dstFile)
, m_fileNameToRemove(fileNameToRemove)
{}
void doAction();
private:
QString m_fileNameToRemove;
};
class ReplaceOnePostJobAction : public FilePropertiesPostJobAction
{
public:
ReplaceOnePostJobAction(const QString &srcFile,
const QString &dstFile,
const QString &fileNameToRemove,
const QUuid &srcUuid,
int in)
: FilePropertiesPostJobAction(srcFile, dstFile)
, m_fileNameToRemove(fileNameToRemove)
, m_uuid(srcUuid)
, m_in(in)
{}
void doAction();
private:
QString m_fileNameToRemove;
QUuid m_uuid;
int m_in;
};
class ReplaceAllPostJobAction : public FilePropertiesPostJobAction
{
public:
ReplaceAllPostJobAction(const QString &srcFile, const QString &dstFile, const QString &srcHash)
: FilePropertiesPostJobAction(srcFile, dstFile)
, m_hash(srcHash)
{}
void doAction();
private:
QString m_hash;
};
class ProxyReplacePostJobAction : public FilePropertiesPostJobAction
{
public:
ProxyReplacePostJobAction(const QString &srcFile, const QString &dstFile, const QString &srcHash)
: FilePropertiesPostJobAction(srcFile, dstFile)
, m_srcFile(srcFile)
, m_dstFile(dstFile)
, m_hash(srcHash)
{}
void doAction();
private:
QString m_srcFile;
QString m_dstFile;
QString m_hash;
};
class ProxyFinalizePostJobAction : public FilePropertiesPostJobAction
{
public:
ProxyFinalizePostJobAction(const QString &srcFile, const QString &dstFile)
: FilePropertiesPostJobAction(srcFile, dstFile)
, m_dstFile(dstFile)
{}
void doAction();
private:
QString m_dstFile;
};
class SubtitlesDock;
class ImportSrtPostJobAction : public PostJobAction
{
public:
ImportSrtPostJobAction(const QString &srtFile,
const QString &trackName,
const QString &lang,
bool includeNonspoken,
SubtitlesDock *dock)
: m_srtFile(srtFile)
, m_trackName(trackName)
, m_lang(lang)
, m_includeNonspoken(includeNonspoken)
, m_dock(dock)
{}
virtual ~ImportSrtPostJobAction() {}
void doAction();
protected:
const QString m_srtFile;
const QString m_trackName;
const QString m_lang;
const bool m_includeNonspoken;
SubtitlesDock *m_dock;
};
#endif // POSTJOBACTION_H

69
src/jobs/qimagejob.cpp Normal file
View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020-2022 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 "qimagejob.h"
#include "util.h"
#include <QImage>
#include <QImageReader>
#include <QRunnable>
#include <QtConcurrent/QtConcurrent>
QImageJob::QImageJob(const QString &destFilePath, const QString &srcFilePath, const int height)
: AbstractJob(srcFilePath)
, m_srcFilePath(srcFilePath)
, m_destFilePath(destFilePath)
, m_height(height)
{
setTarget(destFilePath);
setLabel(tr("Make proxy for %1").arg(Util::baseName(srcFilePath)));
}
QImageJob::~QImageJob()
{
if (m_destFilePath.contains("proxies") && m_destFilePath.contains(".pending.")) {
QFile::remove(m_destFilePath);
}
}
void QImageJob::start()
{
AbstractJob::start();
auto result = QtConcurrent::run([=]() {
appendToLog(QStringLiteral("Reading source image \"%1\"\n").arg(m_srcFilePath));
QImageReader reader;
reader.setAutoTransform(true);
reader.setDecideFormatFromContent(true);
reader.setFileName(m_srcFilePath);
QImage image(reader.read());
if (!image.isNull()) {
image = image.scaledToHeight(m_height, Qt::SmoothTransformation);
if (image.save(m_destFilePath)) {
appendToLog(
QStringLiteral("Successfully saved image as \"%1\"\n").arg(m_destFilePath));
QMetaObject::invokeMethod(this, "onFinished", Qt::QueuedConnection, Q_ARG(int, 0));
} else {
appendToLog(QStringLiteral("Failed to save image as \"%1\"\n").arg(m_destFilePath));
QMetaObject::invokeMethod(this, "onFinished", Qt::QueuedConnection, Q_ARG(int, 1));
}
} else {
appendToLog(QStringLiteral("Failed to read source image \"%1\"\n").arg(m_srcFilePath));
QMetaObject::invokeMethod(this, "onFinished", Qt::QueuedConnection, Q_ARG(int, 1));
}
});
}

40
src/jobs/qimagejob.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 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/>.
*/
#ifndef QIMAGEJOB_H
#define QIMAGEJOB_H
#include "abstractjob.h"
#include <QSize>
class QImageJob : public AbstractJob
{
Q_OBJECT
public:
QImageJob(const QString &destFilePath, const QString &srcFilePath, const int height);
virtual ~QImageJob();
void start();
void execute();
private:
QString m_srcFilePath;
QString m_destFilePath;
int m_height;
};
#endif // QIMAGEJOB_H

View File

@@ -0,0 +1,520 @@
/*
* Copyright (c) 2025 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 "screencapturejob.h"
#include "Logger.h"
#include "ffmpegjob.h"
#include "jobqueue.h"
#include "mainwindow.h"
#include "postjobaction.h"
#include "screencapture/screencapture.h"
#include "settings.h"
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTimer>
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#endif
ScreenCaptureJob::ScreenCaptureJob(const QString &name,
const QString &filename,
const QRect &captureRect,
bool recordAudio)
: AbstractJob(name, QThread::NormalPriority)
, m_filename(filename)
, m_rect(captureRect)
, m_isAutoOpen(true)
, m_recordAudio(recordAudio)
{
QAction *action = new QAction(tr("Open"), this);
action->setToolTip(tr("Open the capture"));
action->setData("Open");
connect(action, SIGNAL(triggered(bool)), this, SLOT(onOpenTriggered()));
m_successActions << action;
m_standardActions.clear();
}
ScreenCaptureJob::~ScreenCaptureJob() {}
void ScreenCaptureJob::start()
{
LOG_DEBUG() << "starting screen capture job";
// Create and start progress timer
connect(&m_progressTimer, &QTimer::timeout, this, [=]() {
auto secs = time().elapsed() / 1000;
emit progressUpdated(m_item, -secs);
});
m_progressTimer.start(1000); // Update every second
QStringList args;
#ifdef Q_OS_MAC
args << "-C";
args << "-g";
args << "-k";
args << "-v";
args << m_filename;
LOG_DEBUG() << "screencapture " + args.join(' ');
AbstractJob::start("screencapture", args);
#else
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
// On Linux, check if we're on Wayland and try D-Bus first
if (ScreenCapture::isWayland()) {
if (startWaylandRecording()) {
AbstractJob::start("sleep", {"infinity"});
return;
}
LOG_WARNING() << "Wayland D-Bus recording failed, falling back to ffmpeg (may not work)";
}
#endif
QString vcodec("libx264");
args << "-f"
#ifdef Q_OS_WIN
<< "gdigrab";
args << "-offset_x" << QString::number(m_rect.x());
args << "-offset_y" << QString::number(m_rect.y());
args << "-framerate" << QString::number(MLT.profile().fps());
args << "-video_size" << QString("%1x%2").arg(m_rect.width()).arg(m_rect.height());
args << "-i"
<< "desktop";
if (m_recordAudio) {
args << "-f"
<< "dshow";
args << "-i"
<< "audio=" + Settings.audioInput();
}
if (Settings.encodeUseHardware() && !Settings.encodeHardware().isEmpty()) {
vcodec = "h264_mf";
args << "-rate_control"
<< "quality";
args << "-q:v"
<< "90";
args << "-pix_fmt"
<< "nv12";
args << "-hw_encoding"
<< "true";
#else
<< "x11grab";
args << "-grab_x" << QString::number(m_rect.x());
args << "-grab_y" << QString::number(m_rect.y());
args << "-framerate" << QString::number(MLT.profile().fps());
args << "-video_size" << QString("%1x%2").arg(m_rect.width()).arg(m_rect.height());
args << "-i"
<< ":0.0";
if (m_recordAudio) {
args << "-f"
<< "pulse";
args << "-i" << Settings.audioInput();
}
if (Settings.encodeUseHardware() && !Settings.encodeHardware().isEmpty()) {
if (Settings.encodeHardware().contains("h264_nvenc")) {
vcodec = "h264_nvenc";
args << "-preset"
<< "p4";
args << "-tune"
<< "hq";
args << "-rc"
<< "vbr";
args << "-cq"
<< "19";
args << "-b:v"
<< "0";
} else if (Settings.encodeHardware().contains("h264_vaapi")) {
vcodec = "h264_vaapi";
args << "-qp"
<< "18";
args << "-init_hw_device"
<< "vaapi=vaapi0:";
args << "-filter_hw_device"
<< "vaapi0";
args << "-vf"
<< "format=nv12,hwupload";
args << "-quality"
<< "1";
args << "-rc_mode"
<< "CQP";
}
#endif
} else {
args << "-crf"
<< "18";
args << "-preset"
<< "veryfast";
args << "-tune"
<< "film";
args << "-pix_fmt"
<< "yuv420p";
}
args << "-codec:v" << vcodec;
if (vcodec == "h264_mf") {
args << "-profile:v"
<< "100";
} else {
args << "-profile:v"
<< "high";
}
args << "-bf"
<< "2";
args << "-g" << QString::number(qRound(2.0 * MLT.profile().fps()));
args << "-color_range"
<< "jpeg";
args << "-color_primaries"
<< "bt709";
args << "-color_trc"
<< "bt709";
args << "-colorspace"
<< "bt709";
args << "-y" << m_filename;
QString shotcutPath = qApp->applicationDirPath();
QFileInfo ffmpegPath(shotcutPath, "ffmpeg");
setReadChannel(QProcess::StandardError);
LOG_DEBUG() << ffmpegPath.absoluteFilePath() + " " + args.join(' ');
AbstractJob::start(ffmpegPath.absoluteFilePath(), args);
#endif
}
void ScreenCaptureJob::stop()
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
if (m_dbusService == DBusService::GNOME) {
// Stop GNOME screencast
auto bus = QDBusConnection::sessionBus();
QDBusMessage msg = QDBusMessage::createMethodCall("org.gnome.Shell.Screencast",
"/org/gnome/Shell/Screencast",
"org.gnome.Shell.Screencast",
"StopScreencast");
QDBusPendingCall async = bus.asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this);
connect(watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<bool> reply = *w;
LOG_DEBUG() << "GNOME Screencast stopped";
// emit finished(this, true);
w->deleteLater();
});
} else if (m_dbusService == DBusService::KDE) {
// For KDE Spectacle, user stops recording via the Spectacle UI
// We just wait for the signals
LOG_DEBUG() << "Waiting for KDE Spectacle to finish recording (user controlled)";
}
if (m_dbusService != DBusService::None) {
AbstractJob::stop();
setKilled(false);
return;
}
#endif
// Try to terminate gracefully
write("q");
QTimer::singleShot(1000, this, [this]() {
if (m_progressTimer.isActive())
AbstractJob::stop();
});
}
void ScreenCaptureJob::onOpenTriggered()
{
MAIN.open(m_filename);
}
void ScreenCaptureJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
m_progressTimer.stop();
LOG_DEBUG() << "screen capture job finished with exit code" << exitCode;
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
if (m_dbusService == DBusService::GNOME) {
LOG_INFO() << "job succeeeded";
appendToLog(QStringLiteral("Completed successfully in %1\n")
.arg(QTime::fromMSecsSinceStartOfDay(time().elapsed()).toString()));
emit progressUpdated(m_item, 100);
emit finished(this, true);
if (!m_actualFilename.endsWith(".mp4")) {
// Remux the file from Matroska to chosen WebM
QFileInfo fileInfo(m_filename);
QString inputFileName = m_actualFilename.isEmpty()
? fileInfo.path() + "/" + fileInfo.completeBaseName()
+ ".mkv"
: m_actualFilename;
if (m_isAutoOpen && QFileInfo::exists(inputFileName)) {
m_isAutoOpen = false;
// Create FFmpeg remux job
QStringList args;
args << "-i" << inputFileName;
args << "-c"
<< "copy";
args << "-y" << m_filename;
FfmpegJob *remuxJob = new FfmpegJob(m_filename, args, false);
remuxJob->setLabel(tr("Remux %1").arg(fileInfo.fileName()));
remuxJob->setPostJobAction(
new OpenPostJobAction(inputFileName, m_filename, inputFileName));
JOBS.add(remuxJob);
return;
}
}
if (!m_actualFilename.isEmpty()) {
m_filename = m_actualFilename;
}
}
if (m_dbusService != DBusService::None) {
exitCode = 0; // ignore exit code from sleep
exitStatus = QProcess::NormalExit;
}
#endif
AbstractJob::onFinished(exitCode, exitStatus);
if (m_isAutoOpen && exitCode == 0 && QFileInfo::exists(m_filename)) {
// Automatically open the captured file
m_isAutoOpen = false;
QTimer::singleShot(0, this, [this]() { MAIN.open(m_filename); });
}
}
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
bool ScreenCaptureJob::startWaylandRecording()
{
// Check desktop environment
QString desktop = qEnvironmentVariable("XDG_CURRENT_DESKTOP").toLower();
LOG_DEBUG() << "XDG_CURRENT_DESKTOP:" << desktop;
// Try GNOME first
if (desktop.contains("gnome")) {
if (startGnomeScreencast()) {
return true;
}
}
// Try KDE
if (desktop.contains("kde") || desktop.contains("plasma")) {
if (startKdeSpectacle()) {
return true;
}
}
// If we reach here, neither GNOME nor KDE worked
// This shouldn't happen as MainWindow should have launched OBS Studio
LOG_ERROR() << "Desktop environment not GNOME or KDE, and fallback not available in job";
return false;
}
bool ScreenCaptureJob::startGnomeScreencast()
{
LOG_DEBUG() << "Attempting GNOME Shell Screencast";
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
LOG_WARNING() << "DBus session bus not connected";
return false;
}
// Check if the service is available
QDBusMessage checkMsg = QDBusMessage::createMethodCall("org.gnome.Shell.Screencast",
"/org/gnome/Shell/Screencast",
"org.freedesktop.DBus.Introspectable",
"Introspect");
QDBusPendingCall async = bus.asyncCall(checkMsg, 1000);
async.waitForFinished();
QDBusPendingReply<QString> reply = async;
if (reply.isError()) {
LOG_WARNING() << "GNOME Shell Screencast service not available:" << reply.error().message();
return false;
}
// Prepare options
QVariantMap options;
options.insert("framerate", static_cast<int>(MLT.profile().fps()));
options.insert("draw-cursor", true);
// Call ScreencastArea
LOG_DEBUG() << "Recording region:" << m_rect;
auto msg = QDBusMessage::createMethodCall("org.gnome.Shell.Screencast",
"/org/gnome/Shell/Screencast",
"org.gnome.Shell.Screencast",
"ScreencastArea");
msg << m_rect.x();
msg << m_rect.y();
msg << m_rect.width();
msg << m_rect.height();
QFileInfo fileInfo(m_filename);
msg << fileInfo.path() + "/" + fileInfo.completeBaseName() + ".mkv";
msg << options;
QDBusPendingCall screencastCall = bus.asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(screencastCall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<bool, QString> reply = *w;
if (reply.isError()) {
LOG_ERROR() << "GNOME Screencast call failed:" << reply.error().message();
AbstractJob::stop();
} else {
bool success = reply.argumentAt<0>();
QString filename = reply.argumentAt<1>();
LOG_DEBUG() << "GNOME Screencast started, success:" << success
<< "filename:" << filename;
m_actualFilename = filename;
}
w->deleteLater();
});
m_dbusService = DBusService::GNOME;
LOG_INFO() << "Started GNOME Shell Screencast to:"
<< fileInfo.path() + "/" + fileInfo.completeBaseName() + ".mkv";
// Job will be controlled via stop() method
return true;
}
bool ScreenCaptureJob::startKdeSpectacle()
{
LOG_DEBUG() << "Attempting KDE Spectacle recording";
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
LOG_WARNING() << "DBus session bus not connected";
return false;
}
// Connect to RecordingTaken and RecordingFailed signals
bool connected = bus.connect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingTaken",
this,
SLOT(onDBusRecordingTaken(QString)));
if (!connected) {
LOG_WARNING() << "Failed to connect to RecordingTaken signal";
return false;
}
connected = bus.connect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingFailed",
this,
SLOT(onDBusRecordingFailed()));
if (!connected) {
LOG_WARNING() << "Failed to connect to RecordingFailed signal";
bus.disconnect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingTaken",
this,
SLOT(onDBusRecordingTaken(QString)));
return false;
}
// Call RecordRegion for recording
LOG_DEBUG() << "Recording region:" << m_rect;
auto msg = QDBusMessage::createMethodCall("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
m_rect.isNull() ? "RecordScreen" : "RecordRegion");
msg << 1; // includeMousePointer
QDBusPendingCall async = bus.asyncCall(msg);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<> reply = *w;
if (reply.isError()) {
LOG_ERROR() << "KDE Spectacle recording call failed:" << reply.error().message();
if (m_rect.isNull()) {
AbstractJob::stop();
} else {
m_rect = QRect();
w->deleteLater();
startKdeSpectacle();
}
} else {
LOG_DEBUG() << "KDE Spectacle recording initiated";
}
w->deleteLater();
});
m_dbusService = DBusService::KDE;
LOG_INFO() << "Started KDE Spectacle screen recording";
return true;
}
void ScreenCaptureJob::onDBusRecordingTaken(const QString &fileName)
{
LOG_DEBUG() << "Recording taken, file:" << fileName;
// Move/copy the file to our desired location if different
if (!fileName.isEmpty() && fileName != m_filename) {
QFile source(fileName);
if (QFile::exists(m_filename)) {
QFile::remove(m_filename);
}
if (source.copy(m_filename)) {
LOG_INFO() << "Copied recording from" << fileName << "to" << m_filename;
source.remove(); // Remove the original
} else {
LOG_WARNING() << "Failed to copy recording, using original location";
m_filename = fileName;
}
}
auto bus = QDBusConnection::sessionBus();
bus.disconnect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingTaken",
this,
SLOT(onDBusRecordingTaken(QString)));
bus.disconnect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingFailed",
this,
SLOT(onDBusRecordingFailed()));
stop();
}
void ScreenCaptureJob::onDBusRecordingFailed()
{
LOG_ERROR() << "Recording failed";
auto bus = QDBusConnection::sessionBus();
bus.disconnect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingTaken",
this,
SLOT(onDBusRecordingTaken(QString)));
bus.disconnect("org.kde.Spectacle",
"/",
"org.kde.Spectacle",
"RecordingFailed",
this,
SLOT(onDBusRecordingFailed()));
AbstractJob::stop();
}
#endif

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 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/>.
*/
#ifndef SCREENCAPTUREJOB_H
#define SCREENCAPTUREJOB_H
#include "abstractjob.h"
#include <QRect>
#include <QTimer>
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
#include <QDBusConnection>
#endif
class ScreenCaptureJob : public AbstractJob
{
Q_OBJECT
public:
ScreenCaptureJob(const QString &name,
const QString &filename,
const QRect &captureRect,
bool recordAudio = true);
virtual ~ScreenCaptureJob();
void start() override;
void stop() override;
private slots:
void onOpenTriggered();
void onFinished(int exitCode, QProcess::ExitStatus exitStatus) override;
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
void onDBusRecordingTaken(const QString &fileName);
void onDBusRecordingFailed();
#endif
private:
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
enum DBusService { None, GNOME, KDE };
bool startWaylandRecording();
bool startGnomeScreencast();
bool startKdeSpectacle();
#endif
QString m_filename;
QString m_actualFilename;
QRect m_rect;
bool m_isAutoOpen;
bool m_recordAudio;
QTimer m_progressTimer;
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
DBusService m_dbusService = DBusService::None;
#endif
};
#endif // SCREENCAPTUREJOB_H

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2012-2024 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 "videoqualityjob.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include <QAction>
#include <QDesktopServices>
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <QUrl>
VideoQualityJob::VideoQualityJob(const QString &name,
const QString &xml,
const QString &reportPath,
int frameRateNum,
int frameRateDen)
: MeltJob(name, xml, frameRateNum, frameRateDen)
, m_reportPath(reportPath)
{
QAction *action = new QAction(tr("Open"), this);
action->setData("Open");
action->setToolTip(tr("Open original and encoded side-by-side in the Shotcut player"));
connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered()));
m_successActions << action;
action = new QAction(tr("View Report"), this);
connect(action, SIGNAL(triggered()), this, SLOT(onViewReportTriggered()));
m_successActions << action;
action = new QAction(tr("Show In Files"), this);
connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered()));
m_successActions << action;
action = new QAction(tr("Show In Folder"), this);
connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered()));
m_successActions << action;
setLabel(tr("Measure %1").arg(objectName()));
setStandardOutputFile(reportPath);
}
void VideoQualityJob::onOpenTiggered()
{
// Parse the XML.
QFile file(xmlPath());
file.open(QIODevice::ReadOnly);
QDomDocument dom(xmlPath());
dom.setContent(&file);
file.close();
// Locate the VQM transition.
QDomNodeList transitions = dom.elementsByTagName("transition");
for (int i = 0; i < transitions.length(); i++) {
QDomElement property = transitions.at(i).firstChildElement("property");
while (!property.isNull()) {
// Change the render property to 1.
if (property.attribute("name") == "render") {
property.firstChild().setNodeValue("1");
// Save the new XML.
file.open(QIODevice::WriteOnly);
QTextStream textStream(&file);
dom.save(textStream, 2);
file.close();
MAIN.open(xmlPath().toUtf8().constData());
break;
}
property = property.nextSiblingElement("property");
}
}
}
void VideoQualityJob::onViewReportTriggered()
{
TextViewerDialog dialog(&MAIN);
dialog.setWindowTitle(tr("Video Quality Measurement"));
QFile f(m_reportPath);
f.open(QIODevice::ReadOnly);
QString s(f.readAll());
f.close();
dialog.setText(s);
dialog.exec();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2012-2018 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/>.
*/
#ifndef VIDEOQUALITYJOB_H
#define VIDEOQUALITYJOB_H
#include "meltjob.h"
class VideoQualityJob : public MeltJob
{
Q_OBJECT
public:
VideoQualityJob(const QString &name,
const QString &xml,
const QString &reportPath,
int frameRateNum,
int frameRateDen);
private slots:
void onOpenTiggered();
void onViewReportTriggered();
private:
QString m_reportPath;
};
#endif // VIDEOQUALITYJOB_H

118
src/jobs/whisperjob.cpp Normal file
View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2024 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 "whisperjob.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "mainwindow.h"
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QThread>
#include <QTimer>
WhisperJob::WhisperJob(const QString &name,
const QString &iWavFile,
const QString &oSrtFile,
const QString &lang,
bool translate,
int maxLength,
QThread::Priority priority)
: AbstractJob(name, priority)
, m_iWavFile(iWavFile)
, m_oSrtFile(oSrtFile)
, m_lang(lang)
, m_translate(translate)
, m_maxLength(maxLength)
, m_previousPercent(0)
{
setTarget(oSrtFile);
}
WhisperJob::~WhisperJob()
{
LOG_DEBUG() << "begin";
}
void WhisperJob::start()
{
QString whisperPath = Settings.whisperExe();
auto modelPath = Settings.whisperModel();
setReadChannel(QProcess::StandardOutput);
setProcessChannelMode(QProcess::MergedChannels);
QString of = m_oSrtFile;
of.remove(".srt");
QStringList args;
args << "-f" << m_iWavFile;
args << "-m" << modelPath;
args << "-l" << m_lang;
if (m_translate) {
args << "-tr";
}
args << "-of" << of;
args << "-osrt";
args << "-pp";
args << "-ml" << QString::number(m_maxLength);
args << "-sow";
#if QT_POINTER_SIZE == 4
// Limit to 1 rendering thread on 32-bit process to reduce memory usage.
auto threadCount = 1;
#else
auto threadCount = qMax(1, QThread::idealThreadCount() - 1);
#endif
args << "-t" << QString::number(threadCount);
LOG_DEBUG() << whisperPath + " " + args.join(' ');
AbstractJob::start(whisperPath, args);
emit progressUpdated(m_item, 0);
}
void WhisperJob::onViewSrtTriggered()
{
QFile srtFile(m_oSrtFile);
QString text = srtFile.readAll();
TextViewerDialog dialog(&MAIN, true);
dialog.setWindowTitle(tr("SRT"));
dialog.setText(text);
dialog.exec();
}
void WhisperJob::onReadyRead()
{
QString msg;
do {
msg = readLine();
if (!msg.isEmpty()) {
int index = msg.indexOf("progress = ");
if (index > -1) {
QString num = msg.mid(index + 11).remove("%").trimmed();
int percent = num.toInt();
if (percent != m_previousPercent) {
emit progressUpdated(m_item, percent);
QCoreApplication::processEvents();
m_previousPercent = percent;
}
}
appendToLog(msg);
}
} while (!msg.isEmpty());
}

54
src/jobs/whisperjob.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 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/>.
*/
#ifndef WHISPERJOB_H
#define WHISPERJOB_H
#include "abstractjob.h"
#include <QTemporaryFile>
class WhisperJob : public AbstractJob
{
Q_OBJECT
public:
WhisperJob(const QString &name,
const QString &iWavFile,
const QString &oSrtFile,
const QString &lang,
bool translate,
int maxLength,
QThread::Priority priority = Settings.jobPriority());
virtual ~WhisperJob();
public slots:
void start();
void onViewSrtTriggered();
protected slots:
void onReadyRead();
private:
const QString m_iWavFile;
const QString m_oSrtFile;
const QString m_lang;
const bool m_translate;
const int m_maxLength;
int m_previousPercent;
};
#endif // WHISPERJOB_H