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 "filtertask.h"
0008 #include "assets/model/assetparametermodel.hpp"
0009 #include "bin/bin.h"
0010 #include "bin/projectclip.h"
0011 #include "bin/projectitemmodel.h"
0012 #include "core.h"
0013 #include "effects/effectstack/model/effectstackmodel.hpp"
0014 #include "kdenlive_debug.h"
0015 #include "kdenlivesettings.h"
0016 #include "macros.hpp"
0017 #include "mainwindow.h"
0018 #include "profiles/profilemodel.hpp"
0019 #include "xml/xml.hpp"
0020 
0021 #include <QProcess>
0022 #include <QThread>
0023 
0024 #include <KLocalizedString>
0025 
0026 FilterTask::FilterTask(const ObjectId &owner, const QString &binId, const std::weak_ptr<AssetParameterModel> &model, const QString &assetId, int in, int out,
0027                        const QString &filterName, const std::unordered_map<QString, QVariant> &filterParams,
0028                        const std::unordered_map<QString, QString> &filterData, const QStringList &consumerArgs, QObject *object)
0029     : AbstractTask(owner, AbstractTask::FILTERCLIPJOB, object)
0030     , length(0)
0031     , m_binId(binId)
0032     , m_inPoint(in)
0033     , m_outPoint(out)
0034     , m_assetId(assetId)
0035     , m_model(model)
0036     , m_filterName(filterName)
0037     , m_filterParams(filterParams)
0038     , m_filterData(filterData)
0039     , m_consumerArgs(consumerArgs)
0040 {
0041     m_description = i18n("Processing filter %1", filterName);
0042 }
0043 
0044 void FilterTask::start(const ObjectId &owner, const QString &binId, const std::weak_ptr<AssetParameterModel> &model, const QString &assetId, int in, int out,
0045                        const QString &filterName, const std::unordered_map<QString, QVariant> &filterParams,
0046                        const std::unordered_map<QString, QString> &filterData, const QStringList &consumerArgs, QObject *object, bool force)
0047 {
0048     FilterTask *task = new FilterTask(owner, binId, model, assetId, in, out, filterName, filterParams, filterData, consumerArgs, object);
0049     // Otherwise, start a filter thread.
0050     task->m_isForce = force;
0051     pCore->taskManager.startTask(owner.itemId, task);
0052 }
0053 
0054 void FilterTask::run()
0055 {
0056     AbstractTaskDone whenFinished(m_owner.itemId, this);
0057     if (m_isCanceled || pCore->taskManager.isBlocked()) {
0058         return;
0059     }
0060     QMutexLocker lock(&m_runMutex);
0061     m_running = true;
0062 
0063     QString url;
0064     auto binClip = pCore->projectItemModel()->getClipByBinID(m_binId);
0065     std::unique_ptr<Mlt::Producer> producer = nullptr;
0066     Mlt::Profile profile(pCore->getCurrentProfilePath().toUtf8().constData());
0067     if (binClip) {
0068         // Filter applied on a timeline or bin clip
0069         url = binClip->url();
0070         if (url.isEmpty()) {
0071             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("No producer for this clip.")),
0072                                       Q_ARG(int, int(KMessageWidget::Warning)));
0073             return;
0074         }
0075         if (KdenliveSettings::gpu_accel()) {
0076             producer = binClip->getClone();
0077             if (m_outPoint == -1) {
0078                 m_outPoint = producer->get_length() - 1;
0079             }
0080             if (m_inPoint == -1) {
0081                 m_inPoint = 0;
0082             }
0083             if (m_inPoint != 0 || m_outPoint != producer->get_length() - 1) {
0084                 producer->set_in_and_out(m_inPoint, m_outPoint);
0085             }
0086             Mlt::Filter converter(profile, "avcolor_space");
0087             producer->attach(converter);
0088         } else {
0089             producer = std::make_unique<Mlt::Producer>(profile, url.toUtf8().constData());
0090             if (!producer || !producer->is_valid()) {
0091                 if (!binClip->isReloading) {
0092                     QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection,
0093                                               Q_ARG(QString, i18n("Cannot open file %1", binClip->url())), Q_ARG(int, int(KMessageWidget::Warning)));
0094                 } else {
0095                     QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection,
0096                                               Q_ARG(QString, i18n("Cannot process file %1", binClip->url())), Q_ARG(int, int(KMessageWidget::Warning)));
0097                 }
0098             }
0099             if (m_outPoint == -1) {
0100                 m_outPoint = producer->get_length() - 1;
0101             }
0102             if (m_inPoint == -1) {
0103                 m_inPoint = 0;
0104             }
0105             if (m_inPoint != 0 || m_outPoint != producer->get_length() - 1) {
0106                 producer->set_in_and_out(m_inPoint, m_outPoint);
0107             }
0108             // Ensure all user defined properties are passed
0109             const char *list = ClipController::getPassPropertiesList();
0110             std::shared_ptr<Mlt::Producer> sourceProducer = binClip->originalProducer();
0111             Mlt::Properties original(sourceProducer->get_properties());
0112             Mlt::Properties cloneProps(producer->get_properties());
0113             cloneProps.pass_list(original, list);
0114             for (int i = 0; i < sourceProducer->filter_count(); i++) {
0115                 std::shared_ptr<Mlt::Filter> filt(sourceProducer->filter(i));
0116                 if (filt->property_exists("kdenlive_id")) {
0117                     auto *filter = new Mlt::Filter(*filt.get());
0118                     producer->attach(*filter);
0119                 }
0120             }
0121             if (m_owner.type == KdenliveObjectType::TimelineClip) {
0122                 // Add the timeline clip effects
0123                 std::shared_ptr<EffectStackModel> stack = pCore->getItemEffectStack(pCore->currentTimelineId(), int(m_owner.type), m_owner.itemId);
0124                 stack->passEffects(producer.get(), m_filterName);
0125             }
0126         }
0127         if ((producer == nullptr) || !producer->is_valid()) {
0128             // Clip was removed or something went wrong
0129             if (!binClip->isReloading) {
0130                 QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot open file %1", binClip->url())),
0131                                           Q_ARG(int, int(KMessageWidget::Warning)));
0132             } else {
0133                 QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection,
0134                                           Q_ARG(QString, i18n("Cannot process file %1", binClip->url())), Q_ARG(int, int(KMessageWidget::Warning)));
0135             }
0136             return;
0137         }
0138     } else {
0139         // Filter applied on a track of master producer, leave config to source job
0140         // We are on master or track, configure producer accordingly
0141         if (m_owner.type == KdenliveObjectType::Master) {
0142             producer = pCore->getMasterProducerInstance();
0143         } else if (m_owner.type == KdenliveObjectType::TimelineTrack) {
0144             producer = pCore->getTrackProducerInstance(m_owner.itemId);
0145         }
0146     }
0147 
0148     if (producer == nullptr || !producer->is_valid()) {
0149         // Clip was removed or something went wrong, Notify user?
0150         QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot open source.")),
0151                                   Q_ARG(int, int(KMessageWidget::Warning)));
0152         return;
0153     }
0154     length = producer->get_playtime();
0155     if (length == 0) {
0156         length = producer->get_length();
0157     }
0158 
0159     // Build consumer
0160     QTemporaryFile sourceFile(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.mlt")));
0161     if (!sourceFile.open()) {
0162         // Something went wrong
0163         return;
0164     }
0165     sourceFile.close();
0166     QTemporaryFile destFile(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.mlt")));
0167     if (!destFile.open()) {
0168         // Something went wrong
0169         return;
0170     }
0171     destFile.close();
0172     std::unique_ptr<Mlt::Consumer> consumer(new Mlt::Consumer(profile, "xml", sourceFile.fileName().toUtf8().constData()));
0173     if (!consumer->is_valid()) {
0174         QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot create consumer.")),
0175                                   Q_ARG(int, int(KMessageWidget::Warning)));
0176         return;
0177     }
0178 
0179     consumer->connect(*producer.get());
0180     producer->set_speed(0);
0181 
0182     if (binClip) {
0183         // Build filter
0184         Mlt::Filter filter(profile, m_filterName.toUtf8().data());
0185         if (!filter.is_valid()) {
0186             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot create filter %1", m_filterName)),
0187                                       Q_ARG(int, int(KMessageWidget::Warning)));
0188             return;
0189         }
0190 
0191         // Process filter params
0192         qDebug() << " = = = = = CONFIGURING FILTER PARAMS = = = = =  ";
0193         for (const auto &it : m_filterParams) {
0194             qDebug() << ". . ." << it.first << " = " << it.second;
0195             if (it.first == QLatin1String("in") || it.first == QLatin1String("out")) {
0196                 continue;
0197             }
0198 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0199             if (it.second.type() == QVariant::Double) {
0200 #else
0201             if (it.second.typeId() == QMetaType::Double) {
0202 #endif
0203                 filter.set(it.first.toUtf8().constData(), it.second.toDouble());
0204             } else {
0205                 filter.set(it.first.toUtf8().constData(), it.second.toString().toUtf8().constData());
0206             }
0207         }
0208         if (m_filterData.find(QLatin1String("relativeInOut")) != m_filterData.end()) {
0209             // leave it operate on full clip
0210         } else {
0211             filter.set_in_and_out(m_inPoint, m_outPoint);
0212         }
0213         producer->attach(filter);
0214         filter.set("kdenlive:id", "kdenlive-analysis");
0215     }
0216 
0217     qDebug() << "=== FILTER READY TO PROCESS; LENGTH: " << length;
0218     consumer->run();
0219     consumer.reset();
0220     producer.reset();
0221     // wholeProducer.reset();
0222 
0223     QDomDocument dom(sourceFile.fileName());
0224     Xml::docContentFromFile(dom, sourceFile.fileName(), false);
0225 
0226     // add consumer element
0227     QDomElement consumerNode = dom.createElement("consumer");
0228     QDomNodeList profiles = dom.elementsByTagName("profile");
0229     if (profiles.isEmpty()) {
0230         dom.documentElement().insertAfter(consumerNode, dom.documentElement());
0231     } else {
0232         dom.documentElement().insertAfter(consumerNode, profiles.at(profiles.length() - 1));
0233     }
0234     consumerNode.setAttribute("mlt_service", "xml");
0235     for (const QString &param : qAsConst(m_consumerArgs)) {
0236         if (param.contains(QLatin1Char('='))) {
0237             consumerNode.setAttribute(param.section(QLatin1Char('='), 0, 0), param.section(QLatin1Char('='), 1));
0238         }
0239     }
0240     consumerNode.setAttribute("resource", destFile.fileName());
0241     consumerNode.setAttribute("store", "kdenlive");
0242 
0243     QFile f1(sourceFile.fileName());
0244     f1.open(QIODevice::WriteOnly);
0245     QTextStream stream(&f1);
0246 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0247     stream.setCodec("UTF-8");
0248 #endif
0249     stream << dom.toString();
0250     f1.close();
0251     dom.clear();
0252 
0253     // Step 2: process the xml file and save in another .mlt file
0254     QStringList args({QStringLiteral("progress=1"), sourceFile.fileName()});
0255     m_jobProcess.reset(new QProcess);
0256     QObject::connect(this, &AbstractTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0257     QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &FilterTask::processLogInfo);
0258     m_jobProcess->start(KdenliveSettings::meltpath(), args);
0259     m_jobProcess->waitForFinished(-1);
0260     bool result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0261     m_progress = 100;
0262     if (auto ptr = m_model.lock()) {
0263         QMetaObject::invokeMethod(ptr.get(), "setProgress", Q_ARG(int, 100));
0264     }
0265     if (m_isCanceled || !result) {
0266         if (!m_isCanceled) {
0267             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to filter source.")),
0268                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0269         }
0270         return;
0271     }
0272 
0273     paramVector params;
0274     QString key("results");
0275     if (m_filterData.find(QStringLiteral("key")) != m_filterData.end()) {
0276         key = m_filterData.at(QStringLiteral("key"));
0277     }
0278 
0279     QString resultData;
0280     if (Xml::docContentFromFile(dom, destFile.fileName(), false)) {
0281         qDebug() << "AAAA\nGOT DOC\n" << dom.toString();
0282         QDomNodeList filters = dom.elementsByTagName(QLatin1String("filter"));
0283         for (int i = 0; i < filters.count(); ++i) {
0284             QDomElement currentParameter = filters.item(i).toElement();
0285             if (Xml::getXmlProperty(currentParameter, QLatin1String("mlt_service")) == m_filterName) {
0286                 resultData = Xml::getXmlProperty(currentParameter, key);
0287             } else if (Xml::getXmlProperty(currentParameter, QLatin1String("kdenlive:id")) == QLatin1String("kdenlive-analysis")) {
0288                 resultData = Xml::getXmlProperty(currentParameter, key);
0289             }
0290             if (!resultData.isEmpty()) {
0291                 break;
0292             }
0293         }
0294     }
0295 
0296     if (m_inPoint > 0 && (m_filterData.find(QLatin1String("relativeInOut")) == m_filterData.end())) {
0297         // Motion tracker keyframes always start at master clip 0, so no need to set in/out points
0298         params.append({QStringLiteral("in"), m_inPoint});
0299         params.append({QStringLiteral("out"), m_outPoint});
0300     }
0301     params.append({key, QVariant(resultData)});
0302     if (m_filterData.find(QStringLiteral("storedata")) != m_filterData.end()) {
0303         // Store a copy of the data in clip analysis
0304         QString dataName = (m_filterData.find(QStringLiteral("displaydataname")) != m_filterData.end()) ? m_filterData.at(QStringLiteral("displaydataname"))
0305                                                                                                         : QStringLiteral("data");
0306         auto binClip = pCore->projectItemModel()->getClipByBinID(m_binId);
0307         if (binClip) {
0308             QMetaObject::invokeMethod(binClip.get(), "updatedAnalysisData", Q_ARG(QString, dataName), Q_ARG(QString, resultData), Q_ARG(int, m_inPoint));
0309         }
0310         // binClip->updatedAnalysisData(dataName, resultData, m_inPoint);
0311     }
0312     if (auto ptr = m_model.lock()) {
0313         qDebug() << "===== SETTING FILTER PARAM: " << params;
0314         QMetaObject::invokeMethod(ptr.get(), "setParametersFromTask", Q_ARG(paramVector, std::move(params)));
0315     }
0316 }
0317 
0318 void FilterTask::processLogInfo()
0319 {
0320     const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
0321     m_logDetails.append(buffer);
0322     // Parse MLT output
0323     if (buffer.contains(QLatin1String("percentage:"))) {
0324         int progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
0325         if (progress == m_progress) {
0326             return;
0327         }
0328         if (auto ptr = m_model.lock()) {
0329             m_progress = progress;
0330             QMetaObject::invokeMethod(ptr.get(), "setProgress", Q_ARG(int, m_progress));
0331         }
0332     }
0333 }