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 &params, 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 &params, 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 }