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 }