Timeline preview: refactor and move all functions into a new previewmanager class

parent d9b73c74
......@@ -294,7 +294,6 @@ KdenliveDoc::KdenliveDoc(const QUrl &url, const QUrl &projectFolder, QUndoGroup
QDir dir2(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir2.mkdir(documentId);
updateProjectFolderPlacesEntry();
connect(this, &KdenliveDoc::cleanupOldPreviews, this, &KdenliveDoc::doCleanupOldPreviews);
}
void KdenliveDoc::slotSetDocumentNotes(const QString &notes)
......@@ -313,23 +312,6 @@ KdenliveDoc::~KdenliveDoc()
if (!m_autosave->fileName().isEmpty()) m_autosave->remove();
delete m_autosave;
}
// Remove all timeline preview undo data
QString id = m_documentProperties.value(QStringLiteral("documentid"));
if (id.isEmpty() || id.toLong() == 0) {
// Something is wrong, make sure we don't trash valuable data
// id should be a number (ms since epoch)
return;
}
QDir dir = getCacheDir();
if (m_url.isEmpty()) {
// Doc was not saved, double check path and delete folder
if (dir.dirName() == id)
dir.removeRecursively();
} else {
if (dir.cd("undo") && dir.absolutePath().contains(id)) {
dir.removeRecursively();
}
}
}
int KdenliveDoc::setSceneList()
......@@ -1638,91 +1620,6 @@ void KdenliveDoc::doAddAction(const QString &name, QAction *a, QKeySequence shor
pCore->window()->actionCollection()->setDefaultShortcut(a, shortcut);
}
QDir KdenliveDoc::getCacheDir()
{
QString documentId = m_documentProperties.value(QStringLiteral("documentid"));
return QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/"+ documentId);
}
void KdenliveDoc::invalidatePreviews(QList <int> chunks)
{
// We are not at the bottom of undo stack, chunks have already been archived previously
QMutexLocker lock(&m_previewMutex);
QDir dir = getCacheDir();
QString ext = m_documentProperties.value(QStringLiteral("previewextension"));
if (m_commandStack->index() == m_commandStack->count() && !dir.exists(QString("undo/%1").arg(m_commandStack->index() - 1))) {
// Archive just created chunks
int ix = m_commandStack->index() - 1;
if (!dir.exists("undo"))
dir.mkdir("undo");
if (!dir.exists("undo")) {
// Cannot create undo dir, abort
return;
}
dir.mkdir(QString("undo/%1").arg(ix));
bool foundPreviews = false;
foreach(int i, chunks) {
QString current = QString("%1.%2").arg(i).arg(ext);
if (dir.rename(current, QString("undo/%1/%2").arg(ix).arg(current))) {
foundPreviews = true;
}
}
if (!foundPreviews) {
if (dir.cd(QString("undo/%1").arg(ix)) && dir.absolutePath().contains("/undo/")) {
dir.removeRecursively();
}
}
else emit cleanupOldPreviews(ix);
} else {
// Restore existing chunks, delete others
QDir srcdir(dir);
QStringList filters;
filters << QString("*.%1").arg(ext);
QList <int> foundChunks;
// Check if we just undo the last stack action, then backup, otherwise delete
bool lastUndo = false;
int max = m_commandStack->count();
if (m_commandStack->index() == max - 1) {
if (!srcdir.exists(QString("undo/%1").arg(max))) {
lastUndo = true;
bool foundPreviews = false;
dir.mkdir(QString("undo/%1").arg(max));
foreach(int i, chunks) {
QString current = QString("%1.%2").arg(i).arg(ext);
if (dir.rename(current, QString("undo/%1/%2").arg(max).arg(current))) {
foundPreviews = true;
}
}
if (!foundPreviews) {
QDir tmpDir = dir;
if (tmpDir.cd(QString("undo/%1").arg(max)) && tmpDir.absolutePath().contains("/undo/")) {
tmpDir.removeRecursively();
}
}
}
}
if (!lastUndo) {
foreach(int i, chunks) {
srcdir.remove(QString("%1.%2").arg(i).arg(ext));
}
}
if (!dir.cd(QString("undo/%1").arg(m_commandStack->index())))
return;
QStringList filesnames = dir.entryList(filters, QDir::Files);
foreach(const QString & fname, filesnames) {
int existingChunk = fname.section(".", 0, 0).toInt();
if (chunks.contains(existingChunk)) {
// Restore chunks
foundChunks << existingChunk;
QFile::copy(dir.absoluteFilePath(fname), srcdir.absoluteFilePath(fname));
}
}
qSort(foundChunks);
emit reloadChunks(foundChunks);
}
setModified(true);
}
void KdenliveDoc::previewProgress(int p)
{
pCore->window()->setPreviewProgress(p);
......@@ -1783,40 +1680,13 @@ void KdenliveDoc::selectPreviewProfile()
}
}
void KdenliveDoc::doCleanupOldPreviews(int ix)
void KdenliveDoc::checkPreviewStack()
{
QDir dir = getCacheDir();
if (!dir.cd("undo"))
return;
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
bool ok;
foreach (const QString &num, dirs) {
int nb = num.toInt(&ok);
if (ok && nb < ix - 5) {
QDir tmp = dir;
if (tmp.cd(num) && tmp.absolutePath().contains("/undo/")) {
tmp.removeRecursively();
}
}
}
// A command was pushed in the middle of the stack, remove all cached data from last undos
emit removeInvalidUndo(m_commandStack->count());
}
void KdenliveDoc::checkPreviewStack()
void KdenliveDoc::saveMltPlaylist(const QString fileName)
{
// A command was pushed in the middle of the stack, remove all cached data from last undos
int max = m_commandStack->count();
QDir dir = getCacheDir();
if (!dir.cd("undo"))
return;
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
bool ok;
foreach (const QString &num, dirs) {
int nb = num.toInt(&ok);
if (ok && nb >= max) {
QDir tmp = dir;
if (tmp.cd(num) && tmp.absolutePath().contains("/undo/")) {
tmp.removeRecursively();
}
}
}
m_render->preparePreviewRendering(fileName);
}
......@@ -32,7 +32,6 @@
#include <QObject>
#include <QTimer>
#include <QUrl>
#include <QMutex>
#include <kautosavefile.h>
#include <KDirWatch>
......@@ -132,6 +131,8 @@ public:
QDomDocument xmlSceneList(const QString &scene);
/** @brief Saves the project file xml to a file. */
bool saveSceneList(const QString &path, const QString &scene);
/** @brief Saves only the MLT xml to a file for preview rendering. */
void saveMltPlaylist(const QString fileName);
void cacheImage(const QString &fileId, const QImage &img) const;
void setProjectFolder(QUrl url);
void setZone(int start, int end);
......@@ -173,8 +174,6 @@ public:
void previewProgress(int p);
/** @brief Select most appropriate rendering profile for timeline preview based on fps / size. */
void selectPreviewProfile();
/** @brief Get the directory to store timeline previews */
QDir getCacheDir();
private:
QUrl m_url;
......@@ -193,7 +192,6 @@ private:
ClipManager *m_clipManager;
MltVideoProfile m_profile;
QString m_searchFolder;
QMutex m_previewMutex;
/** @brief Tells whether the current document has been changed after being saved. */
bool m_modified;
......@@ -241,7 +239,6 @@ private slots:
void slotSetDocumentNotes(const QString &notes);
void switchProfile(MltVideoProfile profile, const QString &id, const QDomElement &xml);
void slotSwitchProfile();
void doCleanupOldPreviews(int ix);
/** @brief Check if we did a new action invalidating more recent undo items. */
void checkPreviewStack();
......@@ -263,10 +260,8 @@ signals:
void reloadEffects();
/** @brief Fps was changed, update timeline */
void updateFps(bool changed);
/** @brief Some timeline preview chunks restored, reload them */
void reloadChunks(QList <int> chunks);
/** @brief Only keep 5 undo levels of timeline previews, ask for cleanup */
void cleanupOldPreviews(int ix);
/** @brief If a command is pushed when we are in the middle of undo stack, invalidate further undo history */
void removeInvalidUndo(int ix);
};
......
......@@ -79,7 +79,7 @@ ProjectSettings::ProjectSettings(KdenliveDoc *doc, QMap <QString, QString> metad
m_proxyextension = doc->getDocumentProperty(QStringLiteral("proxyextension"));
m_previewparams = doc->getDocumentProperty(QStringLiteral("previewparameters"));
m_previewextension = doc->getDocumentProperty(QStringLiteral("previewextension"));
m_previewDir = doc->getCacheDir();
m_previewDir = doc->getDocumentProperty(QStringLiteral("cachedir"));
}
else {
currentProf = KdenliveSettings::default_profile();
......@@ -288,7 +288,7 @@ void ProjectSettings::slotDeleteProxies()
void ProjectSettings::slotDeletePreviews()
{
if (KMessageBox::warningContinueCancel(this, i18n("Deleting these preview files will invalidate all timeline previews for this project.")) != KMessageBox::Continue) return;
if (KMessageBox::warningContinueCancel(this, i18n("Deleting the project preview files in this folder will invalidate all timeline previews:\n%1", m_previewDir.absolutePath())) != KMessageBox::Continue) return;
buttonBox->setEnabled(false);
//TODO
emit disablePreviews();
......
......@@ -72,8 +72,7 @@ Render::Render(Kdenlive::MonitorId rendererName, BinController *binController, G
m_isLoopMode(false),
m_blackClip(NULL),
m_isActive(false),
m_isRefreshing(false),
m_abortPreview(false)
m_isRefreshing(false)
{
qRegisterMetaType<stringMap> ("stringMap");
analyseAudio = KdenliveSettings::monitor_audio();
......@@ -102,7 +101,6 @@ Render::Render(Kdenlive::MonitorId rendererName, BinController *binController, G
Render::~Render()
{
m_abortPreview = true;
closeMlt();
}
......@@ -113,7 +111,6 @@ void Render::closeMlt()
delete m_mltConsumer;
delete m_mltProducer;
delete m_blackClip;
m_previewThread.waitForFinished();
}
void Render::slotSwitchFullscreen()
......@@ -1618,23 +1615,9 @@ void Render::updateSlowMotionProducers(const QString &id, QMap <QString, QString
}
}
void Render::abortPreview()
void Render::preparePreviewRendering(const QString sceneListFile)
{
if (m_previewThread.isRunning()) {
m_abortPreview = true;
m_previewThread.waitForFinished();
}
}
void Render::previewRendering(QList <int> frames, const QString &cacheDir, QStringList consumerParams, const QString extension)
{
abortPreview();
m_previewChunks << frames;
qSort(m_previewChunks);
QDir dir(cacheDir);
dir.mkpath(QStringLiteral("."));
// Save temporary scenelist
QString sceneListFile = dir.absoluteFilePath("preview.mlt");
Mlt::Consumer xmlConsumer(*m_qmlView->profile(), "xml", sceneListFile.toUtf8().constData());
if (!xmlConsumer.is_valid())
return;
......@@ -1645,52 +1628,5 @@ void Render::previewRendering(QList <int> frames, const QString &cacheDir, QStri
return;
xmlConsumer.connect(prod);
xmlConsumer.run();
m_previewThread = QtConcurrent::run(this, &Render::doPreviewRender, dir, sceneListFile, consumerParams, extension);
}
void Render::doPreviewRender(QDir folder, QString scene, QStringList consumerParams, const QString &extension)
{
int progress;
int chunkSize = KdenliveSettings::timelinechunks();
consumerParams << "an=1";
emit previewRender(0, QString(), 0);
int ct = 0;
while (!m_previewChunks.isEmpty()) {
int i = m_previewChunks.takeFirst();
ct++;
if (m_abortPreview) {
m_previewChunks.prepend(i);
emit previewRender(0, QString(), 1000);
break;
}
QString fileName = QString("%1.%2").arg(i).arg(extension);
if (m_previewChunks.isEmpty()) {
progress = 1000;
} else {
progress = (double) (ct) / (ct + m_previewChunks.count()) * 1000;
}
if (folder.exists(fileName)) {
// This chunk already exists
emit previewRender(i, folder.absoluteFilePath(fileName), progress);
continue;
}
// Build rendering process
QStringList args;
args << scene;
args << "in=" + QString::number(i);
args << "out=" + QString::number(i + chunkSize - 1);
args << "-consumer" << "avformat:" + folder.absoluteFilePath(fileName);
args << consumerParams;
int result = QProcess::execute(KdenliveSettings::rendererpath(), args);
if (result != 0) {
// Something is wrong, abort
qDebug()<<"+++++++++\n++ ERROR ++\n++++++";
emit previewRender(i, QString(), -1);
QFile::remove(folder.absoluteFilePath(fileName));
break;
}
emit previewRender(i, folder.absoluteFilePath(fileName), progress);
}
QFile::remove(scene);
m_abortPreview = false;
}
......@@ -251,8 +251,7 @@ class Render: public AbstractRender
void prepareProfileReset(double fps);
void finishProfileReset();
void updateSlowMotionProducers(const QString &id, QMap <QString, QString> passProperties);
void previewRendering(QList <int> frames, const QString &cacheDir, QStringList consumerParams, const QString extension);
void abortPreview();
void preparePreviewRendering(const QString sceneListFile);
private:
......@@ -265,7 +264,6 @@ private:
Mlt::Producer * m_mltProducer;
Mlt::Event *m_showFrameEvent;
Mlt::Event *m_pauseEvent;
QFuture <void> m_previewThread;
BinController *m_binController;
GLWidget *m_qmlView;
double m_fps;
......@@ -289,10 +287,8 @@ private:
bool m_isActive;
/** @brief True if the consumer is currently refreshing itself. */
bool m_isRefreshing;
bool m_abortPreview;
void closeMlt();
QMap<QString, Mlt::Producer *> m_slowmotionProducers;
QList <int> m_previewChunks;
/** @brief Build the MLT Consumer object with initial settings.
* @param profileName The MLT profile to use for the consumer */
......@@ -312,7 +308,6 @@ private slots:
/** @brief Refreshes the monitor display. */
void refresh();
void slotCheckSeeking();
void doPreviewRender(QDir folder, QString scene, QStringList consumerParams, const QString &extension);
signals:
/** @brief The renderer stopped, either playing or rendering. */
......@@ -349,7 +344,6 @@ signals:
void mltFrameReceived(Mlt::Frame *);
/** @brief We want to replace a clip with another, but before we need to change clip producer id so that there is no interference*/
void prepareTimelineReplacement(const QString &);
void previewRender(int frame, const QString &file, int progress);
public slots:
......
......@@ -25,5 +25,6 @@ set(kdenlive_SRCS
timeline/managers/guidemanager.cpp
timeline/managers/razormanager.cpp
timeline/managers/selectmanager.cpp
timeline/managers/previewmanager.cpp
PARENT_SCOPE)
......@@ -510,17 +510,21 @@ void CustomRuler::activateZone()
update();
}
void CustomRuler::updatePreview(int frame, bool rendered, bool refresh)
bool CustomRuler::updatePreview(int frame, bool rendered, bool refresh)
{
bool result = false;
if (rendered) {
m_renderingPreviews << frame;
m_dirtyRenderingPreviews.removeAll(frame);
} else {
m_renderingPreviews.removeAll(frame);
m_dirtyRenderingPreviews << frame;
if (m_renderingPreviews.removeAll(frame) > 0) {
m_dirtyRenderingPreviews << frame;
result = true;
}
}
if (refresh)
update(frame * m_factor - offset(), MAX_HEIGHT - 3, KdenliveSettings::timelinechunks() * m_factor + 1, 3);
return result;
}
void CustomRuler::updatePreviewDisplay(int start, int end)
......@@ -553,9 +557,10 @@ bool CustomRuler::hasPreviewRange() const
return (!m_dirtyRenderingPreviews.isEmpty() || !m_renderingPreviews.isEmpty());
}
void CustomRuler::addChunks(QList <int> chunks, bool add)
QList <int> CustomRuler::addChunks(QList <int> chunks, bool add)
{
qSort(chunks);
QList <int> toProcess;
if (add) {
foreach(int frame, chunks) {
if (m_renderingPreviews.contains(frame)) {
......@@ -569,10 +574,14 @@ void CustomRuler::addChunks(QList <int> chunks, bool add)
}
} else {
foreach(int frame, chunks) {
m_renderingPreviews.removeAll(frame);
if (m_renderingPreviews.removeAll(frame) > 0) {
// A preview file existed for this chunk, ask deletion
toProcess << frame;
}
m_dirtyRenderingPreviews.removeAll(frame);
}
}
update(chunks.first() * m_factor - offset(), MAX_HEIGHT - 3, (chunks.last() - chunks.first()) * KdenliveSettings::timelinechunks() * m_factor + 1, 3);
return toProcess;
}
......@@ -50,12 +50,12 @@ public:
void updateProjectFps(const Timecode &t);
void updateFrameSize();
void activateZone();
void updatePreview(int frame, bool rendered = true, bool refresh = false);
bool updatePreview(int frame, bool rendered = true, bool refresh = false);
/** @brief Returns a list of rendered timeline preview chunks */
const QStringList previewChunks() const;
/** @brief Returns a list of dirty timeline preview chunks (that need to be generated) */
const QList <int> getDirtyChunks() const;
void addChunks(QList <int> chunks, bool add);
QList <int> addChunks(QList <int> chunks, bool add);
/** @brief Returns true if a timeline preview zone has already be defined */
bool hasPreviewRange() const;
/** @brief Refresh timeline preview range */
......
/***************************************************************************
* Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***************************************************************************/
#include "previewmanager.h"
#include "../customruler.h"
#include "kdenlivesettings.h"
#include "doc/kdenlivedoc.h"
#include <QtConcurrent>
#include <QStandardPaths>
#include <QProcess>
PreviewManager::PreviewManager(KdenliveDoc *doc, CustomRuler *ruler) : QObject()
, m_doc(doc)
, m_ruler(ruler)
, m_initialized(false)
, m_abortPreview(false)
{
}
PreviewManager::~PreviewManager()
{
if (m_initialized) {
abortRendering();
m_undoDir.removeRecursively();
if (m_cacheDir.entryList(QDir::NoDotAndDotDot).count() == 0) {
m_cacheDir.removeRecursively();
}
}
}
bool PreviewManager::initialize()
{
QString documentId = m_doc->getDocumentProperty(QStringLiteral("documentid"));
m_initialized = true;
if (documentId.isEmpty() || documentId.toLong() == 0) {
// Something is wrong, documentId should be a number (ms since epoch), abort
return false;
}
QString cacheDir = m_doc->getDocumentProperty(QStringLiteral("cachedir"));
if (!cacheDir.isEmpty() && QFile::exists(cacheDir)) {
m_cacheDir = QDir(cacheDir);
} else {
m_cacheDir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
m_cacheDir.mkdir(documentId);
if (!m_cacheDir.cd(documentId)) {
return false;
}
}
if (m_cacheDir.dirName() != documentId || (!m_cacheDir.exists("undo") && !m_cacheDir.mkdir("undo"))) {
// TODO: cannot create undo folder, abort
return false;
}
if (!loadParams()) {
return false;
}
m_doc->setDocumentProperty(QStringLiteral("cachedir"), m_cacheDir.absolutePath());
m_undoDir = QDir(m_cacheDir.absoluteFilePath("undo"));
connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews);
connect(m_doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo);
m_previewTimer.setSingleShot(true);
m_previewTimer.setInterval(3000);
connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender);
m_initialized = true;
return true;
}
bool PreviewManager::loadParams()
{
m_extension= m_doc->getDocumentProperty(QStringLiteral("previewextension"));
m_consumerParams = m_doc->getDocumentProperty(QStringLiteral("previewparameters")).split(" ");
if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
m_doc->selectPreviewProfile();
m_consumerParams = m_doc->getDocumentProperty(QStringLiteral("previewparameters")).split(" ");
m_extension= m_doc->getDocumentProperty(QStringLiteral("previewextension"));
}
if (m_consumerParams.isEmpty() || m_extension.isEmpty()) {
return false;
}
m_consumerParams << "an=1";
return true;
}
void PreviewManager::invalidatePreviews(QList <int> chunks)
{
// We are not at the bottom of undo stack, chunks have already been archived previously
QMutexLocker lock(&m_previewMutex);
m_previewTimer.stop();
int stackIx = m_doc->commandStack()->index();
int stackMax = m_doc->commandStack()->count();
abortRendering();
if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) {
// We just added a new command in stack, archive existing chunks
int ix = stackIx - 1;
m_undoDir.mkdir(QString::number(ix));
bool foundPreviews = false;
foreach(int i, chunks) {
QString current = QString("%1.%2").arg(i).arg(m_extension);
if (m_cacheDir.rename(current, QString("undo/%1/%2").arg(ix).arg(current))) {
foundPreviews = true;
}
}
if (!foundPreviews) {
// No preview files found, remove undo folder
m_undoDir.rmdir(QString::number(ix));
} else {
// new chunks archived, cleanup old ones
emit cleanupOldPreviews();
}
} else {
// Restore existing chunks, delete others
// Check if we just undo the last stack action, then backup, otherwise delete
bool lastUndo = false;
if (stackIx == stackMax - 1) {
if (!m_undoDir.exists(QString::number(stackMax))) {
lastUndo = true;
bool foundPreviews = false;
m_undoDir.mkdir(QString::number(stackMax));
foreach(int i, chunks) {
QString current = QString("%1.%2").arg(i).arg(m_extension);
if (m_cacheDir.rename(current, QString("undo/%1/%2").arg(stackMax).arg(current))) {
foundPreviews = true;
}