File indexing completed on 2024-12-01 04:28:37
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 "transcodetask.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 "doc/kdenlivedoc.h" 0015 #include "kdenlive_debug.h" 0016 #include "kdenlivesettings.h" 0017 #include "macros.hpp" 0018 #include "mainwindow.h" 0019 0020 #include <QProcess> 0021 #include <QTemporaryFile> 0022 #include <QThread> 0023 0024 #include <KLocalizedString> 0025 0026 TranscodeTask::TranscodeTask(const ObjectId &owner, const QString &suffix, const QString &preParams, const QString ¶ms, int in, int out, 0027 bool replaceProducer, QObject *object, bool checkProfile) 0028 : AbstractTask(owner, AbstractTask::TRANSCODEJOB, object) 0029 , m_jobDuration(0) 0030 , m_isFfmpegJob(true) 0031 , m_suffix(suffix) 0032 , m_transcodeParams(params) 0033 , m_transcodePreParams(preParams) 0034 , m_replaceProducer(replaceProducer) 0035 , m_inPoint(in) 0036 , m_outPoint(out) 0037 , m_checkProfile(checkProfile) 0038 , m_jobProcess(nullptr) 0039 { 0040 m_description = i18n("Transcoding"); 0041 } 0042 0043 void TranscodeTask::start(const ObjectId &owner, const QString &suffix, const QString &preParams, const QString ¶ms, int in, int out, bool replaceProducer, 0044 QObject *object, bool force, bool checkProfile) 0045 { 0046 // See if there is already a task for this MLT service and resource. 0047 if (pCore->taskManager.hasPendingJob(owner, AbstractTask::TRANSCODEJOB)) { 0048 // return; 0049 } 0050 TranscodeTask *task = new TranscodeTask(owner, suffix, preParams, params, in, out, replaceProducer, object, checkProfile); 0051 if (task) { 0052 // Otherwise, start a new audio levels generation thread. 0053 task->m_isForce = force; 0054 pCore->taskManager.startTask(owner.itemId, task); 0055 } 0056 } 0057 0058 void TranscodeTask::run() 0059 { 0060 AbstractTaskDone whenFinished(m_owner.itemId, this); 0061 if (m_isCanceled || pCore->taskManager.isBlocked()) { 0062 return; 0063 } 0064 QMutexLocker lock(&m_runMutex); 0065 m_running = true; 0066 auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId)); 0067 ClipType::ProducerType type = binClip->clipType(); 0068 QString source; 0069 QTemporaryFile src; 0070 if (type == ClipType::Text || type == ClipType::Timeline) { 0071 src.setFileTemplate(QDir::temp().absoluteFilePath(QString("XXXXXX.mlt"))); 0072 if (src.open()) { 0073 source = src.fileName(); 0074 QDomDocument doc; 0075 binClip->getProducerXML(doc, false, true); 0076 QTextStream out(&src); 0077 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0078 out.setCodec("UTF-8"); 0079 #endif 0080 out << doc.toString(); 0081 src.close(); 0082 } 0083 } else { 0084 source = binClip->url(); 0085 } 0086 if (source.isEmpty()) { 0087 return; 0088 } 0089 0090 QString transcoderExt = m_transcodeParams.section(QLatin1String("%1"), 1).section(QLatin1Char(' '), 0, 0); 0091 if (transcoderExt.isEmpty()) { 0092 qDebug() << "// INVALID TRANSCODING PROFILE"; 0093 m_progress = 100; 0094 return; 0095 } 0096 QFileInfo finfo(source); 0097 QString fileName; 0098 QDir dir; 0099 if (type == ClipType::Text) { 0100 fileName = binClip->name(); 0101 dir = QDir(pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().adjusted(QUrl::RemoveFilename).toLocalFile() 0102 : KdenliveSettings::defaultprojectfolder()); 0103 } else { 0104 fileName = finfo.fileName().section(QLatin1Char('.'), 0, -2); 0105 dir = finfo.absoluteDir(); 0106 } 0107 int fileCount = 1; 0108 QString num = QString::number(fileCount).rightJustified(4, '0', false); 0109 QString path; 0110 if (m_suffix.isEmpty()) { 0111 path = fileName + num + transcoderExt; 0112 } else { 0113 path = fileName + m_suffix + transcoderExt; 0114 fileCount = 0; 0115 } 0116 while (dir.exists(path)) { 0117 ++fileCount; 0118 num = QString::number(fileCount).rightJustified(4, '0', false); 0119 path = fileName + num + m_suffix + transcoderExt; 0120 } 0121 QString destUrl = dir.absoluteFilePath(path.section(QLatin1Char('.'), 0, -2)); 0122 0123 bool result; 0124 if (type == ClipType::Playlist || type == ClipType::SlideShow || type == ClipType::Text || type == ClipType::Timeline) { 0125 // change FFmpeg params to MLT format 0126 m_isFfmpegJob = false; 0127 // insert transcoded filename 0128 m_transcodeParams.replace(QStringLiteral("%1"), QString("-consumer %1")); 0129 // Convert param style 0130 QStringList params = m_transcodeParams.split(QLatin1Char('-'), Qt::SkipEmptyParts); 0131 QStringList mltParameters; 0132 for (const QString &s : qAsConst(params)) { 0133 QString t = s.simplified(); 0134 if (t.count(QLatin1Char(' ')) == 0) { 0135 t.append(QLatin1String("=1")); 0136 } else { 0137 if (t.contains(QLatin1String("%1"))) { 0138 // file name 0139 mltParameters.prepend(t.section(QLatin1Char(' '), 1).replace(QLatin1String("%1"), QString("avformat:%1").arg(destUrl))); 0140 mltParameters.prepend(QStringLiteral("-consumer")); 0141 continue; 0142 } 0143 if (t.startsWith(QLatin1String("aspect "))) { 0144 // Fix aspect ratio calculation 0145 t.replace(QLatin1Char(' '), QLatin1String("=@")); 0146 t.replace(QLatin1Char(':'), QLatin1String("/")); 0147 } else { 0148 t.replace(QLatin1Char(' '), QLatin1String("=")); 0149 } 0150 } 0151 mltParameters << t; 0152 } 0153 int threadCount = QThread::idealThreadCount(); 0154 if (threadCount > 2) { 0155 threadCount = qMin(threadCount - 1, 4); 0156 } else { 0157 threadCount = 1; 0158 } 0159 mltParameters.append(QStringLiteral("real_time=-%1").arg(threadCount)); 0160 mltParameters.append(QStringLiteral("threads=%1").arg(threadCount)); 0161 0162 // Ask for progress reporting 0163 mltParameters << QStringLiteral("progress=1"); 0164 if (m_outPoint > 0) { 0165 mltParameters.prepend(QString("out=%1").arg(m_outPoint)); 0166 mltParameters.prepend(QString("in=%1").arg(m_inPoint)); 0167 } 0168 mltParameters.prepend(source); 0169 m_jobProcess.reset(new QProcess); 0170 // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); 0171 QObject::connect(this, &TranscodeTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection); 0172 QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &TranscodeTask::processLogInfo); 0173 m_jobProcess->start(KdenliveSettings::meltpath(), mltParameters); 0174 AbstractTask::setPreferredPriority(m_jobProcess->processId()); 0175 m_jobProcess->waitForFinished(-1); 0176 result = m_jobProcess->exitStatus() == QProcess::NormalExit; 0177 } else { 0178 m_isFfmpegJob = true; 0179 QStringList parameters; 0180 if (KdenliveSettings::ffmpegpath().isEmpty()) { 0181 // FFmpeg not detected, cannot process the Job 0182 QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, 0183 Q_ARG(QString, i18n("FFmpeg not found, please set path in Kdenlive's settings Environment")), 0184 Q_ARG(int, int(KMessageWidget::Warning))); 0185 return; 0186 } 0187 m_jobDuration = int(binClip->duration().seconds()); 0188 parameters << QStringLiteral("-y"); 0189 if (m_inPoint > -1) { 0190 parameters << QStringLiteral("-ss") << QString::number(GenTime(m_inPoint, pCore->getCurrentFps()).seconds()); 0191 } 0192 parameters << QStringLiteral("-stats"); 0193 if (!m_transcodePreParams.isEmpty()) { 0194 parameters << m_transcodePreParams.split(QStringLiteral(" ")); 0195 } 0196 parameters << QStringLiteral("-i") << source; 0197 if (m_outPoint > -1) { 0198 parameters << QStringLiteral("-to") << QString::number(GenTime(m_outPoint - m_inPoint, pCore->getCurrentFps()).seconds()); 0199 } 0200 // Only output error data 0201 parameters << QStringLiteral("-v") << QStringLiteral("error"); 0202 // Make sure we keep the stream order 0203 parameters << QStringLiteral("-sn") << QStringLiteral("-dn"); 0204 if (!m_transcodeParams.contains(QStringLiteral("-map ")) && !m_transcodeParams.contains(QStringLiteral(" amerge="))) { 0205 parameters << QStringLiteral("-map") << QStringLiteral("0"); 0206 } 0207 QStringList params = m_transcodeParams.split(QLatin1Char(' ')); 0208 for (const QString &s : qAsConst(params)) { 0209 QString t = s.simplified(); 0210 if (t.startsWith(QLatin1String("%1"))) { 0211 parameters << t.replace(QLatin1String("%1"), destUrl); 0212 } else { 0213 parameters << t; 0214 } 0215 } 0216 qDebug() << "/// FULL TRANSCODE PARAMS:\n" << parameters << "\n------"; 0217 m_jobProcess.reset(new QProcess); 0218 // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); 0219 QObject::connect(this, &TranscodeTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection); 0220 QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &TranscodeTask::processLogInfo); 0221 m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly); 0222 AbstractTask::setPreferredPriority(m_jobProcess->processId()); 0223 m_jobProcess->waitForFinished(-1); 0224 result = m_jobProcess->exitStatus() == QProcess::NormalExit; 0225 } 0226 destUrl.append(transcoderExt); 0227 // remove temporary playlist if it exists 0228 m_progress = 100; 0229 QMetaObject::invokeMethod(m_object, "updateJobProgress"); 0230 if (result) { 0231 if (QFileInfo(destUrl).size() == 0) { 0232 QFile::remove(destUrl); 0233 // File was not created 0234 QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create file.")), 0235 Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails)); 0236 } else { 0237 if (m_replaceProducer && binClip && binClip->clipType() != ClipType::Timeline) { 0238 QMap<QString, QString> sourceProps; 0239 QMap<QString, QString> newProps; 0240 sourceProps.insert(QStringLiteral("resource"), binClip->url()); 0241 sourceProps.insert(QStringLiteral("kdenlive:originalurl"), binClip->url()); 0242 sourceProps.insert(QStringLiteral("kdenlive:proxy"), binClip->getProducerProperty(QStringLiteral("kdenlive:proxy"))); 0243 sourceProps.insert(QStringLiteral("kdenlive:clipname"), binClip->clipName()); 0244 sourceProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1")); 0245 newProps.insert(QStringLiteral("resource"), destUrl); 0246 newProps.insert(QStringLiteral("kdenlive:originalurl"), destUrl); 0247 newProps.insert(QStringLiteral("kdenlive:clipname"), QFileInfo(destUrl).fileName()); 0248 newProps.insert(QStringLiteral("kdenlive:proxy"), QString()); 0249 newProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1")); 0250 if (m_checkProfile) { 0251 pCore->bin()->shouldCheckProfile = true; 0252 } 0253 pCore->bin()->slotEditClipCommand(binClip->clipId(), sourceProps, newProps); 0254 } else { 0255 QString folder = QStringLiteral("-1"); 0256 if (binClip) { 0257 auto containingFolder = std::static_pointer_cast<ProjectFolder>(binClip->parent()); 0258 if (containingFolder) { 0259 folder = containingFolder->clipId(); 0260 } 0261 } 0262 QMetaObject::invokeMethod(pCore->window(), "addProjectClip", Qt::QueuedConnection, Q_ARG(QString, destUrl), Q_ARG(QString, folder)); 0263 // id = ClipCreator::createClipFromFile(destUrl, folderId, pCore->projectItemModel()); 0264 } 0265 } 0266 } else { 0267 // Proxy process crashed 0268 QFile::remove(destUrl); 0269 if (!m_isCanceled) { 0270 QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create file.")), 0271 Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails)); 0272 } 0273 } 0274 } 0275 0276 void TranscodeTask::processLogInfo() 0277 { 0278 const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError()); 0279 m_logDetails.append(buffer); 0280 if (m_isFfmpegJob) { 0281 // Parse FFmpeg output 0282 if (m_jobDuration == 0) { 0283 if (buffer.contains(QLatin1String("Duration:"))) { 0284 QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); 0285 if (!data.isEmpty()) { 0286 QStringList numbers = data.split(QLatin1Char(':')); 0287 if (numbers.size() < 3) { 0288 return; 0289 } 0290 m_jobDuration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toInt(); 0291 } 0292 } 0293 } else if (buffer.contains(QLatin1String("time="))) { 0294 int progress = 0; 0295 QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); 0296 if (!time.isEmpty()) { 0297 QStringList numbers = time.split(QLatin1Char(':')); 0298 if (numbers.size() < 3) { 0299 progress = time.toInt(); 0300 if (progress == 0) { 0301 return; 0302 } 0303 } else { 0304 progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + qRound(numbers.at(2).toDouble()); 0305 } 0306 } 0307 m_progress = 100 * progress / m_jobDuration; 0308 QMetaObject::invokeMethod(m_object, "updateJobProgress"); 0309 // Q_EMIT jobProgress(int(100.0 * progress / m_jobDuration)); 0310 } 0311 } else { 0312 // Parse MLT output 0313 if (buffer.contains(QLatin1String("percentage:"))) { 0314 m_progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt(); 0315 QMetaObject::invokeMethod(m_object, "updateJobProgress"); 0316 } 0317 } 0318 }