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