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 }