übernahme Code Shortcut
This commit is contained in:
266
src/jobs/abstractjob.cpp
Normal file
266
src/jobs/abstractjob.cpp
Normal 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
100
src/jobs/abstractjob.h
Normal 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
|
||||
66
src/jobs/bitrateviewerjob.cpp
Normal file
66
src/jobs/bitrateviewerjob.cpp
Normal 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();
|
||||
}
|
||||
42
src/jobs/bitrateviewerjob.h
Normal file
42
src/jobs/bitrateviewerjob.h
Normal 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
|
||||
78
src/jobs/dockerpulljob.cpp
Normal file
78
src/jobs/dockerpulljob.cpp
Normal 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
43
src/jobs/dockerpulljob.h
Normal 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
319
src/jobs/encodejob.cpp
Normal 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
44
src/jobs/encodejob.h
Normal 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
121
src/jobs/ffmpegjob.cpp
Normal 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
50
src/jobs/ffmpegjob.h
Normal 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
57
src/jobs/ffprobejob.cpp
Normal 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
40
src/jobs/ffprobejob.h
Normal 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
44
src/jobs/gopro2gpxjob.cpp
Normal 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
37
src/jobs/gopro2gpxjob.h
Normal 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
|
||||
215
src/jobs/htmlgeneratorjob.cpp
Normal file
215
src/jobs/htmlgeneratorjob.cpp
Normal 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);
|
||||
}
|
||||
58
src/jobs/htmlgeneratorjob.h
Normal file
58
src/jobs/htmlgeneratorjob.h
Normal 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
227
src/jobs/kokorodokijob.cpp
Normal 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+2800–U+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
55
src/jobs/kokorodokijob.h
Normal 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
239
src/jobs/meltjob.cpp
Normal 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
73
src/jobs/meltjob.h
Normal 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
126
src/jobs/postjobaction.cpp
Normal 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
152
src/jobs/postjobaction.h
Normal 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
69
src/jobs/qimagejob.cpp
Normal 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
40
src/jobs/qimagejob.h
Normal 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
|
||||
520
src/jobs/screencapturejob.cpp
Normal file
520
src/jobs/screencapturejob.cpp
Normal 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
|
||||
67
src/jobs/screencapturejob.h
Normal file
67
src/jobs/screencapturejob.h
Normal 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
|
||||
103
src/jobs/videoqualityjob.cpp
Normal file
103
src/jobs/videoqualityjob.cpp
Normal 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();
|
||||
}
|
||||
41
src/jobs/videoqualityjob.h
Normal file
41
src/jobs/videoqualityjob.h
Normal 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
118
src/jobs/whisperjob.cpp
Normal 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
54
src/jobs/whisperjob.h
Normal 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
|
||||
Reference in New Issue
Block a user