1 /*
2     SPDX-FileCopyrightText: 2016 Jean-Baptiste Mardelle <jb@kdenlive.org>
3 
4 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 
7 #include "previewmanager.h"
8 #include "core.h"
9 #include "doc/docundostack.hpp"
10 #include "doc/kdenlivedoc.h"
11 #include "kdenlivesettings.h"
12 #include "monitor/monitor.h"
13 #include "profiles/profilemodel.hpp"
14 #include "timeline2/view/timelinecontroller.h"
15 
16 #include <KLocalizedString>
17 #include <KMessageBox>
18 #include <QCollator>
19 #include <QProcess>
20 #include <QMutexLocker>
21 #include <QStandardPaths>
22 #include <QCollator>
23 
PreviewManager(TimelineController * controller,Mlt::Tractor * tractor)24 PreviewManager::PreviewManager(TimelineController *controller, Mlt::Tractor *tractor)
25     : QObject()
26     , workingPreview(-1)
27     , m_controller(controller)
28     , m_tractor(tractor)
29     , m_previewTrack(nullptr)
30     , m_overlayTrack(nullptr)
31     , m_previewTrackIndex(-1)
32     , m_initialized(false)
33 {
34     m_previewGatherTimer.setSingleShot(true);
35     m_previewGatherTimer.setInterval(200);
36     QObject::connect(&m_previewProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &PreviewManager::processEnded);
37 
38 
39     // Find path for Kdenlive renderer
40 #ifdef Q_OS_WIN
41     m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe");
42 #else
43     m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render");
44 #endif
45     if (!QFile::exists(m_renderer)) {
46         m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render"));
47         if (m_renderer.isEmpty()) {
48             KMessageBox::sorry(qApp->activeWindow(), i18n("Could not find the kdenlive_render application, something is wrong with your installation. Rendering will not work"));
49         }
50     }
51     connect(this, &PreviewManager::abortPreview, &m_previewProcess, &QProcess::kill, Qt::DirectConnection);
52     connect(&m_previewProcess, &QProcess::readyReadStandardError, this, &PreviewManager::receivedStderr);
53 }
54 
~PreviewManager()55 PreviewManager::~PreviewManager()
56 {
57     if (m_initialized) {
58         abortRendering();
59         if (m_undoDir.dirName() == QLatin1String("undo")) {
60             m_undoDir.removeRecursively();
61         }
62         if ((pCore->currentDoc()->url().isEmpty() && m_cacheDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty()) ||
63             m_cacheDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) {
64             if (m_cacheDir.dirName() == QLatin1String("preview")) {
65                 m_cacheDir.removeRecursively();
66             }
67         }
68     }
69     delete m_overlayTrack;
70     delete m_previewTrack;
71 }
72 
initialize()73 bool PreviewManager::initialize()
74 {
75     // Make sure our document id does not contain .. tricks
76     bool ok;
77     KdenliveDoc *doc = pCore->currentDoc();
78     QString documentId = QDir::cleanPath(doc->getDocumentProperty(QStringLiteral("documentid")));
79     documentId.toLongLong(&ok, 10);
80     if (!ok || documentId.isEmpty()) {
81         // Something is wrong, documentId should be a number (ms since epoch), abort
82         pCore->displayMessage(i18n("Wrong document ID, cannot create temporary folder"), ErrorMessage);
83         return false;
84     }
85     m_cacheDir = doc->getCacheDir(CachePreview, &ok);
86     if (!m_cacheDir.exists() || !ok) {
87         pCore->displayMessage(i18n("Cannot create folder %1", m_cacheDir.absolutePath()), ErrorMessage);
88         return false;
89     }
90     if (m_cacheDir.dirName() != QLatin1String("preview") || m_cacheDir == QDir() ||
91         (!m_cacheDir.exists(QStringLiteral("undo")) && !m_cacheDir.mkdir(QStringLiteral("undo"))) || !m_cacheDir.absolutePath().contains(documentId)) {
92         pCore->displayMessage(i18n("Something is wrong with cache folder %1", m_cacheDir.absolutePath()), ErrorMessage);
93         return false;
94     }
95     if (!loadParams()) {
96         pCore->displayMessage(i18n("Invalid timeline preview parameters"), ErrorMessage);
97         return false;
98     }
99     m_undoDir = QDir(m_cacheDir.absoluteFilePath(QStringLiteral("undo")));
100 
101     // Make sure our cache dirs are inside the temporary folder
102     if (!m_cacheDir.makeAbsolute() || !m_undoDir.makeAbsolute() || !m_undoDir.mkpath(QStringLiteral("."))) {
103         pCore->displayMessage(i18n("Something is wrong with cache folders"), ErrorMessage);
104         return false;
105     }
106 
107     connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews);
108     connect(doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo, Qt::DirectConnection);
109     m_previewTimer.setSingleShot(true);
110     m_previewTimer.setInterval(3000);
111     connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender);
112     connect(this, &PreviewManager::previewRender, this, &PreviewManager::gotPreviewRender, Qt::DirectConnection);
113     connect(&m_previewGatherTimer, &QTimer::timeout, this, &PreviewManager::slotProcessDirtyChunks);
114     m_initialized = true;
115     return true;
116 }
117 
buildPreviewTrack()118 bool PreviewManager::buildPreviewTrack()
119 {
120     if (m_previewTrack != nullptr) {
121         return false;
122     }
123     // Create overlay track
124     qDebug() << "/// BUILDING PREVIEW TRACK\n----------------------\n----------------__";
125     m_previewTrack = new Mlt::Playlist(pCore->getCurrentProfile()->profile());
126     m_previewTrack->set("id", "timeline_preview");
127     m_tractor->lock();
128     reconnectTrack();
129     m_tractor->unlock();
130     return true;
131 }
132 
loadChunks(QVariantList previewChunks,QVariantList dirtyChunks,const QDateTime & documentDate,Mlt::Playlist & playlist)133 void PreviewManager::loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, const QDateTime &documentDate, Mlt::Playlist &playlist)
134 {
135     if (previewChunks.isEmpty()) {
136         previewChunks = m_renderedChunks;
137     }
138     if (dirtyChunks.isEmpty()) {
139         dirtyChunks = m_dirtyChunks;
140     }
141 
142     // First chech if there are invalid chunks (created after document date)
143     QFileInfoList chunksList = m_cacheDir.entryInfoList({QString("*.%1").arg(m_extension)}, QDir::Files, QDir::Time);
144     for (auto &chunkFile : chunksList) {
145         if (chunkFile.lastModified() > documentDate) {
146             // This chunk is invalid
147             QString chunkName = chunkFile.fileName().section(QLatin1Char('.'), 0, 0);
148             previewChunks.removeAll(chunkName);
149             dirtyChunks << chunkName;
150             // Physically remove chunk file
151             m_cacheDir.remove(chunkFile.fileName());
152         } else {
153             // Done
154             break;
155         }
156     }
157     QStringList existingChuncks;
158     if (!previewChunks.isEmpty()) {
159         existingChuncks = m_cacheDir.entryList(QDir::Files);
160     }
161 
162     int max = playlist.count();
163     std::shared_ptr<Mlt::Producer> clip;
164     m_tractor->lock();
165     for (int i = 0; i < max; i++) {
166         if (playlist.is_blank(i)) {
167             continue;
168         }
169         int position = playlist.clip_start(i);
170         if (previewChunks.contains(QString::number(position))) {
171             if (existingChuncks.contains(QString("%1.%2").arg(position).arg(m_extension))) {
172                 clip.reset(playlist.get_clip(i));
173                 m_renderedChunks << position;
174                 m_previewTrack->insert_at(position, clip.get(), 1);
175             } else {
176                 dirtyChunks << position;
177             }
178         }
179     }
180     m_previewTrack->consolidate_blanks();
181     m_tractor->unlock();
182     if (!previewChunks.isEmpty()) {
183         emit m_controller->renderedChunksChanged();
184     }
185     if (!dirtyChunks.isEmpty()) {
186         QMutexLocker lock(&m_dirtyMutex);
187         for (const auto &i : qAsConst(dirtyChunks)) {
188             if (!m_dirtyChunks.contains(i)) {
189                 m_dirtyChunks << i;
190             }
191         }
192         emit m_controller->dirtyChunksChanged();
193     }
194 }
195 
deletePreviewTrack()196 void PreviewManager::deletePreviewTrack()
197 {
198     m_tractor->lock();
199     disconnectTrack();
200     delete m_previewTrack;
201     m_previewTrack = nullptr;
202     m_dirtyChunks.clear();
203     m_renderedChunks.clear();
204     emit m_controller->dirtyChunksChanged();
205     emit m_controller->renderedChunksChanged();
206     m_tractor->unlock();
207 }
208 
getCacheDir() const209 const QDir PreviewManager::getCacheDir() const
210 {
211     return m_cacheDir;
212 }
213 
reconnectTrack()214 void PreviewManager::reconnectTrack()
215 {
216     disconnectTrack();
217     if (!m_previewTrack && !m_overlayTrack) {
218         m_previewTrackIndex = -1;
219         return;
220     }
221     m_previewTrackIndex = m_tractor->count();
222     int increment = 0;
223     if (m_previewTrack) {
224         m_tractor->insert_track(*m_previewTrack, m_previewTrackIndex);
225         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(m_previewTrackIndex));
226         tk->set("hide", 2);
227         //tk->set("id", "timeline_preview");
228         increment++;
229     }
230     if (m_overlayTrack) {
231         m_tractor->insert_track(*m_overlayTrack, m_previewTrackIndex + increment);
232         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(m_previewTrackIndex + increment));
233         tk->set("hide", 2);
234         //tk->set("id", "timeline_overlay");
235     }
236 }
237 
disconnectTrack()238 void PreviewManager::disconnectTrack()
239 {
240     if (m_previewTrackIndex > -1) {
241         Mlt::Producer *prod = m_tractor->track(m_previewTrackIndex);
242         if (strcmp(prod->get("id"), "timeline_preview") == 0 || strcmp(prod->get("id"), "timeline_overlay") == 0) {
243             m_tractor->remove_track(m_previewTrackIndex);
244         }
245         delete prod;
246         if (m_tractor->count() == m_previewTrackIndex + 1) {
247             // overlay track still here, remove
248             Mlt::Producer *trkprod = m_tractor->track(m_previewTrackIndex);
249             if (strcmp(trkprod->get("id"), "timeline_overlay") == 0) {
250                 m_tractor->remove_track(m_previewTrackIndex);
251             }
252             delete trkprod;
253         }
254     }
255     m_previewTrackIndex = -1;
256 }
257 
disable()258 void PreviewManager::disable()
259 {
260     if (m_previewTrackIndex > -1) {
261         if (m_previewTrack) {
262             m_previewTrack->set("hide", 3);
263         }
264         if (m_overlayTrack) {
265             m_overlayTrack->set("hide", 3);
266         }
267     }
268 }
269 
enable()270 void PreviewManager::enable()
271 {
272     if (m_previewTrackIndex > -1) {
273         if (m_previewTrack) {
274             m_previewTrack->set("hide", 2);
275         }
276         if (m_overlayTrack) {
277             m_overlayTrack->set("hide", 2);
278         }
279     }
280 }
281 
loadParams()282 bool PreviewManager::loadParams()
283 {
284     KdenliveDoc *doc = pCore->currentDoc();
285     m_extension = doc->getDocumentProperty(QStringLiteral("previewextension"));
286 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
287     m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts);
288 #else
289     m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), Qt::SkipEmptyParts);
290 #endif
291 
292     if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
293         doc->selectPreviewProfile();
294 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
295         m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts);
296 #else
297         m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), Qt::SkipEmptyParts);
298 #endif
299         m_extension = doc->getDocumentProperty(QStringLiteral("previewextension"));
300     }
301     if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
302         return false;
303     }
304     // Remove the r= and s= parameter (forcing framerate / frame size) as it causes rendering failure.
305     // These parameters should be provided by MLT's profile
306     // NOTE: this is still required for DNxHD so leave it
307     /*for (int i = 0; i < m_consumerParams.count(); i++) {
308         if (m_consumerParams.at(i).startsWith(QStringLiteral("r=")) || m_consumerParams.at(i).startsWith(QStringLiteral("s="))) {
309             m_consumerParams.removeAt(i);
310             i--;
311         }
312     }*/
313     if (doc->getDocumentProperty(QStringLiteral("resizepreview")).toInt() != 0) {
314         int resizeWidth = doc->getDocumentProperty(QStringLiteral("previewheight")).toInt();
315         m_consumerParams << QStringLiteral("s=%1x%2").arg(int(resizeWidth * pCore->getCurrentDar())).arg(resizeWidth);
316     }
317     m_consumerParams << QStringLiteral("an=1");
318     if (KdenliveSettings::gpu_accel()) {
319         m_consumerParams << QStringLiteral("glsl.=1");
320     }
321     return true;
322 }
323 
invalidatePreviews()324 void PreviewManager::invalidatePreviews()
325 {
326     QMutexLocker lock(&m_previewMutex);
327     bool timer = KdenliveSettings::autopreview();
328     if (m_previewTimer.isActive()) {
329         m_previewTimer.stop();
330         timer = true;
331     }
332     KdenliveDoc *doc = pCore->currentDoc();
333     int stackIx = doc->commandStack()->index();
334     int stackMax = doc->commandStack()->count();
335     if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) {
336         // We just added a new command in stack, archive existing chunks
337         int ix = stackIx - 1;
338         m_undoDir.mkdir(QString::number(ix));
339         bool foundPreviews = false;
340         for (const auto &i : m_dirtyChunks) {
341             QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
342             if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(ix).arg(current))) {
343                 foundPreviews = true;
344             }
345         }
346         if (!foundPreviews) {
347             // No preview files found, remove undo folder
348             m_undoDir.rmdir(QString::number(ix));
349         } else {
350             // new chunks archived, cleanup old ones
351             emit cleanupOldPreviews();
352         }
353     } else {
354         // Restore existing chunks, delete others
355         // Check if we just undo the last stack action, then backup, otherwise delete
356         bool lastUndo = false;
357         if (stackIx + 1 == stackMax) {
358             if (!m_undoDir.exists(QString::number(stackMax))) {
359                 lastUndo = true;
360                 bool foundPreviews = false;
361                 m_undoDir.mkdir(QString::number(stackMax));
362                 for (const auto &i : m_dirtyChunks) {
363                     QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
364                     if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(stackMax).arg(current))) {
365                         foundPreviews = true;
366                     }
367                 }
368                 if (!foundPreviews) {
369                     m_undoDir.rmdir(QString::number(stackMax));
370                 }
371             }
372         }
373         bool moveFile = true;
374         QDir tmpDir = m_undoDir;
375         if (!tmpDir.cd(QString::number(stackIx))) {
376             moveFile = false;
377         }
378         QVariantList foundChunks;
379         for (const auto &i : m_dirtyChunks) {
380             QString cacheFileName = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension);
381             if (!lastUndo) {
382                 m_cacheDir.remove(cacheFileName);
383             }
384             if (moveFile) {
385                 if (QFile::copy(tmpDir.absoluteFilePath(cacheFileName), m_cacheDir.absoluteFilePath(cacheFileName))) {
386                     foundChunks << i;
387                 } else {
388                     qDebug() << "// ERROR PROCESSE CHUNK: " << i << ", " << cacheFileName;
389                 }
390             }
391         }
392         if (!foundChunks.isEmpty()) {
393             std::sort(foundChunks.begin(), foundChunks.end());
394             m_dirtyMutex.lock();
395             for (auto &ck : foundChunks) {
396                 m_dirtyChunks.removeAll(ck);
397                 m_renderedChunks << ck;
398             }
399             m_dirtyMutex.unlock();
400             emit m_controller->dirtyChunksChanged();
401             emit m_controller->renderedChunksChanged();
402             reloadChunks(foundChunks);
403         }
404     }
405     doc->setModified(true);
406     if (timer) {
407         m_previewTimer.start();
408     }
409 }
410 
doCleanupOldPreviews()411 void PreviewManager::doCleanupOldPreviews()
412 {
413     if (m_undoDir.dirName() != QLatin1String("undo")) {
414         return;
415     }
416     QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
417 
418     // Use QCollator to do a natural sorting so that 10 is after 2
419     QCollator collator;
420     collator.setNumericMode(true);
421     std::sort(dirs.begin(), dirs.end(), [&collator](const QString &file1, const QString &file2) { return collator.compare(file1, file2) < 0; });
422     bool ok;
423     while (dirs.count() > 5) {
424         QDir tmp = m_undoDir;
425         QString dirName = dirs.takeFirst();
426         dirName.toInt(&ok);
427         if (ok && tmp.cd(dirName)) {
428             tmp.removeRecursively();
429         }
430     }
431 }
432 
clearPreviewRange(bool resetZones)433 void PreviewManager::clearPreviewRange(bool resetZones)
434 {
435     m_previewGatherTimer.stop();
436     abortRendering();
437     m_tractor->lock();
438     bool hasPreview = m_previewTrack != nullptr;
439     QMutexLocker lock(&m_dirtyMutex);
440     for (const auto &ix : qAsConst(m_renderedChunks)) {
441         m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension));
442         if (!m_dirtyChunks.contains(ix)) {
443             m_dirtyChunks << ix;
444         }
445         if (!hasPreview) {
446             continue;
447         }
448         int trackIx = m_previewTrack->get_clip_index_at(ix.toInt());
449         if (!m_previewTrack->is_blank(trackIx)) {
450             Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx);
451             delete prod;
452         }
453     }
454     if (hasPreview) {
455         m_previewTrack->consolidate_blanks();
456     }
457     m_tractor->unlock();
458     m_renderedChunks.clear();
459     // Reload preview params
460     loadParams();
461     if (resetZones) {
462         m_dirtyChunks.clear();
463     }
464     emit m_controller->renderedChunksChanged();
465     emit m_controller->dirtyChunksChanged();
466 }
467 
addPreviewRange(const QPoint zone,bool add)468 void PreviewManager::addPreviewRange(const QPoint zone, bool add)
469 {
470     int chunkSize = KdenliveSettings::timelinechunks();
471     int startChunk = zone.x() / chunkSize;
472     int endChunk = int(rintl(zone.y() / chunkSize));
473     QList<int> toRemove;
474     qDebug() << " // / RESUQEST CHUNKS; " << startChunk << " = " << endChunk;
475     QMutexLocker lock(&m_dirtyMutex);
476     for (int i = startChunk; i <= endChunk; i++) {
477         int frame = i * chunkSize;
478         if (add) {
479             if (!m_renderedChunks.contains(frame) && !m_dirtyChunks.contains(frame)) {
480                 m_dirtyChunks << frame;
481             }
482         } else {
483             if (m_renderedChunks.contains(frame)) {
484                 toRemove << frame;
485                 m_renderedChunks.removeAll(frame);
486             } else {
487                 m_dirtyChunks.removeAll(frame);
488             }
489         }
490     }
491     if (add) {
492         qDebug() << "CHUNKS CHANGED: " << m_dirtyChunks;
493         emit m_controller->dirtyChunksChanged();
494         if (m_previewProcess.state() == QProcess::NotRunning && KdenliveSettings::autopreview()) {
495             m_previewTimer.start();
496         }
497     } else {
498         // Remove processed chunks
499         bool isRendering = m_previewProcess.state() != QProcess::NotRunning;
500         m_previewGatherTimer.stop();
501         abortRendering();
502         m_tractor->lock();
503         bool hasPreview = m_previewTrack != nullptr;
504         for (int ix : qAsConst(toRemove)) {
505             m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix).arg(m_extension));
506             if (!hasPreview) {
507                 continue;
508             }
509             int trackIx = m_previewTrack->get_clip_index_at(ix);
510             if (!m_previewTrack->is_blank(trackIx)) {
511                 Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx);
512                 delete prod;
513             }
514         }
515         if (hasPreview) {
516             m_previewTrack->consolidate_blanks();
517         }
518         emit m_controller->renderedChunksChanged();
519         emit m_controller->dirtyChunksChanged();
520         m_tractor->unlock();
521         if (isRendering || KdenliveSettings::autopreview()) {
522             m_previewTimer.start();
523         }
524     }
525 }
526 
abortRendering()527 void PreviewManager::abortRendering()
528 {
529     if (m_previewProcess.state() == QProcess::NotRunning) {
530         return;
531     }
532     emit abortPreview();
533     m_previewProcess.waitForFinished();
534     if (m_previewProcess.state() != QProcess::NotRunning) {
535         m_previewProcess.kill();
536         m_previewProcess.waitForFinished();
537     }
538     // Re-init time estimation
539     emit previewRender(-1, QString(), 1000);
540 }
541 
startPreviewRender()542 void PreviewManager::startPreviewRender()
543 {
544     QMutexLocker lock(&m_previewMutex);
545     if (m_renderedChunks.isEmpty() && m_dirtyChunks.isEmpty()) {
546         m_controller->addPreviewRange(true);
547     }
548     if (!m_dirtyChunks.isEmpty()) {
549         // Abort any rendering
550         abortRendering();
551         m_waitingThumbs.clear();
552         // clear log
553         m_errorLog.clear();
554         const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt"));
555         pCore->getMonitor(Kdenlive::ProjectMonitor)->sceneList(m_cacheDir.absolutePath(), sceneList);
556         m_previewTimer.stop();
557         doPreviewRender(sceneList);
558     }
559 }
560 
receivedStderr()561 void PreviewManager::receivedStderr()
562 {
563     QStringList resultList = QString::fromLocal8Bit(m_previewProcess.readAllStandardError()).split(QLatin1Char('\n'));
564     for (auto &result : resultList) {
565         if (result.startsWith(QLatin1String("START:"))) {
566             workingPreview = result.section(QLatin1String("START:"), 1).simplified().toInt();
567             qDebug() << "// GOT START INFO: " << workingPreview;
568             emit m_controller->workingPreviewChanged();
569         } else if (result.startsWith(QLatin1String("DONE:"))) {
570             int chunk = result.section(QLatin1String("DONE:"), 1).simplified().toInt();
571             m_processedChunks++;
572             QString fileName = QStringLiteral("%1.%2").arg(chunk).arg(m_extension);
573             qDebug() << "---------------\nJOB PROGRRESS: " << m_chunksToRender << ", " << m_processedChunks << " = "
574                      << (100 * m_processedChunks / m_chunksToRender);
575             emit previewRender(chunk, m_cacheDir.absoluteFilePath(fileName), 1000 * m_processedChunks / m_chunksToRender);
576         } else {
577             m_errorLog.append(result);
578         }
579     }
580 }
581 
doPreviewRender(const QString & scene)582 void PreviewManager::doPreviewRender(const QString &scene)
583 {
584     // initialize progress bar
585     QMutexLocker lock(&m_dirtyMutex);
586     std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end());
587     if (m_dirtyChunks.isEmpty()) {
588         return;
589     }
590     Q_ASSERT(m_previewProcess.state() == QProcess::NotRunning);
591 
592     QStringList chunks;
593     for (QVariant &frame : m_dirtyChunks) {
594         chunks << frame.toString();
595     }
596     m_chunksToRender = m_dirtyChunks.count();
597     m_processedChunks = 0;
598     int chunkSize = KdenliveSettings::timelinechunks();
599     QStringList args{KdenliveSettings::rendererpath(),
600                      scene,
601                      m_cacheDir.absolutePath(),
602                      QStringLiteral("-split"),
603                      chunks.join(QLatin1Char(',')),
604                      QString::number(chunkSize - 1),
605                      pCore->getCurrentProfilePath(),
606                      m_extension,
607                      m_consumerParams.join(QLatin1Char(' '))};
608     qDebug() << " -  - -STARTING PREVIEW JOBS: " << args;
609     pCore->currentDoc()->previewProgress(0);
610     m_previewProcess.start(m_renderer, args);
611     if (m_previewProcess.waitForStarted()) {
612         qDebug() << " -  - -STARTING PREVIEW JOBS . . . STARTED";
613     }
614 }
615 
processEnded(int,QProcess::ExitStatus status)616 void PreviewManager::processEnded(int, QProcess::ExitStatus status)
617 {
618     const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt"));
619     QFile::remove(sceneList);
620     if (status == QProcess::QProcess::CrashExit) {
621         pCore->currentDoc()->previewProgress(-1);
622         if (workingPreview >= 0) {
623             const QString fileName = QStringLiteral("%1.%2").arg(workingPreview).arg(m_extension);
624             if (m_cacheDir.exists(fileName)) {
625                 m_cacheDir.remove(fileName);
626             }
627         }
628     } else {
629         pCore->currentDoc()->previewProgress(1000);
630     }
631     workingPreview = -1;
632     emit m_controller->workingPreviewChanged();
633 }
634 
slotProcessDirtyChunks()635 void PreviewManager::slotProcessDirtyChunks()
636 {
637     if (m_dirtyChunks.isEmpty()) {
638         return;
639     }
640     invalidatePreviews();
641     if (KdenliveSettings::autopreview()) {
642         m_previewTimer.start();
643     }
644 }
645 
slotRemoveInvalidUndo(int ix)646 void PreviewManager::slotRemoveInvalidUndo(int ix)
647 {
648     QMutexLocker lock(&m_previewMutex);
649     if (m_undoDir.dirName() != QLatin1String("undo")) {
650         // Make sure we delete correct folder
651         return;
652     }
653     QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
654     bool ok;
655     for (const QString &dir : qAsConst(dirs)) {
656         if (dir.toInt(&ok) >= ix && ok) {
657             QDir tmp = m_undoDir;
658             if (tmp.cd(dir)) {
659                 tmp.removeRecursively();
660             }
661         }
662     }
663 }
664 
invalidatePreview(int startFrame,int endFrame)665 void PreviewManager::invalidatePreview(int startFrame, int endFrame)
666 {
667     if (m_previewTrack == nullptr) {
668         return;
669     }
670     int chunkSize = KdenliveSettings::timelinechunks();
671     int start = startFrame - startFrame % chunkSize;
672     int end = endFrame - endFrame % chunkSize;
673 
674     std::sort(m_renderedChunks.begin(), m_renderedChunks.end());
675     m_previewGatherTimer.stop();
676     bool stopPreview = m_previewProcess.state() == QProcess::Running;
677     if (m_renderedChunks.isEmpty() || ((workingPreview < m_renderedChunks.first().toInt() || workingPreview > m_renderedChunks.last().toInt()) && (end < m_renderedChunks.first().toInt() || start > m_renderedChunks.last().toInt()))) {
678         // invalidated zone is not in the preview zone, don't stop process
679         stopPreview = false;
680     }
681     if (stopPreview) {
682         abortRendering();
683     }
684     m_tractor->lock();
685     bool chunksChanged = false;
686     for (int i = start; i <= end; i += chunkSize) {
687         if (m_renderedChunks.contains(i)) {
688             int ix = m_previewTrack->get_clip_index_at(i);
689             if (m_previewTrack->is_blank(ix)) {
690                 continue;
691             }
692             Mlt::Producer *prod = m_previewTrack->replace_with_blank(ix);
693             delete prod;
694             QVariant val(i);
695             m_renderedChunks.removeAll(val);
696             if (!m_dirtyChunks.contains(val)) {
697                 QMutexLocker lock(&m_dirtyMutex);
698                 m_dirtyChunks << val;
699                 chunksChanged = true;
700             }
701         }
702     }
703     m_tractor->unlock();
704     if (chunksChanged) {
705         m_previewTrack->consolidate_blanks();
706         emit m_controller->renderedChunksChanged();
707         emit m_controller->dirtyChunksChanged();
708     }
709     if (stopPreview) {
710         startPreviewRender();
711     }
712     m_previewGatherTimer.start();
713 }
714 
reloadChunks(const QVariantList chunks)715 void PreviewManager::reloadChunks(const QVariantList chunks)
716 {
717     if (m_previewTrack == nullptr || chunks.isEmpty()) {
718         return;
719     }
720     m_tractor->lock();
721     for (const auto &ix : chunks) {
722         if (m_previewTrack->is_blank_at(ix.toInt())) {
723             QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension));
724             fileName.prepend(QStringLiteral("avformat:"));
725             Mlt::Producer prod(pCore->getCurrentProfile()->profile(), fileName.toUtf8().constData());
726             if (prod.is_valid()) {
727                 // m_ruler->updatePreview(ix, true);
728                 prod.set("mlt_service", "avformat-novalidate");
729                 prod.set("mute_on_pause", 1);
730                 m_previewTrack->insert_at(ix.toInt(), &prod, 1);
731             }
732         }
733     }
734     m_previewTrack->consolidate_blanks();
735     m_tractor->unlock();
736 }
737 
gotPreviewRender(int frame,const QString & file,int progress)738 void PreviewManager::gotPreviewRender(int frame, const QString &file, int progress)
739 {
740     if (m_previewTrack == nullptr) {
741         return;
742     }
743     if (frame < 0) {
744         pCore->currentDoc()->previewProgress(1000);
745         return;
746     }
747     if (file.isEmpty() || progress < 0) {
748         pCore->currentDoc()->previewProgress(progress);
749         if (progress < 0) {
750             pCore->displayMessage(i18n("Preview rendering failed, check your parameters. %1Show details...%2",
751                                        QString("<a href=\"" + QString::fromLatin1(QUrl::toPercentEncoding(file)) + QStringLiteral("\">")),
752                                        QStringLiteral("</a>")),
753                                   MltError);
754         }
755         return;
756     }
757     if (m_previewTrack->is_blank_at(frame)) {
758         Mlt::Producer prod(pCore->getCurrentProfile()->profile(), QString("avformat:%1").arg(file).toUtf8().constData());
759         if (prod.is_valid()) {
760             QMutexLocker lock(&m_dirtyMutex);
761             m_dirtyChunks.removeAll(frame);
762             m_renderedChunks << frame;
763             emit m_controller->renderedChunksChanged();
764             prod.set("mlt_service", "avformat-novalidate");
765             prod.set("mute_on_pause", 1);
766             qDebug() << "|||| PLUGGING PREVIEW CHUNK AT: " << frame;
767             m_tractor->lock();
768             m_previewTrack->insert_at(frame, &prod, 1);
769             m_previewTrack->consolidate_blanks();
770             m_tractor->unlock();
771             pCore->currentDoc()->previewProgress(progress);
772             pCore->currentDoc()->setModified(true);
773         } else {
774             qCDebug(KDENLIVE_LOG) << "* * * INVALID PROD: " << file;
775             corruptedChunk(frame, file);
776         }
777     } else {
778         qCDebug(KDENLIVE_LOG) << "* * * NON EMPTY PROD: " << frame;
779     }
780 }
781 
corruptedChunk(int frame,const QString & fileName)782 void PreviewManager::corruptedChunk(int frame, const QString &fileName)
783 {
784     emit abortPreview();
785     m_previewProcess.waitForFinished();
786     if (workingPreview >= 0) {
787         workingPreview = -1;
788         emit m_controller->workingPreviewChanged();
789     }
790     emit previewRender(0, m_errorLog, -1);
791     m_cacheDir.remove(fileName);
792     if (!m_dirtyChunks.contains(frame)) {
793         QMutexLocker lock(&m_dirtyMutex);
794         m_dirtyChunks << frame;
795         std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end());
796     }
797 }
798 
setOverlayTrack(Mlt::Playlist * overlay)799 int PreviewManager::setOverlayTrack(Mlt::Playlist *overlay)
800 {
801     m_overlayTrack = overlay;
802     m_overlayTrack->set("id", "timeline_overlay");
803     reconnectTrack();
804     return m_previewTrackIndex;
805 }
806 
removeOverlayTrack()807 void PreviewManager::removeOverlayTrack()
808 {
809     delete m_overlayTrack;
810     m_overlayTrack = nullptr;
811     reconnectTrack();
812 }
813 
previewChunks() const814 QPair<QStringList, QStringList> PreviewManager::previewChunks() const
815 {
816     QStringList renderedChunks;
817     QStringList dirtyChunks;
818     for (const QVariant &frame : m_renderedChunks) {
819         renderedChunks << frame.toString();
820     }
821     QMutexLocker lock(&m_dirtyMutex);
822     for (const QVariant &frame : m_dirtyChunks) {
823         dirtyChunks << frame.toString();
824     }
825     return {renderedChunks, dirtyChunks};
826 }
827 
hasOverlayTrack() const828 bool PreviewManager::hasOverlayTrack() const
829 {
830     return m_overlayTrack != nullptr;
831 }
832 
hasPreviewTrack() const833 bool PreviewManager::hasPreviewTrack() const
834 {
835     return m_previewTrack != nullptr;
836 }
837 
addedTracks() const838 int PreviewManager::addedTracks() const
839 {
840     if (m_previewTrack) {
841         if (m_overlayTrack) {
842             return 2;
843         }
844         return 1;
845     } else if (m_overlayTrack) {
846         return 1;
847     }
848     return -1;
849 }
850