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 }