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 }