File indexing completed on 2024-12-01 04:28:36

0001 /*
0002     SPDX-FileCopyrightText: 2011 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "customjobtask.h"
0008 #include "bin/bin.h"
0009 #include "bin/clipcreator.hpp"
0010 #include "bin/projectclip.h"
0011 #include "bin/projectfolder.h"
0012 #include "bin/projectitemmodel.h"
0013 #include "core.h"
0014 #include "dialogs/clipjobmanager.h"
0015 #include "doc/kdenlivedoc.h"
0016 #include "kdenlive_debug.h"
0017 #include "kdenlivesettings.h"
0018 #include "macros.hpp"
0019 #include "mainwindow.h"
0020 #include "monitor/monitor.h"
0021 #include "ui_customjobinterface_ui.h"
0022 
0023 #include <QProcess>
0024 #include <QTemporaryFile>
0025 #include <QThread>
0026 
0027 #include <KLocalizedString>
0028 
0029 static QStringList requestedOutput;
0030 
0031 CustomJobTask::CustomJobTask(const ObjectId &owner, const QString &jobName, const QMap<QString, QString> &jobParams, int in, int out, const QString &jobId,
0032                              QObject *object)
0033     : AbstractTask(owner, AbstractTask::TRANSCODEJOB, object)
0034     , m_jobDuration(0)
0035     , m_isFfmpegJob(true)
0036     , m_parameters(jobParams)
0037     , m_inPoint(in)
0038     , m_outPoint(out)
0039     , m_jobId(jobId)
0040     , m_jobProcess(nullptr)
0041 {
0042     m_description = jobName;
0043     m_tmpFrameFile.setFileTemplate(QString("%1/kdenlive-frame-.XXXXXX.png").arg(QDir::tempPath()));
0044 }
0045 
0046 void CustomJobTask::start(QObject *object, const QString &jobId)
0047 {
0048     std::vector<QString> binIds = pCore->bin()->selectedClipsIds(true);
0049     QMap<QString, QString> jobData = ClipJobManager::getJobParameters(jobId);
0050     if (jobData.size() < 4) {
0051         qDebug() << ":::: INVALID JOB DATA FOR: " << jobId << "\n____________________";
0052         return;
0053     }
0054     const QString jobName = jobData.value("description");
0055     QString param1Value;
0056     QString param2Value;
0057     const QString paramArgs = jobData.value("parameters");
0058     bool requestParam1 = paramArgs.contains(QLatin1String("{param1}"));
0059     bool requestParam2 = paramArgs.contains(QLatin1String("{param2}"));
0060     if (requestParam1 || requestParam2) {
0061         const QString param1 = jobData.value(QLatin1String("param1type"));
0062         const QString param2 = jobData.value(QLatin1String("param2type"));
0063         // We need to request user data for the job parameters
0064         QScopedPointer<QDialog> dia(new QDialog(QApplication::activeWindow()));
0065         Ui::CustomJobInterface_UI dia_ui;
0066         dia_ui.setupUi(dia.data());
0067         const QString jobDetails = jobData.value(QLatin1String("details"));
0068         if (!jobDetails.isEmpty()) {
0069             dia_ui.taskDescription->setPlainText(jobDetails);
0070         } else {
0071             dia_ui.taskDescription->setVisible(false);
0072         }
0073         dia->setWindowTitle(i18n("%1 parameters", jobName));
0074         if (requestParam1 && param1 != QLatin1String("frame")) {
0075             dia_ui.param1Label->setText(jobData.value(QLatin1String("param1name")));
0076             if (param1 == QLatin1String("file")) {
0077                 dia_ui.param1List->setVisible(false);
0078             } else {
0079                 dia_ui.param1Url->setVisible(false);
0080                 QStringList listValues = jobData.value(QLatin1String("param1list")).split(QLatin1String("  "));
0081                 dia_ui.param1List->addItems(listValues);
0082             }
0083         } else {
0084             // Hide param1
0085             dia_ui.param1Label->setVisible(false);
0086             dia_ui.param1Url->setVisible(false);
0087             dia_ui.param1List->setVisible(false);
0088         }
0089         if (requestParam2) {
0090             dia_ui.param2Label->setText(jobData.value(QLatin1String("param2name")));
0091             if (param2 == QLatin1String("file")) {
0092                 dia_ui.param2List->setVisible(false);
0093             } else {
0094                 dia_ui.param2Url->setVisible(false);
0095                 QStringList listValues = jobData.value(QLatin1String("param2list")).split(QLatin1String("  "));
0096                 dia_ui.param2List->addItems(listValues);
0097             }
0098         } else {
0099             // Hide param1
0100             dia_ui.param2Label->setVisible(false);
0101             dia_ui.param2Url->setVisible(false);
0102             dia_ui.param2List->setVisible(false);
0103         }
0104         if (dia->exec() != QDialog::Accepted) {
0105             // Abort
0106             return;
0107         }
0108         if (requestParam1) {
0109             if (param1 == QLatin1String("file")) {
0110                 param1Value = dia_ui.param1Url->url().toLocalFile();
0111             }
0112             if (param1 == QLatin1String("frame")) {
0113                 param1Value = QStringLiteral("%tmpfile");
0114             } else {
0115                 param1Value = dia_ui.param1List->currentText();
0116             }
0117             jobData.insert(QLatin1String("param1value"), param1Value);
0118         }
0119         if (requestParam2) {
0120             if (param2 == QLatin1String("file")) {
0121                 param2Value = dia_ui.param2Url->url().toLocalFile();
0122             } else {
0123                 param2Value = dia_ui.param2List->currentText();
0124             }
0125             jobData.insert(QLatin1String("param2value"), param2Value);
0126         }
0127     }
0128     for (auto &id : binIds) {
0129         CustomJobTask *task = nullptr;
0130         ObjectId owner;
0131         int in = -1;
0132         int out = -1;
0133         if (id.contains(QLatin1Char('/'))) {
0134             QStringList binData = id.split(QLatin1Char('/'));
0135             if (binData.size() < 3) {
0136                 // Invalid subclip data
0137                 qDebug() << "=== INVALID SUBCLIP DATA: " << id;
0138                 continue;
0139             }
0140             owner = ObjectId(KdenliveObjectType::BinClip, binData.first().toInt(), QUuid());
0141             in = binData.at(1).toInt();
0142             out = binData.at(2).toInt();
0143         } else {
0144             // Process full clip
0145             owner = ObjectId(KdenliveObjectType::BinClip, id.toInt(), QUuid());
0146         }
0147         task = new CustomJobTask(owner, jobName, jobData, in, out, jobId, object);
0148         if (task) {
0149             // Otherwise, start a filter thread.
0150             pCore->taskManager.startTask(owner.itemId, task);
0151         }
0152     }
0153 }
0154 
0155 void CustomJobTask::run()
0156 {
0157     AbstractTaskDone whenFinished(m_owner.itemId, this);
0158     if (m_isCanceled || pCore->taskManager.isBlocked()) {
0159         return;
0160     }
0161     QMutexLocker lock(&m_runMutex);
0162     m_running = true;
0163     auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId));
0164     if (!binClip) {
0165         return;
0166     }
0167     QString source = binClip->url();
0168     QString folderId = binClip->parent()->clipId();
0169     const QString binary = m_parameters.value(QLatin1String("binary"));
0170     if (!QFile::exists(binary)) {
0171         QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection,
0172                                   Q_ARG(QString, i18n("Application %1 not found, please update the job settings", binary)),
0173                                   Q_ARG(int, int(KMessageWidget::Warning)));
0174         return;
0175     }
0176     m_isFfmpegJob = QFileInfo(binary).fileName().contains(QLatin1String("ffmpeg"));
0177     QString jobParameters = m_parameters.value(QLatin1String("parameters"));
0178     QStringList parameters;
0179     m_jobDuration = int(binClip->duration().seconds());
0180     QFileInfo sourceInfo(source);
0181 
0182     // Tell ffmpeg to overwrite, we do the file exist check ourselves
0183     if (m_isFfmpegJob) {
0184         parameters << QStringLiteral("-y");
0185         if (m_inPoint > -1) {
0186             parameters << QStringLiteral("-ss") << QString::number(GenTime(m_inPoint, pCore->getCurrentFps()).seconds());
0187         }
0188         parameters << QStringLiteral("-stats");
0189     }
0190 
0191     // Get output file name
0192     QString extension = m_parameters.value(QLatin1String("output"));
0193     if (extension.isEmpty()) {
0194         extension = sourceInfo.suffix();
0195     }
0196     if (!extension.startsWith(QLatin1Char('.'))) {
0197         extension.prepend(QLatin1Char('.'));
0198     }
0199     const QString destName = sourceInfo.baseName();
0200     QDir baseDir = sourceInfo.absoluteDir();
0201     QString destPath = baseDir.absoluteFilePath(destName + extension);
0202     if (QFileInfo::exists(destPath) || requestedOutput.contains(destPath)) {
0203         QString fixedName = destName;
0204         static const QRegularExpression regex(QRegularExpression::anchoredPattern(QStringLiteral(R"(.*-(\d{4})$)")));
0205         QRegularExpressionMatch match = regex.match(fixedName);
0206         if (!match.hasMatch()) {
0207             // if the file has no index suffix, append -0001
0208             fixedName.append(QString::asprintf("-%04d", 1));
0209         } else {
0210             const int currentSuffix = match.captured(1).toInt();
0211             fixedName.replace(match.capturedStart(1), match.capturedLength(1), QString::asprintf("-%04d", currentSuffix + 1));
0212         }
0213         destPath = baseDir.absoluteFilePath(fixedName + extension);
0214         while (QFileInfo::exists(destPath) || requestedOutput.contains(destPath)) {
0215             // if the file name has already an index suffix,
0216             // increase the number
0217             match = regex.match(fixedName);
0218             const int currentSuffix = match.captured(1).toInt();
0219             fixedName.replace(match.capturedStart(1), match.capturedLength(1), QString::asprintf("-%04d", currentSuffix + 1));
0220             destPath = baseDir.absoluteFilePath(fixedName + extension);
0221         }
0222     }
0223     // Extract frame is necessary
0224     requestedOutput << destPath;
0225     parameters << jobParameters.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0226 
0227     bool outputPlaced = false;
0228     for (auto &p : parameters) {
0229         // Replace
0230         if (p.contains(QStringLiteral("{output}"))) {
0231             p.replace(QStringLiteral("{output}"), destPath);
0232             outputPlaced = true;
0233         }
0234         if (p.contains(QStringLiteral("{source}"))) {
0235             p.replace(QStringLiteral("{source}"), source);
0236         }
0237         if (p.contains(QStringLiteral("{param1}"))) {
0238             QString param1Value = m_parameters.value(QLatin1String("param1value"));
0239             if (param1Value == QStringLiteral("%tmpfile")) {
0240                 if (m_tmpFrameFile.open()) {
0241                     param1Value = m_tmpFrameFile.fileName();
0242                     m_tmpFrameFile.close();
0243                     // Extract frame to file
0244                     qDebug() << "=====================\nEXTRACTING FRAME TO: " << param1Value << "\n\n==============================";
0245                     pCore->getMonitor(Kdenlive::ClipMonitor)->extractFrame(param1Value);
0246                 }
0247             }
0248             p.replace(QStringLiteral("{param1}"), param1Value);
0249         }
0250         if (p.contains(QStringLiteral("{param2}"))) {
0251             p.replace(QStringLiteral("{param2}"), m_parameters.value(QLatin1String("param2value")));
0252         }
0253     }
0254     if (!outputPlaced) {
0255         parameters << destPath;
0256     }
0257 
0258     if (m_isFfmpegJob && m_outPoint > -1) {
0259         int inputIndex = parameters.indexOf(source);
0260         if (inputIndex > -1) {
0261             parameters.insert(inputIndex + 1, QStringLiteral("-to"));
0262             parameters.insert(inputIndex + 2, QString::number(GenTime(m_outPoint - m_inPoint, pCore->getCurrentFps()).seconds()));
0263         }
0264     }
0265     if (m_isFfmpegJob) {
0266         // Only output error data
0267         parameters << QStringLiteral("-v") << QStringLiteral("error");
0268     }
0269     // Make sure we keep the stream order
0270     // parameters << QStringLiteral("-sn") << QStringLiteral("-dn") << QStringLiteral("-map") << QStringLiteral("0");
0271 
0272     qDebug() << "/// CUSTOM TASK PARAMS:\n" << parameters << "\n------";
0273     m_jobProcess.reset(new QProcess);
0274     // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
0275     QObject::connect(this, &CustomJobTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0276     QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &CustomJobTask::processLogInfo);
0277     m_jobProcess->start(binary, parameters, QIODevice::ReadOnly);
0278     AbstractTask::setPreferredPriority(m_jobProcess->processId());
0279     m_jobProcess->waitForFinished(-1);
0280     bool result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0281     requestedOutput.removeAll(destPath);
0282     // remove temporary playlist if it exists
0283     m_progress = 100;
0284     QMetaObject::invokeMethod(m_object, "updateJobProgress");
0285     if (result) {
0286         if (QFileInfo(destPath).size() == 0) {
0287             QFile::remove(destPath);
0288             // File was not created
0289             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create file.")),
0290                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0291         } else {
0292             QStringList lutExtentions = {QLatin1String("cube"), QLatin1String("3dl"), QLatin1String("dat"),
0293                                          QLatin1String("m3d"),  QLatin1String("csp"), QLatin1String("interp")};
0294             const QString ext = destPath.section(QLatin1Char('.'), -1);
0295             if (lutExtentions.contains(ext)) {
0296                 QMap<QString, QString> params;
0297                 params.insert(QStringLiteral("av.file"), destPath);
0298                 QMetaObject::invokeMethod(pCore->bin(), "addFilterToClip", Qt::QueuedConnection, Q_ARG(QString, QString::number(m_owner.itemId)),
0299                                           Q_ARG(QString, QStringLiteral("avfilter.lut3d")), Q_ARG(stringMap, params));
0300             } else {
0301                 QMetaObject::invokeMethod(pCore->bin(), "addProjectClipInFolder", Qt::QueuedConnection, Q_ARG(QString, destPath),
0302                                           Q_ARG(QString, QString::number(m_owner.itemId)), Q_ARG(QString, folderId), Q_ARG(QString, m_jobId));
0303             }
0304         }
0305     } else {
0306         // Process crashed
0307         QFile::remove(destPath);
0308         if (!m_isCanceled) {
0309             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create file.")),
0310                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0311         }
0312     }
0313 }
0314 
0315 void CustomJobTask::processLogInfo()
0316 {
0317     const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
0318     m_logDetails.append(buffer);
0319     if (m_isFfmpegJob) {
0320         // Parse FFmpeg output
0321         if (m_jobDuration == 0) {
0322             if (buffer.contains(QLatin1String("Duration:"))) {
0323                 QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
0324                 if (!data.isEmpty()) {
0325                     QStringList numbers = data.split(QLatin1Char(':'));
0326                     if (numbers.size() < 3) {
0327                         return;
0328                     }
0329                     m_jobDuration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toInt();
0330                 }
0331             }
0332         } else if (buffer.contains(QLatin1String("time="))) {
0333             int progress = 0;
0334             QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
0335             if (!time.isEmpty()) {
0336                 QStringList numbers = time.split(QLatin1Char(':'));
0337                 if (numbers.size() < 3) {
0338                     progress = time.toInt();
0339                     if (progress == 0) {
0340                         return;
0341                     }
0342                 } else {
0343                     progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + qRound(numbers.at(2).toDouble());
0344                 }
0345             }
0346             m_progress = 100 * progress / m_jobDuration;
0347             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0348             // emit jobProgress(int(100.0 * progress / m_jobDuration));
0349         }
0350     } else {
0351         // Parse MLT output
0352         if (buffer.contains(QLatin1String("percentage:"))) {
0353             m_progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
0354             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0355         }
0356     }
0357 }