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 "scenesplittask.h" 0008 #include "bin/bin.h" 0009 #include "bin/clipcreator.hpp" 0010 #include "bin/model/markerlistmodel.hpp" 0011 #include "bin/projectclip.h" 0012 #include "bin/projectfolder.h" 0013 #include "bin/projectitemmodel.h" 0014 #include "core.h" 0015 #include "doc/kdenlivedoc.h" 0016 #include "kdenlive_debug.h" 0017 #include "kdenlivesettings.h" 0018 #include "macros.hpp" 0019 #include "mainwindow.h" 0020 #include "ui_scenecutdialog_ui.h" 0021 0022 #include <QJsonArray> 0023 #include <QJsonDocument> 0024 #include <QJsonObject> 0025 #include <QPointer> 0026 #include <QProcess> 0027 #include <QTemporaryFile> 0028 #include <QThread> 0029 0030 #include <KLocalizedString> 0031 #include <project/projectmanager.h> 0032 0033 SceneSplitTask::SceneSplitTask(const ObjectId &owner, double threshold, int markersCategory, bool addSubclips, int minDuration, QObject *object) 0034 : AbstractTask(owner, AbstractTask::ANALYSECLIPJOB, object) 0035 , m_threshold(threshold) 0036 , m_jobDuration(0) 0037 , m_markersType(markersCategory) 0038 , m_subClips(addSubclips) 0039 , m_minInterval(minDuration) 0040 , m_jobProcess(nullptr) 0041 { 0042 m_description = i18n("Detecting scene change"); 0043 qDebug() << "Threshold is" << threshold << QString::number(threshold); 0044 } 0045 0046 void SceneSplitTask::start(QObject *object, bool force) 0047 { 0048 Q_UNUSED(object) 0049 QPointer<QDialog> d = new QDialog; 0050 Ui::SceneCutDialog_UI view; 0051 view.setupUi(d); 0052 view.threshold->setValue(KdenliveSettings::scenesplitthreshold()); 0053 view.add_markers->setChecked(KdenliveSettings::scenesplitmarkers()); 0054 view.cut_scenes->setChecked(KdenliveSettings::scenesplitsubclips()); 0055 // Set up categories 0056 view.marker_category->setMarkerModel(pCore->projectManager()->getGuideModel().get()); 0057 d->setWindowTitle(i18nc("@title:window", "Scene Detection")); 0058 if (d->exec() != QDialog::Accepted) { 0059 return; 0060 } 0061 int threshold = view.threshold->value(); 0062 bool addMarkers = view.add_markers->isChecked(); 0063 bool addSubclips = view.cut_scenes->isChecked(); 0064 int markersCategory = addMarkers ? view.marker_category->currentCategory() : -1; 0065 int minDuration = view.minDuration->value(); 0066 KdenliveSettings::setScenesplitthreshold(threshold); 0067 KdenliveSettings::setScenesplitmarkers(view.add_markers->isChecked()); 0068 KdenliveSettings::setScenesplitsubclips(view.cut_scenes->isChecked()); 0069 0070 std::vector<QString> binIds = pCore->bin()->selectedClipsIds(true); 0071 for (auto &id : binIds) { 0072 SceneSplitTask *task = nullptr; 0073 ObjectId owner; 0074 if (id.contains(QLatin1Char('/'))) { 0075 QStringList binData = id.split(QLatin1Char('/')); 0076 if (binData.size() < 3) { 0077 // Invalid subclip data 0078 qDebug() << "=== INVALID SUBCLIP DATA: " << id; 0079 continue; 0080 } 0081 owner = ObjectId(KdenliveObjectType::BinClip, binData.first().toInt(), QUuid()); 0082 auto binClip = pCore->projectItemModel()->getClipByBinID(binData.first()); 0083 task = new SceneSplitTask(owner, threshold / 100., markersCategory, addSubclips, minDuration, binClip.get()); 0084 0085 } else { 0086 owner = ObjectId(KdenliveObjectType::BinClip, id.toInt(), QUuid()); 0087 auto binClip = pCore->projectItemModel()->getClipByBinID(id); 0088 task = new SceneSplitTask(owner, threshold / 100., markersCategory, addSubclips, minDuration, binClip.get()); 0089 } 0090 // See if there is already a task for this MLT service and resource. 0091 if (task && pCore->taskManager.hasPendingJob(owner, AbstractTask::ANALYSECLIPJOB)) { 0092 delete task; 0093 task = nullptr; 0094 } 0095 if (task) { 0096 // Otherwise, start a new audio levels generation thread. 0097 task->m_isForce = force; 0098 pCore->taskManager.startTask(owner.itemId, task); 0099 } 0100 } 0101 } 0102 0103 void SceneSplitTask::run() 0104 { 0105 AbstractTaskDone whenFinished(m_owner.itemId, this); 0106 if (m_isCanceled || pCore->taskManager.isBlocked()) { 0107 return; 0108 } 0109 QMutexLocker lock(&m_runMutex); 0110 m_running = true; 0111 auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId)); 0112 const QString source = binClip->url(); 0113 ClipType::ProducerType type = binClip->clipType(); 0114 bool result; 0115 if (type != ClipType::AV && type != ClipType::Video) { 0116 // This job can only process video files 0117 QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cannot analyse this clip type.")), 0118 Q_ARG(int, int(KMessageWidget::Warning))); 0119 qDebug() << "=== ABORT 1"; 0120 return; 0121 } 0122 if (KdenliveSettings::ffmpegpath().isEmpty()) { 0123 // FFmpeg not detected, cannot process the Job 0124 QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, 0125 Q_ARG(QString, i18n("FFmpeg not found, please set path in Kdenlive's settings Environment.")), 0126 Q_ARG(int, int(KMessageWidget::Warning))); 0127 qDebug() << "=== ABORT 2"; 0128 return; 0129 } 0130 m_jobDuration = int(binClip->duration().seconds()); 0131 int producerDuration = binClip->frameDuration(); 0132 // QStringList parameters = 0133 // {QStringLiteral("-loglevel"),QStringLiteral("info"),QStringLiteral("-i"),source,QStringLiteral("-filter:v"),QString("scdet"),QStringLiteral("-f"),QStringLiteral("null"),QStringLiteral("-")}; 0134 QStringList parameters = {QStringLiteral("-y"), 0135 QStringLiteral("-loglevel"), 0136 QStringLiteral("info"), 0137 QStringLiteral("-i"), 0138 source, 0139 QStringLiteral("-filter:v"), 0140 QString("select='gt(scene,%1)',showinfo").arg(m_threshold), 0141 QStringLiteral("-vsync"), 0142 QStringLiteral("vfr"), 0143 QStringLiteral("-f"), 0144 QStringLiteral("null"), 0145 QStringLiteral("-")}; 0146 0147 m_jobProcess.reset(new QProcess); 0148 // m_jobProcess->setStandardErrorFile("/tmp/test_settings.txt"); 0149 m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); 0150 qDebug() << "=== READY TO START JOB:" << parameters; 0151 QObject::connect(this, &SceneSplitTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection); 0152 QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardOutput, this, &SceneSplitTask::processLogInfo); 0153 QObject::connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &SceneSplitTask::processLogErr); 0154 m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters); 0155 // m_jobProcess->closeReadChannel(QProcess::StandardError); 0156 m_jobProcess->waitForStarted(); 0157 // QString data; 0158 /*while(m_jobProcess->waitForReadyRead()) { 0159 //data.append(m_jobProcess->readAll()); 0160 qDebug()<<"???? READ: \n"<<m_jobProcess->readAll(); 0161 }*/ 0162 m_jobProcess->waitForFinished(-1); 0163 result = m_jobProcess->exitStatus() == QProcess::NormalExit; 0164 0165 // remove temporary playlist if it exists 0166 m_progress = 100; 0167 QMetaObject::invokeMethod(m_object, "updateJobProgress"); 0168 if (result && !m_isCanceled) { 0169 qDebug() << "========================\n\nGOR RESULTS: " << m_results << "\n\n========="; 0170 auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId)); 0171 if (m_markersType >= 0) { 0172 // Build json data for markers 0173 QJsonArray list; 0174 int ix = 1; 0175 int lastCut = 0; 0176 for (auto &marker : m_results) { 0177 int pos = GenTime(marker).frames(pCore->getCurrentFps()); 0178 if (m_minInterval > 0 && ix > 1 && pos - lastCut < m_minInterval) { 0179 continue; 0180 } 0181 lastCut = pos; 0182 QJsonObject currentMarker; 0183 currentMarker.insert(QLatin1String("pos"), QJsonValue(pos)); 0184 currentMarker.insert(QLatin1String("comment"), QJsonValue(i18n("Scene %1", ix))); 0185 currentMarker.insert(QLatin1String("type"), QJsonValue(m_markersType)); 0186 list.push_back(currentMarker); 0187 ix++; 0188 } 0189 QJsonDocument json(list); 0190 QMetaObject::invokeMethod(m_object, "importJsonMarkers", Q_ARG(QString, QString(json.toJson()))); 0191 } 0192 if (m_subClips) { 0193 // Create zones 0194 int ix = 1; 0195 int lastCut = 0; 0196 QJsonArray list; 0197 QJsonDocument json; 0198 for (double &marker : m_results) { 0199 int pos = GenTime(marker).frames(pCore->getCurrentFps()); 0200 if (pos <= lastCut + 1 || pos - lastCut < m_minInterval) { 0201 continue; 0202 } 0203 QJsonObject currentZone; 0204 currentZone.insert(QLatin1String("name"), QJsonValue(i18n("Scene %1", ix))); 0205 currentZone.insert(QLatin1String("in"), QJsonValue(lastCut)); 0206 currentZone.insert(QLatin1String("out"), QJsonValue(pos - 1)); 0207 list.push_back(currentZone); 0208 lastCut = pos; 0209 ix++; 0210 } 0211 if (lastCut < producerDuration) { 0212 QJsonObject currentZone; 0213 currentZone.insert(QLatin1String("name"), QJsonValue(i18n("Scene %1", ix))); 0214 currentZone.insert(QLatin1String("in"), QJsonValue(lastCut)); 0215 currentZone.insert(QLatin1String("out"), QJsonValue(producerDuration)); 0216 list.push_back(currentZone); 0217 } 0218 json.setArray(list); 0219 if (!json.isEmpty()) { 0220 QString dataMap(json.toJson()); 0221 QMetaObject::invokeMethod(pCore->projectItemModel().get(), "loadSubClips", Q_ARG(QString, QString::number(m_owner.itemId)), 0222 Q_ARG(QString, dataMap), Q_ARG(bool, true)); 0223 } 0224 } 0225 } else { 0226 // Proxy process crashed 0227 QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to analyse clip.")), 0228 Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails)); 0229 } 0230 } 0231 0232 void SceneSplitTask::processLogErr() 0233 { 0234 const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError()); 0235 qDebug() << "ERROR: ----\n" << buffer; 0236 } 0237 0238 void SceneSplitTask::processLogInfo() 0239 { 0240 const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardOutput()); 0241 m_logDetails.append(buffer); 0242 int progress = 0; 0243 // Parse FFmpeg output 0244 qDebug() << "-------------\n" << buffer; 0245 if (buffer.contains(QLatin1String("[Parsed_showinfo"))) { 0246 QString timeMarker("pts_time:"); 0247 bool ok; 0248 QStringList output = buffer.split("[Parsed_showinfo"); 0249 output.removeFirst(); 0250 for (const QString &o : qAsConst(output)) { 0251 if (o.contains(timeMarker)) { 0252 double res = o.section(timeMarker, 1).section(QLatin1Char(' '), 0, 0).toDouble(&ok); 0253 if (ok) { 0254 m_results << res; 0255 } 0256 } 0257 } 0258 } 0259 if (m_jobDuration == 0) { 0260 qDebug() << "=== NO DURATION!!!"; 0261 if (buffer.contains(QLatin1String("Duration:"))) { 0262 QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); 0263 if (!data.isEmpty()) { 0264 qDebug() << "==== GOT DURATION:" << data; 0265 QStringList numbers = data.split(QLatin1Char(':')); 0266 if (numbers.size() < 3) { 0267 return; 0268 } 0269 m_jobDuration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toInt(); 0270 } 0271 } 0272 } else if (buffer.contains(QLatin1String("time="))) { 0273 QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); 0274 qDebug() << "=== GOT PROGRESS TIME: " << time; 0275 if (!time.isEmpty()) { 0276 QStringList numbers = time.split(QLatin1Char(':')); 0277 if (numbers.size() < 3) { 0278 progress = time.toInt(); 0279 if (progress == 0) { 0280 return; 0281 } 0282 } else { 0283 progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + qRound(numbers.at(2).toDouble()); 0284 } 0285 } 0286 m_progress = 100 * progress / m_jobDuration; 0287 QMetaObject::invokeMethod(m_object, "updateJobProgress"); 0288 // Q_EMIT jobProgress(int(100.0 * progress / m_jobDuration)); 0289 } 0290 }