File indexing completed on 2024-05-05 04:54:09

0001 /*
0002     SPDX-FileCopyrightText: 2016 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "previewmanager.h"
0008 #include "bin/projectitemmodel.h"
0009 #include "core.h"
0010 #include "dialogs/wizard.h"
0011 #include "doc/docundostack.hpp"
0012 #include "doc/kdenlivedoc.h"
0013 #include "kdenlivesettings.h"
0014 #include "mainwindow.h"
0015 #include "monitor/monitor.h"
0016 #include "profiles/profilemodel.hpp"
0017 #include "timeline2/view/timelinecontroller.h"
0018 #include "timeline2/view/timelinewidget.h"
0019 #include "xml/xml.hpp"
0020 
0021 #include <KLocalizedString>
0022 #include <KMessageBox>
0023 #include <QCollator>
0024 #include <QCryptographicHash>
0025 #include <QMutexLocker>
0026 #include <QSaveFile>
0027 #include <QStandardPaths>
0028 
0029 PreviewManager::PreviewManager(Mlt::Tractor *tractor, QUuid uuid, QObject *parent)
0030     : QObject(parent)
0031     , workingPreview(-1)
0032     , m_tractor(tractor)
0033     , m_uuid(uuid)
0034     , m_previewTrack(nullptr)
0035     , m_overlayTrack(nullptr)
0036     , m_warnOnCrash(true)
0037     , m_previewTrackIndex(-1)
0038     , m_initialized(false)
0039 {
0040     m_previewGatherTimer.setSingleShot(true);
0041     m_previewGatherTimer.setInterval(200);
0042     QObject::connect(&m_previewProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &PreviewManager::processEnded);
0043 
0044     if (KdenliveSettings::kdenliverendererpath().isEmpty() || !QFileInfo::exists(KdenliveSettings::kdenliverendererpath())) {
0045         KdenliveSettings::setKdenliverendererpath(QString());
0046         Wizard::fixKdenliveRenderPath();
0047         if (KdenliveSettings::kdenliverendererpath().isEmpty()) {
0048             KMessageBox::error(QApplication::activeWindow(),
0049                                i18n("Could not find the kdenlive_render application, something is wrong with your installation. Rendering will not work"));
0050         }
0051     }
0052 
0053     connect(this, &PreviewManager::abortPreview, &m_previewProcess, &QProcess::kill, Qt::DirectConnection);
0054     connect(&m_previewProcess, &QProcess::readyReadStandardError, this, &PreviewManager::receivedStderr);
0055 }
0056 
0057 PreviewManager::~PreviewManager()
0058 {
0059     if (m_initialized) {
0060         abortRendering();
0061         if (m_undoDir.dirName() == QLatin1String("undo")) {
0062             m_undoDir.removeRecursively();
0063         }
0064         if ((pCore->currentDoc()->url().isEmpty() && m_cacheDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty()) ||
0065             m_cacheDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) {
0066             if (m_cacheDir.dirName() == QLatin1String("preview")) {
0067                 m_cacheDir.removeRecursively();
0068             }
0069         }
0070     }
0071     delete m_overlayTrack;
0072     delete m_previewTrack;
0073 }
0074 
0075 bool PreviewManager::initialize()
0076 {
0077     // Make sure our document id does not contain .. tricks
0078     bool ok;
0079     KdenliveDoc *doc = pCore->currentDoc();
0080     QString documentId = QDir::cleanPath(doc->getDocumentProperty(QStringLiteral("documentid")));
0081     documentId.toLongLong(&ok, 10);
0082     if (!ok || documentId.isEmpty()) {
0083         // Something is wrong, documentId should be a number (ms since epoch), abort
0084         pCore->displayMessage(i18n("Wrong document ID, cannot create temporary folder"), ErrorMessage);
0085         return false;
0086     }
0087     m_cacheDir = doc->getCacheDir(CachePreview, &ok, m_uuid);
0088     qDebug() << ":: GOT CACHE DIR: " << m_cacheDir.absolutePath();
0089     if (!ok || !m_cacheDir.exists()) {
0090         pCore->displayMessage(i18n("Cannot read folder %1", m_cacheDir.absolutePath()), ErrorMessage);
0091         return false;
0092     }
0093     if (m_uuid == doc->uuid()) {
0094         if (m_cacheDir.dirName() != QLatin1String("preview") || m_cacheDir == QDir() ||
0095             (!m_cacheDir.exists(QStringLiteral("undo")) && !m_cacheDir.mkdir(QStringLiteral("undo"))) || !m_cacheDir.absolutePath().contains(documentId)) {
0096             pCore->displayMessage(i18n("Something is wrong with cache folder %1", m_cacheDir.absolutePath()), ErrorMessage);
0097             return false;
0098         }
0099     } else {
0100         if (m_cacheDir.dirName().toLatin1() != QCryptographicHash::hash(m_uuid.toByteArray(), QCryptographicHash::Md5).toHex() || m_cacheDir == QDir() ||
0101             (!m_cacheDir.exists(QStringLiteral("undo")) && !m_cacheDir.mkdir(QStringLiteral("undo"))) || !m_cacheDir.absolutePath().contains(documentId)) {
0102             pCore->displayMessage(i18n("Something is wrong with cache folder %1", m_cacheDir.absolutePath()), ErrorMessage);
0103             return false;
0104         }
0105     }
0106     if (!loadParams()) {
0107         pCore->displayMessage(i18n("Invalid timeline preview parameters"), ErrorMessage);
0108         return false;
0109     }
0110     m_undoDir = QDir(m_cacheDir.absoluteFilePath(QStringLiteral("undo")));
0111 
0112     // Make sure our cache dirs are inside the temporary folder
0113     if (!m_cacheDir.makeAbsolute() || !m_undoDir.makeAbsolute() || !m_undoDir.mkpath(QStringLiteral("."))) {
0114         pCore->displayMessage(i18n("Something is wrong with cache folders"), ErrorMessage);
0115         return false;
0116     }
0117 
0118     connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews);
0119     connect(doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo, Qt::DirectConnection);
0120     m_previewTimer.setSingleShot(true);
0121     m_previewTimer.setInterval(3000);
0122     connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender);
0123     connect(this, &PreviewManager::previewRender, this, &PreviewManager::gotPreviewRender, Qt::DirectConnection);
0124     connect(&m_previewGatherTimer, &QTimer::timeout, this, &PreviewManager::slotProcessDirtyChunks);
0125     m_initialized = true;
0126     return true;
0127 }
0128 
0129 bool PreviewManager::buildPreviewTrack()
0130 {
0131     if (m_previewTrack != nullptr) {
0132         return false;
0133     }
0134     // Create overlay track
0135     qDebug() << "/// BUILDING PREVIEW TRACK\n----------------------\n----------------__";
0136     m_previewTrack = new Mlt::Playlist(pCore->getProjectProfile());
0137     m_previewTrack->set("kdenlive:playlistid", "timeline_preview");
0138     m_tractor->lock();
0139     reconnectTrack();
0140     m_tractor->unlock();
0141     return true;
0142 }
0143 
0144 void PreviewManager::loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, Mlt::Playlist &playlist)
0145 {
0146     if (previewChunks.isEmpty()) {
0147         previewChunks = m_renderedChunks;
0148     }
0149     if (dirtyChunks.isEmpty()) {
0150         dirtyChunks = m_dirtyChunks;
0151     }
0152 
0153     QStringList existingChuncks;
0154     if (!previewChunks.isEmpty()) {
0155         existingChuncks = m_cacheDir.entryList(QDir::Files);
0156     }
0157 
0158     int max = playlist.count();
0159     std::shared_ptr<Mlt::Producer> clip;
0160     m_tractor->lock();
0161     if (max == 0) {
0162         // Empty timeline preview, mark all as dirty
0163         for (auto &prev : previewChunks) {
0164             dirtyChunks << prev;
0165         }
0166     }
0167     for (int i = 0; i < max; i++) {
0168         if (playlist.is_blank(i)) {
0169             continue;
0170         }
0171         int position = playlist.clip_start(i);
0172         if (previewChunks.contains(QString::number(position))) {
0173             if (existingChuncks.contains(QString("%1.%2").arg(position).arg(m_extension))) {
0174                 clip.reset(playlist.get_clip(i));
0175                 m_renderedChunks << position;
0176                 m_previewTrack->insert_at(position, clip.get(), 1);
0177             } else {
0178                 dirtyChunks << position;
0179             }
0180         }
0181     }
0182     m_previewTrack->consolidate_blanks();
0183     m_tractor->unlock();
0184     if (!dirtyChunks.isEmpty()) {
0185         std::sort(dirtyChunks.begin(), dirtyChunks.end(), chunkSort);
0186         QMutexLocker lock(&m_dirtyMutex);
0187         for (const auto &i : qAsConst(dirtyChunks)) {
0188             if (!m_dirtyChunks.contains(i)) {
0189                 m_dirtyChunks << i;
0190             }
0191         }
0192         Q_EMIT dirtyChunksChanged();
0193     }
0194     if (!previewChunks.isEmpty()) {
0195         Q_EMIT renderedChunksChanged();
0196     }
0197 }
0198 
0199 void PreviewManager::deletePreviewTrack()
0200 {
0201     m_tractor->lock();
0202     disconnectTrack();
0203     delete m_previewTrack;
0204     m_previewTrack = nullptr;
0205     m_dirtyChunks.clear();
0206     m_renderedChunks.clear();
0207     Q_EMIT dirtyChunksChanged();
0208     Q_EMIT renderedChunksChanged();
0209     m_tractor->unlock();
0210 }
0211 
0212 const QDir PreviewManager::getCacheDir() const
0213 {
0214     return m_cacheDir;
0215 }
0216 
0217 void PreviewManager::reconnectTrack()
0218 {
0219     disconnectTrack();
0220     if (!m_previewTrack && !m_overlayTrack) {
0221         m_previewTrackIndex = -1;
0222         return;
0223     }
0224     m_previewTrackIndex = m_tractor->count();
0225     int increment = 0;
0226     if (m_previewTrack) {
0227         m_tractor->insert_track(*m_previewTrack, m_previewTrackIndex);
0228         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(m_previewTrackIndex));
0229         tk->set("hide", 2);
0230         // tk->set("kdenlive:playlistid", "timeline_preview");
0231         increment++;
0232     }
0233     if (m_overlayTrack) {
0234         m_tractor->insert_track(*m_overlayTrack, m_previewTrackIndex + increment);
0235         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(m_previewTrackIndex + increment));
0236         tk->set("hide", 2);
0237         // tk->set("kdenlive:playlistid", "timeline_overlay");
0238     }
0239 }
0240 
0241 void PreviewManager::disconnectTrack()
0242 {
0243     if (m_previewTrackIndex > -1) {
0244         Mlt::Producer *prod = m_tractor->track(m_previewTrackIndex);
0245         if (strcmp(prod->get("kdenlive:playlistid"), "timeline_preview") == 0 || strcmp(prod->get("kdenlive:playlistid"), "timeline_overlay") == 0) {
0246             m_tractor->remove_track(m_previewTrackIndex);
0247         }
0248         delete prod;
0249         if (m_tractor->count() == m_previewTrackIndex + 1) {
0250             // overlay track still here, remove
0251             Mlt::Producer *trkprod = m_tractor->track(m_previewTrackIndex);
0252             if (strcmp(trkprod->get("kdenlive:playlistid"), "timeline_overlay") == 0) {
0253                 m_tractor->remove_track(m_previewTrackIndex);
0254             }
0255             delete trkprod;
0256         }
0257     }
0258     m_previewTrackIndex = -1;
0259 }
0260 
0261 void PreviewManager::disable()
0262 {
0263     if (m_previewTrackIndex > -1) {
0264         if (m_previewTrack) {
0265             m_previewTrack->set("hide", 3);
0266         }
0267         if (m_overlayTrack) {
0268             m_overlayTrack->set("hide", 3);
0269         }
0270     }
0271 }
0272 
0273 void PreviewManager::enable()
0274 {
0275     if (m_previewTrackIndex > -1) {
0276         if (m_previewTrack) {
0277             m_previewTrack->set("hide", 2);
0278         }
0279         if (m_overlayTrack) {
0280             m_overlayTrack->set("hide", 2);
0281         }
0282     }
0283 }
0284 
0285 bool PreviewManager::loadParams()
0286 {
0287     KdenliveDoc *doc = pCore->currentDoc();
0288     m_extension = doc->getDocumentProperty(QStringLiteral("previewextension"));
0289     m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0290 
0291     if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
0292         doc->selectPreviewProfile();
0293         m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0294         m_extension = doc->getDocumentProperty(QStringLiteral("previewextension"));
0295     }
0296     if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
0297         return false;
0298     }
0299     // Remove the r= and s= parameter (forcing framerate / frame size) as it causes rendering failure.
0300     // These parameters should be provided by MLT's profile
0301     // NOTE: this is still required for DNxHD so leave it
0302     /*for (int i = 0; i < m_consumerParams.count(); i++) {
0303         if (m_consumerParams.at(i).startsWith(QStringLiteral("r=")) || m_consumerParams.at(i).startsWith(QStringLiteral("s="))) {
0304             m_consumerParams.removeAt(i);
0305             i--;
0306         }
0307     }*/
0308     if (doc->getDocumentProperty(QStringLiteral("resizepreview")).toInt() != 0) {
0309         int resizeWidth = doc->getDocumentProperty(QStringLiteral("previewheight")).toInt();
0310         m_consumerParams << QStringLiteral("s=%1x%2").arg(int(resizeWidth * pCore->getCurrentDar())).arg(resizeWidth);
0311     }
0312     m_consumerParams << QStringLiteral("an=1");
0313     if (KdenliveSettings::gpu_accel()) {
0314         m_consumerParams << QStringLiteral("glsl.=1");
0315     }
0316     return true;
0317 }
0318 
0319 void PreviewManager::invalidatePreviews()
0320 {
0321     QMutexLocker lock(&m_previewMutex);
0322     bool timer = KdenliveSettings::autopreview();
0323     if (m_previewTimer.isActive()) {
0324         m_previewTimer.stop();
0325         timer = true;
0326     }
0327     KdenliveDoc *doc = pCore->currentDoc();
0328     int stackIx = doc->commandStack()->index();
0329     int stackMax = doc->commandStack()->count();
0330     if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) {
0331         // We just added a new command in stack, archive existing chunks
0332         int ix = stackIx - 1;
0333         m_undoDir.mkdir(QString::number(ix));
0334         bool foundPreviews = false;
0335         for (const auto &i : m_dirtyChunks) {
0336             QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
0337             if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(ix).arg(current))) {
0338                 foundPreviews = true;
0339             }
0340         }
0341         if (!foundPreviews) {
0342             // No preview files found, remove undo folder
0343             m_undoDir.rmdir(QString::number(ix));
0344         } else {
0345             // new chunks archived, cleanup old ones
0346             Q_EMIT cleanupOldPreviews();
0347         }
0348     } else {
0349         // Restore existing chunks, delete others
0350         // Check if we just undo the last stack action, then backup, otherwise delete
0351         bool lastUndo = false;
0352         if (stackIx + 1 == stackMax) {
0353             if (!m_undoDir.exists(QString::number(stackMax))) {
0354                 lastUndo = true;
0355                 bool foundPreviews = false;
0356                 m_undoDir.mkdir(QString::number(stackMax));
0357                 for (const auto &i : m_dirtyChunks) {
0358                     QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
0359                     if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(stackMax).arg(current))) {
0360                         foundPreviews = true;
0361                     }
0362                 }
0363                 if (!foundPreviews) {
0364                     m_undoDir.rmdir(QString::number(stackMax));
0365                 }
0366             }
0367         }
0368         bool moveFile = true;
0369         QDir tmpDir = m_undoDir;
0370         if (!tmpDir.cd(QString::number(stackIx))) {
0371             moveFile = false;
0372         }
0373         QVariantList foundChunks;
0374         for (const auto &i : m_dirtyChunks) {
0375             QString cacheFileName = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
0376             if (!lastUndo) {
0377                 m_cacheDir.remove(cacheFileName);
0378             }
0379             if (moveFile) {
0380                 if (QFile::copy(tmpDir.absoluteFilePath(cacheFileName), m_cacheDir.absoluteFilePath(cacheFileName))) {
0381                     foundChunks << i;
0382                 } else {
0383                     qDebug() << "// ERROR PROCESSE CHUNK: " << i << ", " << cacheFileName;
0384                 }
0385             }
0386         }
0387         if (!foundChunks.isEmpty()) {
0388             std::sort(foundChunks.begin(), foundChunks.end(), chunkSort);
0389             m_dirtyMutex.lock();
0390             for (auto &ck : foundChunks) {
0391                 m_dirtyChunks.removeAll(ck);
0392                 m_renderedChunks << ck;
0393             }
0394             m_dirtyMutex.unlock();
0395             Q_EMIT dirtyChunksChanged();
0396             Q_EMIT renderedChunksChanged();
0397             reloadChunks(foundChunks);
0398         }
0399     }
0400     doc->setModified(true);
0401     if (timer) {
0402         m_previewTimer.start();
0403     }
0404 }
0405 
0406 void PreviewManager::doCleanupOldPreviews()
0407 {
0408     if (m_undoDir.dirName() != QLatin1String("undo")) {
0409         return;
0410     }
0411     QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0412 
0413     // Use QCollator to do a natural sorting so that 10 is after 2
0414     QCollator collator;
0415     collator.setNumericMode(true);
0416     std::sort(dirs.begin(), dirs.end(), [&collator](const QString &file1, const QString &file2) { return collator.compare(file1, file2) < 0; });
0417     bool ok;
0418     while (dirs.count() > 5) {
0419         QDir tmp = m_undoDir;
0420         QString dirName = dirs.takeFirst();
0421         dirName.toInt(&ok);
0422         if (ok && tmp.cd(dirName)) {
0423             tmp.removeRecursively();
0424         }
0425     }
0426 }
0427 
0428 void PreviewManager::clearPreviewRange(bool resetZones)
0429 {
0430     m_previewGatherTimer.stop();
0431     abortRendering();
0432     m_tractor->lock();
0433     bool hasPreview = m_previewTrack != nullptr;
0434     QMutexLocker lock(&m_dirtyMutex);
0435     for (const auto &ix : qAsConst(m_renderedChunks)) {
0436         m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension));
0437         if (!m_dirtyChunks.contains(ix)) {
0438             m_dirtyChunks << ix;
0439         }
0440         if (!hasPreview) {
0441             continue;
0442         }
0443         int trackIx = m_previewTrack->get_clip_index_at(ix.toInt());
0444         if (!m_previewTrack->is_blank(trackIx)) {
0445             Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx);
0446             delete prod;
0447         }
0448     }
0449     if (hasPreview) {
0450         m_previewTrack->consolidate_blanks();
0451     }
0452     m_tractor->unlock();
0453     m_renderedChunks.clear();
0454     // Reload preview params
0455     loadParams();
0456     if (resetZones) {
0457         m_dirtyChunks.clear();
0458     }
0459     Q_EMIT renderedChunksChanged();
0460     Q_EMIT dirtyChunksChanged();
0461 }
0462 
0463 void PreviewManager::addPreviewRange(const QPoint zone, bool add)
0464 {
0465     int chunkSize = KdenliveSettings::timelinechunks();
0466     int startChunk = zone.x() / chunkSize;
0467     int endChunk = int(rintl(zone.y() / chunkSize));
0468     QList<int> toRemove;
0469     QMutexLocker lock(&m_dirtyMutex);
0470     for (int i = startChunk; i <= endChunk; i++) {
0471         int frame = i * chunkSize;
0472         if (add) {
0473             if (!m_renderedChunks.contains(frame) && !m_dirtyChunks.contains(frame)) {
0474                 m_dirtyChunks << frame;
0475             }
0476         } else {
0477             if (m_renderedChunks.contains(frame)) {
0478                 toRemove << frame;
0479                 m_renderedChunks.removeAll(frame);
0480             } else {
0481                 m_dirtyChunks.removeAll(frame);
0482             }
0483         }
0484     }
0485     if (add) {
0486         Q_EMIT dirtyChunksChanged();
0487         if (m_previewProcess.state() == QProcess::NotRunning && KdenliveSettings::autopreview()) {
0488             m_previewTimer.start();
0489         }
0490     } else {
0491         // Remove processed chunks
0492         bool isRendering = m_previewProcess.state() != QProcess::NotRunning;
0493         m_previewGatherTimer.stop();
0494         abortRendering();
0495         m_tractor->lock();
0496         bool hasPreview = m_previewTrack != nullptr;
0497         for (int ix : qAsConst(toRemove)) {
0498             m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix).arg(m_extension));
0499             if (!hasPreview) {
0500                 continue;
0501             }
0502             int trackIx = m_previewTrack->get_clip_index_at(ix);
0503             if (!m_previewTrack->is_blank(trackIx)) {
0504                 Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx);
0505                 delete prod;
0506             }
0507         }
0508         if (hasPreview) {
0509             m_previewTrack->consolidate_blanks();
0510         }
0511         Q_EMIT renderedChunksChanged();
0512         Q_EMIT dirtyChunksChanged();
0513         m_tractor->unlock();
0514         if (isRendering || KdenliveSettings::autopreview()) {
0515             m_previewTimer.start();
0516         }
0517     }
0518 }
0519 
0520 void PreviewManager::abortRendering()
0521 {
0522     if (m_previewProcess.state() == QProcess::NotRunning) {
0523         return;
0524     }
0525     // Don't display error message on voluntary abort
0526     m_warnOnCrash = false;
0527     Q_EMIT abortPreview();
0528     m_previewProcess.waitForFinished();
0529     if (m_previewProcess.state() != QProcess::NotRunning) {
0530         m_previewProcess.kill();
0531         m_previewProcess.waitForFinished();
0532     }
0533     // Re-init time estimation
0534     Q_EMIT previewRender(-1, QString(), 1000);
0535 }
0536 
0537 bool PreviewManager::hasDefinedRange() const
0538 {
0539     return (!m_renderedChunks.isEmpty() || !m_dirtyChunks.isEmpty());
0540 }
0541 
0542 void PreviewManager::startPreviewRender()
0543 {
0544     QMutexLocker lock(&m_previewMutex);
0545     if (!m_dirtyChunks.isEmpty()) {
0546         // Abort any rendering
0547         abortRendering();
0548         m_waitingThumbs.clear();
0549         // clear log
0550         m_errorLog.clear();
0551         const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt"));
0552         if (!KdenliveSettings::proxypreview() && pCore->currentDoc()->useProxy()) {
0553             const QString playlist =
0554                 pCore->projectItemModel()->sceneList(m_cacheDir.absolutePath(), QString(), QString(), pCore->currentDoc()->getTimeline(m_uuid)->tractor(), -1);
0555             QDomDocument doc;
0556             doc.setContent(playlist);
0557             KdenliveDoc::useOriginals(doc);
0558             if (!Xml::docContentToFile(doc, sceneList)) {
0559                 return;
0560             }
0561         } else {
0562             pCore->currentDoc()->getTimeline(m_uuid)->sceneList(m_cacheDir.absolutePath(), sceneList);
0563         }
0564         m_previewTimer.stop();
0565         doPreviewRender(sceneList);
0566     }
0567 }
0568 
0569 void PreviewManager::receivedStderr()
0570 {
0571     QStringList resultList = QString::fromLocal8Bit(m_previewProcess.readAllStandardError()).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0572     for (auto &result : resultList) {
0573         if (result.startsWith(QLatin1String("START:"))) {
0574             if (m_previewProcess.state() == QProcess::Running) {
0575                 workingPreview = result.section(QLatin1String("START:"), 1).simplified().toInt();
0576                 Q_EMIT workingPreviewChanged();
0577             }
0578         } else if (result.startsWith(QLatin1String("DONE:"))) {
0579             int chunk = result.section(QLatin1String("DONE:"), 1).simplified().toInt();
0580             m_processedChunks++;
0581             QString fileName = QStringLiteral("%1.%2").arg(chunk).arg(m_extension);
0582             Q_EMIT previewRender(chunk, m_cacheDir.absoluteFilePath(fileName), 1000 * m_processedChunks / m_chunksToRender);
0583         } else {
0584             m_errorLog.append(result);
0585         }
0586     }
0587 }
0588 
0589 void PreviewManager::doPreviewRender(const QString &scene)
0590 {
0591     // initialize progress bar
0592     if (m_dirtyChunks.isEmpty()) {
0593         return;
0594     }
0595     QMutexLocker lock(&m_dirtyMutex);
0596     Q_ASSERT(m_previewProcess.state() == QProcess::NotRunning);
0597     std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end(), chunkSort);
0598     const QStringList dirtyChunks = getCompressedList(m_dirtyChunks);
0599     m_chunksToRender = m_dirtyChunks.count();
0600     m_processedChunks = 0;
0601     int chunkSize = KdenliveSettings::timelinechunks();
0602     QStringList args{QStringLiteral("preview-chunks"),
0603                      scene,
0604                      m_cacheDir.absolutePath(),
0605                      dirtyChunks.join(QLatin1Char(',')),
0606                      QString::number(chunkSize - 1),
0607                      pCore->getCurrentProfilePath(),
0608                      m_extension,
0609                      m_consumerParams.join(QLatin1Char(' '))};
0610     pCore->currentDoc()->previewProgress(0);
0611     m_previewProcess.start(KdenliveSettings::kdenliverendererpath(), args);
0612     if (m_previewProcess.waitForStarted()) {
0613         qDebug() << " -  - -STARTING PREVIEW JOBS . . . STARTED: " << args;
0614     }
0615 }
0616 
0617 void PreviewManager::processEnded(int exitCode, QProcess::ExitStatus status)
0618 {
0619     const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt"));
0620     QFile::remove(sceneList);
0621     if (pCore->window() && (status == QProcess::QProcess::CrashExit || exitCode != 0)) {
0622         Q_EMIT previewRender(0, m_errorLog, -1);
0623         if (workingPreview >= 0) {
0624             const QString fileName = QStringLiteral("%1.%2").arg(workingPreview).arg(m_extension);
0625             if (m_cacheDir.exists(fileName)) {
0626                 m_cacheDir.remove(fileName);
0627             }
0628         }
0629     } else {
0630         // Normal exit and exit code 0: everything okay
0631         pCore->currentDoc()->previewProgress(1000);
0632     }
0633     workingPreview = -1;
0634     m_warnOnCrash = true;
0635     Q_EMIT workingPreviewChanged();
0636 }
0637 
0638 void PreviewManager::slotProcessDirtyChunks()
0639 {
0640     if (m_dirtyChunks.isEmpty()) {
0641         return;
0642     }
0643     invalidatePreviews();
0644     if (KdenliveSettings::autopreview()) {
0645         m_previewTimer.start();
0646     }
0647 }
0648 
0649 void PreviewManager::slotRemoveInvalidUndo(int ix)
0650 {
0651     QMutexLocker lock(&m_previewMutex);
0652     if (m_undoDir.dirName() != QLatin1String("undo")) {
0653         // Make sure we delete correct folder
0654         return;
0655     }
0656     QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0657     bool ok;
0658     for (const QString &dir : qAsConst(dirs)) {
0659         if (dir.toInt(&ok) >= ix && ok) {
0660             QDir tmp = m_undoDir;
0661             if (tmp.cd(dir)) {
0662                 tmp.removeRecursively();
0663             }
0664         }
0665     }
0666 }
0667 
0668 void PreviewManager::invalidatePreview(int startFrame, int endFrame)
0669 {
0670     if (m_previewTrack == nullptr) {
0671         return;
0672     }
0673     int chunkSize = KdenliveSettings::timelinechunks();
0674     int start = startFrame - startFrame % chunkSize;
0675     int end = endFrame - endFrame % chunkSize;
0676 
0677     m_previewGatherTimer.stop();
0678     bool previewWasRunning = m_previewProcess.state() == QProcess::Running;
0679     bool alreadyRendered = false;
0680     bool wasInDirtyZone = false;
0681     if (!m_renderedChunks.isEmpty()) {
0682         // Check if the invalidated zone was already rendered
0683         std::sort(m_renderedChunks.begin(), m_renderedChunks.end(), chunkSort);
0684         if (start <= m_renderedChunks.last().toInt() && end >= m_renderedChunks.first().toInt()) {
0685             alreadyRendered = true;
0686         } else if (workingPreview >= start && workingPreview <= end) {
0687             alreadyRendered = true;
0688         }
0689     }
0690     if (!alreadyRendered && !m_dirtyChunks.isEmpty()) {
0691         // Check if the invalidate zone is in the current todo list (dirtychunks)
0692         std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end(), chunkSort);
0693         if (start <= m_dirtyChunks.last().toInt() && end >= m_dirtyChunks.first().toInt()) {
0694             wasInDirtyZone = true;
0695         }
0696     }
0697     if (alreadyRendered) {
0698         if (previewWasRunning) {
0699             abortRendering();
0700         }
0701         m_tractor->lock();
0702         bool chunksChanged = false;
0703         for (int i = start; i <= end; i += chunkSize) {
0704             if (m_renderedChunks.contains(i)) {
0705                 int ix = m_previewTrack->get_clip_index_at(i);
0706                 if (m_previewTrack->is_blank(ix)) {
0707                     continue;
0708                 }
0709                 Mlt::Producer *prod = m_previewTrack->replace_with_blank(ix);
0710                 delete prod;
0711                 QVariant val(i);
0712                 m_renderedChunks.removeAll(val);
0713                 if (!m_dirtyChunks.contains(val)) {
0714                     QMutexLocker lock(&m_dirtyMutex);
0715                     m_dirtyChunks << val;
0716                     chunksChanged = true;
0717                 }
0718             }
0719         }
0720         m_tractor->unlock();
0721         if (chunksChanged) {
0722             m_previewTrack->consolidate_blanks();
0723             Q_EMIT renderedChunksChanged();
0724             Q_EMIT dirtyChunksChanged();
0725         }
0726     } else if (wasInDirtyZone) {
0727         // Abort rendering, playlist needs to be recreated
0728         if (previewWasRunning) {
0729             abortRendering();
0730         }
0731     } else {
0732         // Invalidated zone outside our rendered zones
0733         return;
0734     }
0735     m_previewGatherTimer.start();
0736 }
0737 
0738 void PreviewManager::reloadChunks(const QVariantList &chunks)
0739 {
0740     if (m_previewTrack == nullptr || chunks.isEmpty()) {
0741         return;
0742     }
0743     m_tractor->lock();
0744     for (const auto &ix : chunks) {
0745         if (m_previewTrack->is_blank_at(ix.toInt())) {
0746             QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension));
0747             fileName.prepend(QStringLiteral("avformat:"));
0748             Mlt::Producer prod(pCore->getProjectProfile(), fileName.toUtf8().constData());
0749             if (prod.is_valid()) {
0750                 // m_ruler->updatePreview(ix, true);
0751                 prod.set("mlt_service", "avformat-novalidate");
0752                 m_previewTrack->insert_at(ix.toInt(), &prod, 1);
0753             }
0754         }
0755     }
0756     m_previewTrack->consolidate_blanks();
0757     m_tractor->unlock();
0758 }
0759 
0760 void PreviewManager::gotPreviewRender(int frame, const QString &file, int progress)
0761 {
0762     if (m_previewTrack == nullptr) {
0763         return;
0764     }
0765     if (frame < 0) {
0766         pCore->currentDoc()->previewProgress(1000);
0767         return;
0768     }
0769     if (file.isEmpty() || progress < 0) {
0770         pCore->currentDoc()->previewProgress(progress);
0771         if (progress < 0) {
0772             if (m_warnOnCrash) {
0773                 pCore->displayMessage(i18n("Preview rendering failed, check your parameters. %1Show details...%2",
0774                                            QString("<a href=\"" + QString::fromLatin1(QUrl::toPercentEncoding(file)) + QStringLiteral("\">")),
0775                                            QStringLiteral("</a>")),
0776                                       MltError);
0777             } else {
0778                 // TODO display info about stopped preview job
0779             }
0780         }
0781         return;
0782     }
0783     if (m_previewTrack->is_blank_at(frame)) {
0784         Mlt::Producer prod(pCore->getProjectProfile(), QString("avformat:%1").arg(file).toUtf8().constData());
0785         if (prod.is_valid() && prod.get_length() == KdenliveSettings::timelinechunks()) {
0786             m_dirtyMutex.lock();
0787             m_dirtyChunks.removeAll(QVariant(frame));
0788             m_dirtyMutex.unlock();
0789             m_renderedChunks << frame;
0790             Q_EMIT renderedChunksChanged();
0791             prod.set("mlt_service", "avformat-novalidate");
0792             m_tractor->lock();
0793             m_previewTrack->insert_at(frame, &prod, 1);
0794             m_previewTrack->consolidate_blanks();
0795             m_tractor->unlock();
0796             pCore->currentDoc()->previewProgress(progress);
0797             pCore->currentDoc()->setModified(true);
0798         } else {
0799             qCDebug(KDENLIVE_LOG) << "* * * INVALID PROD: " << file;
0800             corruptedChunk(frame, file);
0801         }
0802     } else {
0803         qCDebug(KDENLIVE_LOG) << "* * * NON EMPTY PROD: " << frame;
0804     }
0805 }
0806 
0807 void PreviewManager::corruptedChunk(int frame, const QString &fileName)
0808 {
0809     Q_EMIT abortPreview();
0810     m_previewProcess.waitForFinished();
0811     if (workingPreview >= 0) {
0812         workingPreview = -1;
0813         Q_EMIT workingPreviewChanged();
0814     }
0815     Q_EMIT previewRender(0, m_errorLog, -1);
0816     m_cacheDir.remove(fileName);
0817     if (!m_dirtyChunks.contains(frame)) {
0818         QMutexLocker lock(&m_dirtyMutex);
0819         m_dirtyChunks << frame;
0820         std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end(), chunkSort);
0821     }
0822 }
0823 
0824 int PreviewManager::setOverlayTrack(Mlt::Playlist *overlay)
0825 {
0826     m_overlayTrack = overlay;
0827     m_overlayTrack->set("kdenlive:playlistid", "timeline_overlay");
0828     reconnectTrack();
0829     return m_previewTrackIndex;
0830 }
0831 
0832 void PreviewManager::removeOverlayTrack()
0833 {
0834     delete m_overlayTrack;
0835     m_overlayTrack = nullptr;
0836     reconnectTrack();
0837 }
0838 
0839 QPair<QStringList, QStringList> PreviewManager::previewChunks()
0840 {
0841     QMutexLocker lock(&m_dirtyMutex);
0842     std::sort(m_renderedChunks.begin(), m_renderedChunks.end(), chunkSort);
0843     const QStringList renderedChunks = getCompressedList(m_renderedChunks);
0844     std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end(), chunkSort);
0845     const QStringList dirtyChunks = getCompressedList(m_dirtyChunks);
0846     lock.unlock();
0847     return {renderedChunks, dirtyChunks};
0848 }
0849 
0850 const QStringList PreviewManager::getCompressedList(const QVariantList items) const
0851 {
0852     QStringList resultString;
0853     int lastFrame = -1;
0854     QString currentString;
0855     for (const QVariant &frame : items) {
0856         int current = frame.toInt();
0857         if (current - KdenliveSettings::timelinechunks() == lastFrame) {
0858             lastFrame = current;
0859             if (frame == items.last()) {
0860                 currentString.append(QString("-%1").arg(lastFrame));
0861                 resultString << currentString;
0862                 currentString.clear();
0863             }
0864             continue;
0865         }
0866         if (currentString.isEmpty()) {
0867             currentString = frame.toString();
0868         } else if (currentString == QString::number(lastFrame)) {
0869             // Only one chunk, store it
0870             resultString << currentString;
0871             currentString = frame.toString();
0872         } else {
0873             // Range, store
0874             currentString.append(QString("-%1").arg(lastFrame));
0875             resultString << currentString;
0876             currentString = frame.toString();
0877         }
0878         lastFrame = current;
0879     }
0880     if (!currentString.isEmpty()) {
0881         resultString << currentString;
0882     }
0883     return resultString;
0884 }
0885 
0886 bool PreviewManager::hasOverlayTrack() const
0887 {
0888     return m_overlayTrack != nullptr;
0889 }
0890 
0891 bool PreviewManager::hasPreviewTrack() const
0892 {
0893     return m_previewTrack != nullptr;
0894 }
0895 
0896 int PreviewManager::addedTracks() const
0897 {
0898     if (m_previewTrack) {
0899         if (m_overlayTrack) {
0900             return 2;
0901         }
0902         return 1;
0903     } else if (m_overlayTrack) {
0904         return 1;
0905     }
0906     return -1;
0907 }
0908 
0909 bool PreviewManager::isRunning() const
0910 {
0911     return workingPreview >= 0 || m_previewProcess.state() != QProcess::NotRunning;
0912 }