File indexing completed on 2024-12-01 04:28:37

0001 /*
0002     SPDX-FileCopyrightText: 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "speedtask.h"
0008 #include "bin/bin.h"
0009 #include "bin/projectclip.h"
0010 #include "bin/projectitemmodel.h"
0011 #include "core.h"
0012 #include "kdenlive_debug.h"
0013 #include "kdenlivesettings.h"
0014 #include "macros.hpp"
0015 #include "mainwindow.h"
0016 #include "xml/xml.hpp"
0017 
0018 #include <KIO/RenameDialog>
0019 #include <KLineEdit>
0020 #include <KLocalizedString>
0021 #include <KUrlRequester>
0022 #include <QApplication>
0023 #include <QCheckBox>
0024 #include <QDialogButtonBox>
0025 #include <QDoubleSpinBox>
0026 #include <QProcess>
0027 #include <QThread>
0028 #include <QVBoxLayout>
0029 
0030 SpeedTask::SpeedTask(const ObjectId &owner, const QString &destination, int in, int out, std::unordered_map<QString, QVariant> filterParams, QObject *object)
0031     : AbstractTask(owner, AbstractTask::SPEEDJOB, object)
0032     , m_filterParams(filterParams)
0033     , m_destination(destination)
0034 {
0035     m_description = i18n("Changing speed");
0036     m_speed = filterParams.at(QStringLiteral("warp_speed")).toDouble();
0037     m_inPoint = in > -1 ? qRound(in / m_speed) : -1;
0038     m_outPoint = out > -1 ? qRound(out / m_speed) : -1;
0039 }
0040 
0041 void SpeedTask::start(QObject *object, bool force)
0042 {
0043     Q_UNUSED(object)
0044     std::vector<QString> binIds = pCore->bin()->selectedClipsIds(true);
0045     // Show config dialog
0046     QDialog d(qApp->activeWindow());
0047     d.setWindowTitle(i18nc("@title:window", "Clip Speed"));
0048     QDialogButtonBox buttonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Save);
0049     auto *l = new QVBoxLayout;
0050     auto *l2 = new QHBoxLayout;
0051     d.setLayout(l);
0052     QLabel labUrl(&d);
0053     KUrlRequester fileUrl(&d);
0054     auto binClip = pCore->projectItemModel()->getClipByBinID(binIds.front().section(QLatin1Char('/'), 0, 0));
0055     QDir folder = QFileInfo(binClip->url()).absoluteDir();
0056     folder.mkpath(i18n("Speed Change"));
0057     folder.cd(i18n("Speed Change"));
0058     if (binIds.size() > 1) {
0059         labUrl.setText(i18n("Destination Folder"));
0060         fileUrl.setMode(KFile::Directory);
0061         fileUrl.setUrl(QUrl::fromLocalFile(folder.absolutePath()));
0062     } else {
0063         labUrl.setText(i18n("Destination File"));
0064         fileUrl.setMode(KFile::File);
0065         QString filePath = QFileInfo(binClip->url()).fileName().section(QLatin1Char('.'), 0, -2);
0066         filePath.append(QStringLiteral(".mlt"));
0067         fileUrl.setUrl(QUrl::fromLocalFile(folder.absoluteFilePath(filePath)));
0068     }
0069     QFontMetrics fm = fileUrl.lineEdit()->fontMetrics();
0070     fileUrl.setMinimumWidth(int(fm.boundingRect(fileUrl.text().left(50)).width() * 1.4));
0071     QLabel lab(&d);
0072     lab.setText(i18n("Percentage"));
0073     QDoubleSpinBox speedInput(&d);
0074     speedInput.setRange(-100000, 100000);
0075     speedInput.setValue(100);
0076     speedInput.setSuffix(QLatin1String("%"));
0077     speedInput.setFocus();
0078     speedInput.selectAll();
0079     QCheckBox cb(i18n("Pitch compensation"), &d);
0080     cb.setChecked(true);
0081     QToolButton tb(&d);
0082     tb.setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0083     connect(&tb, &QToolButton::clicked, &d, [&]() { pCore->window()->manageClipJobs(AbstractTask::SPEEDJOB, &d); });
0084     l->addWidget(&labUrl);
0085     l->addWidget(&fileUrl);
0086     l->addWidget(&lab);
0087     l->addWidget(&speedInput);
0088     l->addWidget(&cb);
0089     l2->addWidget(&tb);
0090     l2->addStretch(10);
0091     l2->addWidget(&buttonBox);
0092     l->addLayout(l2);
0093     d.connect(&buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject);
0094     d.connect(&buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept);
0095     if (d.exec() != QDialog::Accepted) {
0096         return;
0097     }
0098     double speed = speedInput.value();
0099     bool warp_pitch = cb.isChecked();
0100     std::unordered_map<QString, QString> destinations; // keys are binIds, values are path to target files
0101     std::unordered_map<QString, QVariant> filterParams;
0102     filterParams[QStringLiteral("warp_speed")] = speed / 100.0;
0103     if (warp_pitch) {
0104         filterParams[QStringLiteral("warp_pitch")] = 1;
0105     }
0106     for (const auto &binId : binIds) {
0107         QString mltfile;
0108         if (binIds.size() == 1) {
0109             // converting only 1 clip
0110             mltfile = fileUrl.url().toLocalFile();
0111         } else {
0112             QDir dir(fileUrl.url().toLocalFile());
0113             binClip = pCore->projectItemModel()->getClipByBinID(binId.section(QLatin1Char('/'), 0, 0));
0114             mltfile = QFileInfo(binClip->url()).fileName().section(QLatin1Char('.'), 0, -2);
0115             mltfile.append(QString("-%1.mlt").arg(QString::number(int(speed))));
0116             mltfile = dir.absoluteFilePath(mltfile);
0117         }
0118         // Filter several clips, destination points to a folder
0119         if (QFile::exists(mltfile)) {
0120             KIO::RenameDialog renameDialog(qApp->activeWindow(), i18n("File already exists"), QUrl::fromLocalFile(mltfile), QUrl::fromLocalFile(mltfile),
0121                                            KIO::RenameDialog_Option::RenameDialog_Overwrite);
0122             if (renameDialog.exec() != QDialog::Rejected) {
0123                 QUrl final = renameDialog.newDestUrl();
0124                 if (final.isValid()) {
0125                     mltfile = final.toLocalFile();
0126                 }
0127             } else {
0128                 return;
0129             }
0130         }
0131         destinations[binId] = mltfile;
0132     }
0133 
0134     for (auto &id : binIds) {
0135         SpeedTask *task = nullptr;
0136         ObjectId owner;
0137         if (id.contains(QLatin1Char('/'))) {
0138             QStringList binData = id.split(QLatin1Char('/'));
0139             if (binData.size() < 3) {
0140                 // Invalid subclip data
0141                 qDebug() << "=== INVALID SUBCLIP DATA: " << id;
0142                 continue;
0143             }
0144             owner = ObjectId(KdenliveObjectType::BinClip, binData.first().toInt(), QUuid());
0145             binClip = pCore->projectItemModel()->getClipByBinID(binData.first());
0146             if (binClip) {
0147                 task = new SpeedTask(owner, destinations.at(id), binData.at(1).toInt(), binData.at(2).toInt(), filterParams, binClip.get());
0148             }
0149         } else {
0150             // Process full clip
0151             owner = ObjectId(KdenliveObjectType::BinClip, id.toInt(), QUuid());
0152             binClip = pCore->projectItemModel()->getClipByBinID(id);
0153             if (binClip) {
0154                 task = new SpeedTask(owner, destinations.at(id), -1, -1, filterParams, binClip.get());
0155             }
0156         }
0157         if (task) {
0158             // Otherwise, start a filter thread.
0159             task->m_isForce = force;
0160             pCore->taskManager.startTask(owner.itemId, task);
0161         }
0162     }
0163 }
0164 
0165 void SpeedTask::run()
0166 {
0167     AbstractTaskDone whenFinished(m_owner.itemId, this);
0168     if (m_isCanceled || pCore->taskManager.isBlocked()) {
0169         return;
0170     }
0171     QMutexLocker lock(&m_runMutex);
0172     m_running = true;
0173     qDebug() << " + + + + + + + + STARTING SPEED TASK";
0174 
0175     QString url;
0176     auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId));
0177     QStringList producerArgs = {QStringLiteral("progress=1"), QStringLiteral("-profile"), pCore->getCurrentProfilePath()};
0178     QString folderId = QLatin1String("-1");
0179     if (binClip) {
0180         folderId = binClip->parent()->clipId();
0181         // Filter applied on a timeline or bin clip
0182         url = binClip->url();
0183         if (url.isEmpty()) {
0184             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("No producer for this clip.")),
0185                                       Q_ARG(int, int(KMessageWidget::Warning)));
0186             return;
0187         }
0188         producerArgs << QString("timewarp:%1:%2").arg(m_speed).arg(url);
0189         if (m_inPoint > -1) {
0190             producerArgs << QString("in=%1").arg(m_inPoint);
0191         }
0192         if (m_outPoint > -1) {
0193             producerArgs << QString("out=%1").arg(m_outPoint);
0194         }
0195     } else {
0196         QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("No producer for this clip.")),
0197                                   Q_ARG(int, int(KMessageWidget::Warning)));
0198         return;
0199         // Filter applied on a track of master producer, leave config to source job
0200         // We are on master or track, configure producer accordingly
0201         // TODO
0202         /*if (m_owner.type == KdenliveObjectType::Master) {
0203             producer = pCore->getMasterProducerInstance();
0204         } else if (m_owner.type == KdenliveObjectType::TimelineTrack) {
0205             producer = pCore->getTrackProducerInstance(m_owner.second);
0206         }
0207         if ((producer == nullptr) || !producer->is_valid()) {
0208             // Clip was removed or something went wrong, Notify user?
0209             m_errorMessage.append(i18n("Invalid clip"));
0210             return;
0211         }*/
0212     }
0213 
0214     // Process filter params
0215     for (const auto &it : m_filterParams) {
0216         qDebug() << ". . ." << it.first << " = " << it.second;
0217 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0218         if (it.second.type() == QVariant::Double) {
0219 #else
0220         if (it.second.typeId() == QMetaType::Double) {
0221 #endif
0222             producerArgs << QString("%1=%2").arg(it.first, QString::number(it.second.toDouble()));
0223         } else {
0224             producerArgs << QString("%1=%2").arg(it.first, it.second.toString());
0225         }
0226     }
0227 
0228     // Start the MLT Process
0229     QProcess filterProcess;
0230     producerArgs << QStringLiteral("-consumer") << QString("xml:%1").arg(m_destination) << QStringLiteral("terminate_on_pause=1");
0231     m_jobProcess.reset(new QProcess);
0232     QMetaObject::invokeMethod(m_object, "updateJobProgress");
0233     QObject::connect(this, &AbstractTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0234     QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &SpeedTask::processLogInfo);
0235     qDebug() << "=== STARTING PROCESS: " << producerArgs;
0236     m_jobProcess->start(KdenliveSettings::meltpath(), producerArgs);
0237     m_jobProcess->waitForFinished(-1);
0238     qDebug() << " + + + + + + + + SOURCE FILE PROCESSED: " << m_jobProcess->exitStatus();
0239     bool result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0240     m_progress = 100;
0241     QMetaObject::invokeMethod(m_object, "updateJobProgress");
0242     if (m_isCanceled || !result) {
0243         if (!m_isCanceled) {
0244             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create speed clip.")),
0245                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0246         }
0247         return;
0248     }
0249     QMetaObject::invokeMethod(pCore->bin(), "addProjectClipInFolder", Qt::QueuedConnection, Q_ARG(QString, m_destination),
0250                               Q_ARG(QString, QString::number(m_owner.itemId)), Q_ARG(QString, folderId), Q_ARG(QString, QStringLiteral("timewarp")));
0251     return;
0252 }
0253 
0254 void SpeedTask::processLogInfo()
0255 {
0256     const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
0257     m_logDetails.append(buffer);
0258     // Parse MLT output
0259     if (buffer.contains(QLatin1String("percentage:"))) {
0260         int progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
0261         if (progress == m_progress) {
0262             return;
0263         }
0264         m_progress = progress;
0265         QMetaObject::invokeMethod(m_object, "updateJobProgress");
0266     }
0267 }