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 "proxytask.h"
0008 #include "bin/bin.h"
0009 #include "bin/projectclip.h"
0010 #include "bin/projectitemmodel.h"
0011 #include "core.h"
0012 #include "doc/kdenlivedoc.h"
0013 #include "kdenlive_debug.h"
0014 #include "kdenlivesettings.h"
0015 #include "macros.hpp"
0016 
0017 #include <QProcess>
0018 #include <QTemporaryFile>
0019 #include <QThread>
0020 
0021 #include <KLocalizedString>
0022 
0023 ProxyTask::ProxyTask(const ObjectId &owner, QObject *object)
0024     : AbstractTask(owner, AbstractTask::PROXYJOB, object)
0025     , m_jobDuration(0)
0026     , m_isFfmpegJob(true)
0027     , m_jobProcess(nullptr)
0028 {
0029     m_description = i18n("Creating proxy");
0030 }
0031 
0032 void ProxyTask::start(const ObjectId &owner, QObject *object, bool force)
0033 {
0034     // See if there is already a task for this MLT service and resource.
0035     if (pCore->taskManager.hasPendingJob(owner, AbstractTask::PROXYJOB)) {
0036         return;
0037     }
0038     ProxyTask *task = new ProxyTask(owner, object);
0039     // Otherwise, start a new proxy generation thread.
0040     task->m_isForce = force;
0041     pCore->taskManager.startTask(owner.itemId, task);
0042 }
0043 
0044 void ProxyTask::run()
0045 {
0046     AbstractTaskDone whenFinished(m_owner.itemId, this);
0047     if (m_isCanceled || pCore->taskManager.isBlocked()) {
0048         return;
0049     }
0050     QMutexLocker lock(&m_runMutex);
0051     m_running = true;
0052     auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId));
0053     if (binClip == nullptr) {
0054         return;
0055     }
0056     const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
0057     QFileInfo fInfo(dest);
0058     if (binClip->getProducerIntProperty(QStringLiteral("_overwriteproxy")) == 0 && fInfo.exists() && fInfo.size() > 0) {
0059         // Proxy clip already created
0060         m_progress = 100;
0061         QMetaObject::invokeMethod(m_object, "updateJobProgress");
0062         QMetaObject::invokeMethod(binClip.get(), "updateProxyProducer", Qt::QueuedConnection, Q_ARG(QString, dest));
0063         return;
0064     }
0065 
0066     ClipType::ProducerType type = binClip->clipType();
0067     m_progress = 0;
0068     bool result = false;
0069     QString source = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl"));
0070     int exif = binClip->getProducerIntProperty(QStringLiteral("_exif_orientation"));
0071     if (type == ClipType::Playlist || type == ClipType::SlideShow) {
0072         // change FFmpeg params to MLT format
0073         m_isFfmpegJob = false;
0074         QStringList mltParameters;
0075         QTemporaryFile *playlist = nullptr;
0076         // set clip origin
0077         if (type == ClipType::Playlist) {
0078             // Special case: playlists use the special 'consumer' producer to support resizing
0079             source.prepend(QStringLiteral("consumer:"));
0080         } else {
0081             // create temporary playlist to generate proxy
0082             // we save a temporary .mlt clip for rendering
0083             QDomDocument doc;
0084             QDomElement xml = binClip->toXml(doc, false);
0085             playlist = new QTemporaryFile();
0086             playlist->setFileTemplate(playlist->fileTemplate() + QStringLiteral(".mlt"));
0087             if (playlist->open()) {
0088                 source = playlist->fileName();
0089                 QTextStream out(playlist);
0090 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0091                 out.setCodec("UTF-8");
0092 #endif
0093                 out << doc.toString();
0094                 playlist->close();
0095             }
0096         }
0097         mltParameters << QStringLiteral("-profile") << pCore->getCurrentProfilePath();
0098         mltParameters << source;
0099         // set destination
0100         mltParameters << QStringLiteral("-consumer") << QStringLiteral("avformat:%1").arg(dest) << QStringLiteral("out=%1").arg(binClip->frameDuration());
0101         QString parameter = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified();
0102         if (parameter.isEmpty()) {
0103             // Automatic setting, decide based on hw support
0104             parameter = pCore->currentDoc()->getAutoProxyProfile();
0105             bool nvenc = parameter.contains(QStringLiteral("%nvcodec"));
0106             if (nvenc) {
0107                 parameter = parameter.section(QStringLiteral("-i"), 1);
0108                 parameter.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale"));
0109                 parameter.replace(QStringLiteral("scale_npp"), QStringLiteral("scale"));
0110                 parameter.prepend(QStringLiteral("-pix_fmt yuv420p"));
0111             }
0112         }
0113         int proxyResize = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyresize")).toInt();
0114         parameter.replace(QStringLiteral("%width"), QString::number(proxyResize));
0115         if (parameter.contains(QLatin1String("-i "))) {
0116             // Remove the source input if any
0117             parameter.remove(QLatin1String("-i "));
0118         }
0119 
0120         QStringList params = parameter.split(QLatin1Char('-'), Qt::SkipEmptyParts);
0121         double display_ratio;
0122         if (source.startsWith(QLatin1String("consumer:"))) {
0123             display_ratio = KdenliveDoc::getDisplayRatio(source.section(QLatin1Char(':'), 1));
0124         } else {
0125             display_ratio = KdenliveDoc::getDisplayRatio(source);
0126         }
0127         if (display_ratio < 1e-6) {
0128             display_ratio = 1;
0129         }
0130 
0131         bool skipNext = false;
0132         for (const QString &s : qAsConst(params)) {
0133             QString t = s.simplified();
0134             if (skipNext) {
0135                 skipNext = false;
0136                 continue;
0137             }
0138             if (t.count(QLatin1Char(' ')) == 0) {
0139                 t.append(QLatin1String("=1"));
0140             } else if (t.startsWith(QLatin1String("vf "))) {
0141                 skipNext = true;
0142                 bool ok = false;
0143                 int width = t.section(QLatin1Char('='), 1, 1).section(QLatin1Char(':'), 0, 0).toInt(&ok);
0144                 if (!ok) {
0145                     width = 640;
0146                 }
0147                 int height = int(width / display_ratio);
0148                 // Make sure we get an even height
0149                 height += height % 2;
0150                 mltParameters << QStringLiteral("s=%1x%2").arg(width).arg(height);
0151                 if (t.contains(QStringLiteral("yadif"))) {
0152                     mltParameters << QStringLiteral("progressive=1");
0153                 }
0154                 continue;
0155             } else {
0156                 t.replace(QLatin1Char(' '), QLatin1String("="));
0157                 if (t == QLatin1String("acodec=copy") && type == ClipType::Playlist) {
0158                     // drop this for playlists, otherwise we have no sound in proxies
0159                     continue;
0160                 }
0161             }
0162             mltParameters << t;
0163         }
0164         int threadCount = QThread::idealThreadCount();
0165         if (threadCount > 2) {
0166             threadCount = qMin(threadCount - 1, 4);
0167         } else {
0168             threadCount = 1;
0169         }
0170         // real_time parameter seems to cause rendering artifacts with playlist clips
0171         // mltParameters.append(QStringLiteral("real_time=-%1").arg(threadCount));
0172         mltParameters.append(QStringLiteral("threads=%1").arg(threadCount));
0173         mltParameters.append(QStringLiteral("terminate_on_pause=1"));
0174 
0175         // TODO: currently, when rendering an xml file through melt, the display ration is lost, so we enforce it manually
0176         mltParameters << QStringLiteral("aspect=") + QString::number(display_ratio, 'f');
0177 
0178         // Ask for progress reporting
0179         mltParameters << QStringLiteral("progress=1");
0180 
0181         m_jobProcess.reset(new QProcess);
0182         // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
0183         qDebug() << " :: STARTING PLAYLIST PROXY: " << mltParameters;
0184         QObject::connect(this, &ProxyTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0185         QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &ProxyTask::processLogInfo);
0186         m_jobProcess->start(KdenliveSettings::meltpath(), mltParameters);
0187         AbstractTask::setPreferredPriority(m_jobProcess->processId());
0188         m_jobProcess->waitForFinished(-1);
0189         result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0190         delete playlist;
0191     } else if (type == ClipType::Image) {
0192         m_isFfmpegJob = false;
0193         // Image proxy
0194         QImage i(source);
0195         if (i.isNull()) {
0196             result = false;
0197             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot load image %1.", source)),
0198                                       Q_ARG(int, int(KMessageWidget::Warning)));
0199             m_progress = 100;
0200             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0201             return;
0202         }
0203 
0204         QImage proxy;
0205         // Images are scaled to predefined size.
0206         int maxSize = qMax(i.width(), i.height());
0207         if (KdenliveSettings::proxyimagesize() < maxSize) {
0208             if (i.width() > i.height()) {
0209                 proxy = i.scaledToWidth(KdenliveSettings::proxyimagesize());
0210             } else {
0211                 proxy = i.scaledToHeight(KdenliveSettings::proxyimagesize());
0212             }
0213             if (exif > 1) {
0214                 // Rotate image according to exif data
0215                 QImage processed;
0216                 QTransform matrix;
0217 
0218                 switch (exif) {
0219                 case 2:
0220                     matrix.scale(-1, 1);
0221                     break;
0222                 case 3:
0223                     matrix.rotate(180);
0224                     break;
0225                 case 4:
0226                     matrix.scale(1, -1);
0227                     break;
0228                 case 5:
0229                     matrix.rotate(270);
0230                     matrix.scale(-1, 1);
0231                     break;
0232                 case 6:
0233                     matrix.rotate(90);
0234                     break;
0235                 case 7:
0236                     matrix.rotate(90);
0237                     matrix.scale(-1, 1);
0238                     break;
0239                 case 8:
0240                     matrix.rotate(270);
0241                     break;
0242                 }
0243                 processed = proxy.transformed(matrix);
0244                 processed.save(dest);
0245             } else {
0246                 proxy.save(dest);
0247             }
0248             result = true;
0249         } else {
0250             // Image is too small to be proxied
0251             m_logDetails = i18n("Image too small to be proxied.");
0252             result = false;
0253         }
0254     } else {
0255         m_isFfmpegJob = true;
0256         if (!QFileInfo(KdenliveSettings::ffmpegpath()).isFile()) {
0257             // FFmpeg not detected, cannot process the Job
0258             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection,
0259                                       Q_ARG(QString, i18n("FFmpeg not found, please set path in Kdenlive's settings Environment")),
0260                                       Q_ARG(int, int(KMessageWidget::Warning)));
0261             result = true;
0262             m_progress = 100;
0263             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0264             return;
0265         }
0266         // Only output error data, make sure we don't block when proxy file already exists
0267         QStringList parameters = {QStringLiteral("-hide_banner"), QStringLiteral("-y"), QStringLiteral("-stats"), QStringLiteral("-v"),
0268                                   QStringLiteral("error")};
0269         m_jobDuration = int(binClip->duration().seconds());
0270         if (binClip->hasProducerProperty(QStringLiteral("kdenlive:camcorderproxy"))) {
0271             // ffmpeg -an -i proxy.mp4 -vn -i original.MXF -map 0:v -map 1:a -c:v copy out.MP4
0272             // Create a new proxy file with video from camcorder proxy and audio from source clip
0273             const QString proxyPath = binClip->getProducerProperty(QStringLiteral("kdenlive:camcorderproxy"));
0274             parameters << QStringLiteral("-an") << QStringLiteral("-i") << proxyPath;
0275             parameters << QStringLiteral("-vn") << QStringLiteral("-i") << source;
0276             parameters << QStringLiteral("-map") << QStringLiteral("0:v") << QStringLiteral("-map") << QStringLiteral("1:a");
0277             parameters << QStringLiteral("-c:v") << QStringLiteral("copy") << dest;
0278         } else {
0279             QString proxyParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified();
0280             if (proxyParams.isEmpty()) {
0281                 // Automatic setting, decide based on hw support
0282                 proxyParams = pCore->currentDoc()->getAutoProxyProfile();
0283             }
0284             int proxyResize = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyresize")).toInt();
0285             if (!proxyParams.contains(QLatin1String("mjpeg")) && !proxyParams.contains(QLatin1String("mpeg2video"))) {
0286                 parameters << QStringLiteral("-noautorotate");
0287             }
0288             // Check if source is interlaced
0289             bool interlacedSource = false;
0290             // TODO: should proxy clips keep interlacing ?
0291             /*if (binClip->getProducerIntProperty(QStringLiteral("meta.media.progressive")) == 0) {
0292                 // Interlaced
0293                 interlacedSource = true;
0294                 qDebug() << "::: Interlaced content disabling nvidia codecs";
0295             } else {
0296                 qDebug() << "::: Found progressive content";
0297             }*/
0298             bool vaapi = proxyParams.contains(QStringLiteral("vaapi"));
0299             if (vaapi && interlacedSource) {
0300                 // Disable vaapi if source is interlaced
0301                 proxyParams = proxyParams.section(QStringLiteral("-i "), 1);
0302                 proxyParams.replace(QStringLiteral(",format=nv12,hwupload"), QString());
0303                 proxyParams.replace(QStringLiteral("h264_vaapi"), QStringLiteral("libx264"));
0304             }
0305             bool nvenc = proxyParams.contains(QStringLiteral("%nvcodec"));
0306             if (nvenc) {
0307                 QString pix_fmt = binClip->videoCodecProperty(QStringLiteral("pix_fmt"));
0308                 QString codec = binClip->videoCodecProperty(QStringLiteral("name"));
0309                 QStringList supportedCodecs{QStringLiteral("hevc"),  QStringLiteral("h264"),  QStringLiteral("mjpeg"),
0310                                             QStringLiteral("mpeg1"), QStringLiteral("mpeg2"), QStringLiteral("mpeg4"),
0311                                             QStringLiteral("vc1"),   QStringLiteral("vp8"),   QStringLiteral("vp9")};
0312                 QStringList supportedPixFmts{QStringLiteral("yuv420p"), QStringLiteral("yuyv422"), QStringLiteral("rgb24"),
0313                                              QStringLiteral("bgr24"),   QStringLiteral("yuv422p"), QStringLiteral("yuv444p"),
0314                                              QStringLiteral("rgb32"),   QStringLiteral("yuv410p"), QStringLiteral("yuv411p")};
0315 
0316                 // Check if the transcoded file uses a cuda supported codec (we don't check for specific cards so not 100% exact)
0317                 bool supported = !interlacedSource && supportedCodecs.contains(codec) && supportedPixFmts.contains(pix_fmt);
0318                 if (supported && proxyParams.contains(QStringLiteral("scale_npp")) && !KdenliveSettings::nvScalingEnabled()) {
0319                     supported = false;
0320                 }
0321                 if (supported && proxyParams.contains(QStringLiteral("%frameSize"))) {
0322                     int w = proxyResize;
0323                     int h = 0;
0324                     int oW = binClip->getProducerProperty(QStringLiteral("meta.media.width")).toInt();
0325                     int oH = binClip->getProducerProperty(QStringLiteral("meta.media.height")).toInt();
0326                     if (oH > 0) {
0327                         h = w * oH / oW;
0328                     } else {
0329                         h = int(w / pCore->getCurrentDar());
0330                     }
0331                     h += h % 2;
0332                     proxyParams.replace(QStringLiteral("%frameSize"), QString("%1x%2").arg(w).arg(h));
0333                 }
0334                 if (supported) {
0335                     // Full hardware decoding supported
0336                     codec.append(QStringLiteral("_cuvid"));
0337                     proxyParams.replace(QStringLiteral("%nvcodec"), codec);
0338                 } else {
0339                     proxyParams = proxyParams.section(QStringLiteral("-i "), 1);
0340                     if (!supportedPixFmts.contains(pix_fmt)) {
0341                         proxyParams.prepend(QStringLiteral("-pix_fmt yuv420p "));
0342                     }
0343                     proxyParams.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale"));
0344                     proxyParams.replace(QStringLiteral("scale_npp"), QStringLiteral("scale"));
0345                     if (interlacedSource) {
0346                         // nVidia does not support interlaced encoding
0347                         proxyParams.replace(QStringLiteral("h264_nvenc"), QStringLiteral("libx264"));
0348                     }
0349                 }
0350             }
0351             proxyParams.replace(QStringLiteral("%width"), QString::number(proxyResize));
0352             bool disableAutorotate = binClip->getProducerProperty(QStringLiteral("autorotate")) == QLatin1String("0");
0353             if (disableAutorotate || proxyParams.contains(QStringLiteral("-noautorotate"))) {
0354                 // The noautorotate flag must be passed before input source
0355                 parameters << QStringLiteral("-noautorotate");
0356                 proxyParams.replace(QStringLiteral("-noautorotate"), QString());
0357             }
0358             if (proxyParams.contains(QLatin1String("-i "))) {
0359                 // we have some pre-filename parameters, filename will be inserted later
0360             } else {
0361                 parameters << QStringLiteral("-i") << source;
0362             }
0363             QString params = proxyParams;
0364             for (const QString &s : params.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
0365                 QString t = s.simplified();
0366                 parameters << t;
0367                 if (t == QLatin1String("-i")) {
0368                     parameters << source;
0369                 }
0370             }
0371             if (interlacedSource) {
0372                 // Keep interlacing
0373                 parameters << QStringLiteral("-flags") << QStringLiteral("+ildct+ilme") << QStringLiteral("-top") << QStringLiteral("-1");
0374             }
0375 
0376             // Make sure we keep the stream order
0377             parameters << QStringLiteral("-sn") << QStringLiteral("-dn") << QStringLiteral("-map") << QStringLiteral("0");
0378             // Drop unknown streams instead of aborting
0379             parameters << QStringLiteral("-ignore_unknown");
0380             parameters << dest;
0381             qDebug() << "/// FULL PROXY PARAMS:\n" << parameters << "\n------";
0382         }
0383         m_jobProcess.reset(new QProcess);
0384         // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
0385         QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &ProxyTask::processLogInfo);
0386         QObject::connect(this, &ProxyTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0387         m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly);
0388         AbstractTask::setPreferredPriority(m_jobProcess->processId());
0389         m_jobProcess->waitForFinished(-1);
0390         result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0391     }
0392     // remove temporary playlist if it exists
0393     m_progress = 100;
0394     if (result && !m_isCanceled) {
0395         if (QFileInfo(dest).size() == 0) {
0396             QFile::remove(dest);
0397             // File was not created
0398             result = false;
0399             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create proxy clip.")),
0400                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0401             if (binClip) {
0402                 binClip->setProducerProperty(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
0403             }
0404         } else if (binClip) {
0405             // Job successful
0406             QMetaObject::invokeMethod(binClip.get(), "updateProxyProducer", Qt::QueuedConnection, Q_ARG(QString, dest));
0407         }
0408     } else {
0409         // Proxy process crashed
0410         QFile::remove(dest);
0411         if (!m_isCanceled) {
0412             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create proxy clip.")),
0413                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0414         }
0415     }
0416     QMetaObject::invokeMethod(m_object, "updateJobProgress");
0417     return;
0418 }
0419 
0420 void ProxyTask::processLogInfo()
0421 {
0422     const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
0423     m_logDetails.append(buffer);
0424     if (m_isFfmpegJob) {
0425         // Parse FFmpeg output
0426         if (m_jobDuration == 0) {
0427             if (buffer.contains(QLatin1String("Duration:"))) {
0428                 QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
0429                 if (!data.isEmpty()) {
0430                     QStringList numbers = data.split(QLatin1Char(':'));
0431                     if (numbers.size() < 3) {
0432                         return;
0433                     }
0434                     m_jobDuration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toInt();
0435                 }
0436             }
0437         } else if (buffer.contains(QLatin1String("time="))) {
0438             int progress = 0;
0439             QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
0440             if (!time.isEmpty()) {
0441                 QStringList numbers = time.split(QLatin1Char(':'));
0442                 if (numbers.size() < 3) {
0443                     progress = time.toInt();
0444                     if (progress == 0) {
0445                         return;
0446                     }
0447                 } else {
0448                     progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + qRound(numbers.at(2).toDouble());
0449                 }
0450             }
0451             m_progress = 100 * progress / m_jobDuration;
0452             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0453             // Q_EMIT jobProgress(int(100.0 * progress / m_jobDuration));
0454         }
0455     } else {
0456         // Parse MLT output
0457         if (buffer.contains(QLatin1String("percentage:"))) {
0458             m_progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
0459             QMetaObject::invokeMethod(m_object, "updateJobProgress");
0460             // Q_EMIT jobProgress(progress);
0461         }
0462     }
0463 }