diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 1020a15c1f..81d84c968d 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -131,7 +131,9 @@ ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::share setTags(getProducerProperty(QStringLiteral("kdenlive:tags"))); AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating")))); connectEffectStack(); - if (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly) { + // Timeline clip thumbs will be generated later after the tractor has been updated + if (m_clipType != ClipType::Timeline && + (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly)) { // Generate clip thumbnail ClipLoadTask::start({ObjectType::BinClip, m_binId.toInt()}, QDomElement(), true, -1, -1, this); // Generate audio thumbnail @@ -631,7 +633,6 @@ bool ProjectClip::setProducer(std::shared_ptr producer, bool gene m_duration = getStringDuration(); m_clipStatus = m_usesProxy ? FileStatus::StatusProxy : FileStatus::StatusReady; locker.unlock(); - if (m_clipStatus != currentStatus) { updateRoles << AbstractProjectItem::ClipStatus << AbstractProjectItem::IconOverlay; updateTimelineClips({TimelineModel::StatusRole, TimelineModel::ClipThumbRole}); @@ -2231,10 +2232,12 @@ void ProjectClip::deregisterTimelineClip(int clipId, bool audioClip) Q_EMIT registeredClipChanged(); } -QList ProjectClip::timelineInstances() const +QList ProjectClip::timelineInstances(QUuid activeUuid) const { QList ids; - const QUuid activeUuid = pCore->currentTimelineId(); + if (activeUuid.isNull()) { + activeUuid = pCore->currentTimelineId(); + } for (const auto ®isteredClip : m_registeredClips) { if (auto ptr = registeredClip.second.lock()) { if (ptr->uuid() != activeUuid) { diff --git a/src/bin/projectclip.h b/src/bin/projectclip.h index 2534992744..1a123dc7e3 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -195,7 +195,7 @@ public: */ bool isIncludedInTimeline() override; /** @brief Returns a list of all timeline clip ids for this bin clip */ - QList timelineInstances() const; + QList timelineInstances(QUuid activeUuid = QUuid()) const; /** @brief This function returns a cut to the master producer associated to the timeline clip with given ID. Each clip must have a different master producer (see comment of the class) */ diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index 0a84569bde..7968ad5a36 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -2100,6 +2100,22 @@ void KdenliveDoc::addTimeline(const QUuid &uuid, std::shared_ptr> j(m_timelines); + while (j.hasNext()) { + j.next(); + if (!j.value()->checkConsistency()) { + return false; + } + } + return true; +} + void KdenliveDoc::loadSequenceGroupsAndGuides(const QUuid &uuid) { Q_ASSERT(m_timelines.find(uuid) != m_timelines.end()); diff --git a/src/doc/kdenlivedoc.h b/src/doc/kdenlivedoc.h index 7ec22e4402..af50024be6 100644 --- a/src/doc/kdenlivedoc.h +++ b/src/doc/kdenlivedoc.h @@ -299,6 +299,8 @@ public: /** @brief Set the autoclose attribute to all playlists in @param doc. * This is eg. needed for rendering, as the process would not stop at the end of the playlist if it was not closed */ static void setAutoclosePlaylists(QDomDocument &doc); + /** @brief Check that the timelines hash have not changed between saved version and current status */ + bool checkConsistency(); protected: static int next_id; /// next valid id to assign diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp index 5ddddbd463..466355def4 100644 --- a/src/effects/effectstack/model/effectstackmodel.cpp +++ b/src/effects/effectstack/model/effectstackmodel.cpp @@ -1232,7 +1232,6 @@ bool EffectStackModel::checkConsistency() ct++; } } - return true; } diff --git a/src/project/projectmanager.cpp b/src/project/projectmanager.cpp index 32d0de00d6..d50186bb4b 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" +#include "jobs/cliploadtask.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "monitor/monitormanager.h" @@ -339,7 +340,6 @@ bool ProjectManager::testSaveFileAs(const QString &outputFileName) // QString scene = m_activeTimelineModel->sceneList(saveFolder); int duration = m_activeTimelineModel->duration(); QString scene = pCore->projectItemModel()->sceneList(saveFolder, QString(), QString(), m_activeTimelineModel->tractor(), duration); - QSaveFile file(outputFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qDebug() << "////// ERROR writing to file: " << outputFileName; @@ -412,7 +412,6 @@ bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) pCore->bin()->cleanDocument(); pCore->mixer()->unsetModel(); delete m_project; - m_project = nullptr; } else { pCore->projectItemModel()->clean(); // Close all timelines @@ -420,8 +419,8 @@ bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) for (auto &uid : uuids) { m_project->closeTimeline(uid); } - m_project = nullptr; } + m_project = nullptr; return true; } @@ -816,6 +815,11 @@ void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale, bool isBa openTimeline(binId, uuid); } } + // Now that sequence clips are fully built, fetch thumbnails + const QStringList sequenceIds = pCore->projectItemModel()->getAllSequenceClips().values(); + for (auto &id : sequenceIds) { + ClipLoadTask::start({ObjectType::BinClip, id.toInt()}, QDomElement(), true, -1, -1, this); + } // Raise last active timeline QUuid activeUuid(m_project->getDocumentProperty(QStringLiteral("activetimeline"))); if (activeUuid.isNull()) { @@ -1621,10 +1625,6 @@ bool ProjectManager::openTimeline(const QString &id, const QUuid &uuid, int posi std::shared_ptr timelineModel = TimelineItemModel::construct(uuid, m_project->commandStack()); m_project->addTimeline(uuid, timelineModel); TimelineWidget *timeline = nullptr; - if (pCore->window()) { - // Create tab widget - pCore->window()->openTimeline(uuid, clip->clipName(), timelineModel, pCore->monitorManager()->projectMonitor()->getControllerProxy()); - } if (internalLoad) { qDebug() << "QQQQQQQQQQQQQQQQQQQQ\nINTERNAL SEQUENCE LOAD\n\nQQQQQQQQQQQQQQQQQQQQQQ"; qDebug() << "============= LOADING INTERNAL PLAYLIST: " << uuid; @@ -1740,8 +1740,12 @@ bool ProjectManager::openTimeline(const QString &id, const QUuid &uuid, int posi } updateSequenceProducer(uuid, prod); clip->setProducer(prod, false, false); + m_project->loadSequenceGroupsAndGuides(uuid); + } + if (pCore->window()) { + // Create tab widget + pCore->window()->openTimeline(uuid, clip->clipName(), timelineModel, pCore->monitorManager()->projectMonitor()->getControllerProxy()); } - m_project->loadSequenceGroupsAndGuides(uuid); int activeTrackPosition = m_project->getSequenceProperty(uuid, QStringLiteral("activeTrack"), QString::number(-1)).toInt(); if (timeline == nullptr) { diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index 396663d9ef..1ecd146ca5 100644 --- a/src/timeline2/model/clipmodel.cpp +++ b/src/timeline2/model/clipmodel.cpp @@ -1348,7 +1348,7 @@ QDomElement ClipModel::toXml(QDomDocument &document) bool ClipModel::checkConsistency() { if (!m_effectStack->checkConsistency()) { - qDebug() << "Consistency check failed for effecstack"; + qDebug() << "Consistency check failed for effectstack"; return false; } if (m_currentTrackId == -1) { @@ -1356,7 +1356,11 @@ bool ClipModel::checkConsistency() return true; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); - auto instances = binClip->timelineInstances(); + QUuid timelineUuid; + if (auto ptr = m_parent.lock()) { + timelineUuid = ptr->uuid(); + } + auto instances = binClip->timelineInstances(timelineUuid); bool found = instances.contains(m_id); if (!found) { qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence: " << m_id << ", CURRENT TRACK: " << m_currentTrackId; diff --git a/tests/dataset/test-nesting-effects.kdenlive b/tests/dataset/test-nesting-effects.kdenlive new file mode 100644 index 0000000000..5774af7092 --- /dev/null +++ b/tests/dataset/test-nesting-effects.kdenlive @@ -0,0 +1,796 @@ + + + + + 5 + pause + red.mpg + avformat-novalidate + 2 + video + 25 + 0 + 720 + 576 + 0 + yuv420p + 1.09202 + 601 + mpeg1video + MPEG-1 video + 104857200 + audio + s16p + 48000 + 2 + mp2 + MP2 (MPEG audio layer 2) + 384000 + 1 + 178 + 163 + 1 + 0 + 25 + 1 + 601 + 2 + 1 + 720 + 576 + mpeg + + 0 + 1 + 0 + -1 + 4 + 0 + 22528 + 8843808b89cc22ece225bdab6b6e4f21 + 1 + -1 + + + 2147483647 + continue + black + 1 + color + black_track + rgba + 0 + + + 1 + + + 1 + + + 1 + 67 + 1 + 0 + + + + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 1 + + + + 5 + pause + red.mpg + avformat-novalidate + 1 + 1 + 0 + + 0 + -1 + 4 + 0 + 22528 + 8843808b89cc22ece225bdab6b6e4f21 + 1 + 2 + video + 25 + 0 + 720 + 576 + 0 + yuv420p + 1.09202 + 601 + mpeg1video + MPEG-1 video + 104857200 + audio + s16p + 48000 + 2 + mp2 + MP2 (MPEG audio layer 2) + 384000 + 178 + 163 + 25 + 1 + 601 + 2 + 1 + 720 + 576 + mpeg + was here + 0 + 1 + 0 + 1 + + + 2147483647 + continue + 0 + 1 + color + black_track + rgba + 0 + + + + + 1 + 67 + 1 + + + + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 0 + + + + 5 + pause + red.mpg + avformat-novalidate + 1 + 1 + 0 + + 0 + -1 + 4 + 0 + 22528 + 8843808b89cc22ece225bdab6b6e4f21 + 1 + 2 + video + 25 + 0 + 720 + 576 + 0 + yuv420p + 1.09202 + 601 + mpeg1video + MPEG-1 video + 104857200 + audio + s16p + 48000 + 2 + mp2 + MP2 (MPEG audio layer 2) + 384000 + 178 + 163 + 25 + 1 + 601 + 2 + 1 + 720 + 576 + mpeg + was here + 0 + 1 + 0 + 1 + + + + 4 + + + + + 1 + 67 + 1 + + + + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 0 + + + + 5 + pause + red.mpg + avformat-novalidate + 1 + 1 + 0 + + 0 + -1 + 4 + 0 + 22528 + 8843808b89cc22ece225bdab6b6e4f21 + 1 + 2 + video + 25 + 0 + 720 + 576 + 0 + yuv420p + 1.09202 + 601 + mpeg1video + MPEG-1 video + 104857200 + audio + s16p + 48000 + 2 + mp2 + MP2 (MPEG audio layer 2) + 384000 + 178 + 163 + 25 + 1 + 601 + 2 + 1 + 720 + 576 + mpeg + was here + 0 + 1 + 1 + 0 + + + + 4 + 0 + + 226 + 150 + sepia + sepia + 0 + + + + + + 67 + 1 + + + + + + + + + 67 + 1 + + + + + + + 00:00:00.200 + 5 + Sequence 2 + + {ba2b281d-29ba-40ca-8cdf-90d5cc311a41} + 17 + 5 + 0 + a99f5b4b8b22264335637be74eee19cb + 2 + 2 + {3638a297-8a87-4e73-91f0-882e770efeb9} + 1 + 1 + 4 + 4 + 1 + 0 + 75 + 0 + [ + { + "children": [ + { + "data": "2:0", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "1:0", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + } +] + + [ +] + + 0 + 0 + 0 + + + + + + + 0 + 1 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 2 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 3 + 0.1 + frei0r.cairoblend + frei0r.cairoblend + 237 + 1 + + + 0 + 4 + 0.1 + frei0r.cairoblend + frei0r.cairoblend + 237 + 1 + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + + 1 + + 4 + + + 5 + 5 + + + + 1 + + + 1 + 67 + 1 + 0 + + + + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 1 + + + + 5 + pause + red.mpg + avformat-novalidate + 1 + 1 + 0 + + 0 + -1 + 4 + 0 + 22528 + 8843808b89cc22ece225bdab6b6e4f21 + 1 + 2 + video + 25 + 0 + 720 + 576 + 0 + yuv420p + 1.09202 + 601 + mpeg1video + MPEG-1 video + 104857200 + audio + s16p + 48000 + 2 + mp2 + MP2 (MPEG audio layer 2) + 384000 + 178 + 163 + 25 + 1 + 601 + 2 + 1 + 720 + 576 + mpeg + was here + 0 + 1 + 1 + 0 + + + + 4 + + + 5 + + + + + 67 + 1 + 0 + + + + + + + + + 67 + 1 + 0 + + + + + + + 00:00:00:10 + 10 + Sequence 1 + + {3638a297-8a87-4e73-91f0-882e770efeb9} + 17 + 3 + 0 + 292891f0f2ed4aab81ab0236f74494b4 + 2 + 2 + 0 + 0 + 75 + 0 + 2 + 1 + {3638a297-8a87-4e73-91f0-882e770efeb9} + 1 + 1 + 0 + 4 + 4 + 1 + 2 + 1 + [ + { + "children": [ + { + "data": "2:0", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "1:0", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "2:5", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "1:5", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + } +] + + [ +] + + + + + + + + 0 + 1 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 2 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 3 + 0.1 + frei0r.cairoblend + 1 + 237 + + + 0 + 4 + 0.1 + frei0r.cairoblend + 1 + 237 + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + + Sequences + {3638a297-8a87-4e73-91f0-882e770efeb9} + Samples/Testing?/ + 1 + 1688711817958 + 0 + 0 + 0 + ./;;.LRV;./;;.MP4 + 0 + 0 + [ + { + "color": "#9b59b6", + "comment": "Category 1", + "index": 0 + }, + { + "color": "#3daee9", + "comment": "Category 2", + "index": 1 + }, + { + "color": "#1abc9c", + "comment": "Category 3", + "index": 2 + }, + { + "color": "#1cdc9a", + "comment": "Category 4", + "index": 3 + }, + { + "color": "#c9ce3b", + "comment": "Category 5", + "index": 4 + }, + { + "color": "#fdbc4b", + "comment": "Category 6", + "index": 5 + }, + { + "color": "#f39c1f", + "comment": "Category 7", + "index": 6 + }, + { + "color": "#f47750", + "comment": "Category 8", + "index": 7 + }, + { + "color": "#da4453", + "comment": "Category 9", + "index": 8 + } +] + + 23.07.70 + {3638a297-8a87-4e73-91f0-882e770efeb9};{ba2b281d-29ba-40ca-8cdf-90d5cc311a41} + + + atsc_1080p_25 + + 2000 + 800 + 1000 + + 640 + 30000 + {3638a297-8a87-4e73-91f0-882e770efeb9} + 1.1 + 2 + 5 + + 1 + + + + + + 1 + + + diff --git a/tests/nestingtest.cpp b/tests/nestingtest.cpp index a34c45409a..7b3be882ea 100644 --- a/tests/nestingtest.cpp +++ b/tests/nestingtest.cpp @@ -18,6 +18,7 @@ using namespace fakeit; TEST_CASE("Open and Close Sequence", "[OCS]") { auto binModel = pCore->projectItemModel(); + Q_ASSERT(binModel->clipsCount() == 0); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); @@ -103,6 +104,7 @@ TEST_CASE("Open and Close Sequence", "[OCS]") TEST_CASE("Save File With 2 Sequences", "[SF2]") { auto binModel = pCore->projectItemModel(); + Q_ASSERT(binModel->clipsCount() == 0); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); @@ -375,3 +377,103 @@ TEST_CASE("Save File With 2 Sequences", "[SF2]") pCore->projectManager()->closeCurrentDocument(false, false); } } + +TEST_CASE("Save File, Reopen and check for corruption", "[SF3]") +{ + auto binModel = pCore->projectItemModel(); + Q_ASSERT(binModel->clipsCount() == 0); + binModel->clean(); + std::shared_ptr undoStack = std::make_shared(nullptr); + + SECTION("Open and simply save file") + { + QString path = sourcesPath + "/dataset/test-nesting-effects.kdenlive"; + QUrl openURL = QUrl::fromLocalFile(path); + + QUndoGroup *undoGroup = new QUndoGroup(); + undoGroup->addStack(undoStack.get()); + DocOpenResult openResults = KdenliveDoc::Open(openURL, QDir::temp().path(), undoGroup, false, nullptr); + REQUIRE(openResults.isSuccessful() == true); + std::unique_ptr openedDoc = openResults.getDocument(); + + pCore->projectManager()->m_project = openedDoc.get(); + const QUuid uuid = openedDoc->uuid(); + QDateTime documentDate = QFileInfo(openURL.toLocalFile()).lastModified(); + pCore->projectManager()->updateTimeline(0, false, QString(), QString(), documentDate, 0); + QMap allSequences = binModel->getAllSequenceClips(); + const QString firstSeqId = allSequences.value(uuid); + pCore->projectManager()->openTimeline(firstSeqId, uuid); + std::shared_ptr timeline = openedDoc->getTimeline(uuid); + pCore->projectManager()->testSetActiveDocument(openedDoc.get(), timeline); + // Now reopen all timeline sequences + QList allUuids = allSequences.keys(); + for (auto &u : allUuids) { + if (u == uuid) { + continue; + } + const QString id = allSequences.value(u); + pCore->projectManager()->openTimeline(id, u); + } + + REQUIRE(openedDoc->checkConsistency()); + // Save file + QDir dir = QDir::temp(); + pCore->projectManager()->testSaveFileAs(dir.absoluteFilePath(QStringLiteral("test-nest.kdenlive"))); + pCore->projectManager()->closeCurrentDocument(false, false); + } + SECTION("Reopen and check in/out points") + { + // Create new document + // We mock the project class so that the undoStack function returns our undoStack, and our mocked document + KdenliveDoc::next_id = 0; + QString saveFile = QDir::temp().absoluteFilePath(QStringLiteral("test-nest.kdenlive")); + QUrl openURL = QUrl::fromLocalFile(saveFile); + + Mock pmMock; + When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); + When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))); + QUndoGroup *undoGroup = new QUndoGroup(); + undoGroup->addStack(undoStack.get()); + DocOpenResult openResults = KdenliveDoc::Open(openURL, QDir::temp().path(), undoGroup, false, nullptr); + REQUIRE(openResults.isSuccessful() == true); + std::unique_ptr openedDoc = openResults.getDocument(); + + pCore->projectManager()->m_project = openedDoc.get(); + const QUuid uuid = openedDoc->uuid(); + QDateTime documentDate = QFileInfo(openURL.toLocalFile()).lastModified(); + pCore->projectManager()->updateTimeline(0, false, QString(), QString(), documentDate, 0); + + QMap allSequences = binModel->getAllSequenceClips(); + const QString firstSeqId = allSequences.value(uuid); + pCore->projectManager()->openTimeline(firstSeqId, uuid); + std::shared_ptr timeline = openedDoc->getTimeline(uuid); + // Now reopen all timeline sequences + QList allUuids = allSequences.keys(); + // Collect saved hashes + QMap timelineHashes; + for (auto &u : allUuids) { + timelineHashes.insert(u, openedDoc->getSequenceProperty(u, QStringLiteral("timelineHash"))); + qDebug() << ":::: READING TIMELINE HASH FOR: " << u << " = " << openedDoc->getSequenceProperty(u, QStringLiteral("timelineHash")); + } + for (auto &u : allUuids) { + if (u == uuid) { + continue; + } + const QString id = allSequences.value(u); + pCore->projectManager()->openTimeline(id, u); + } + pCore->projectManager()->testSetActiveDocument(openedDoc.get(), timeline); + REQUIRE(openedDoc->checkConsistency()); + + for (auto &u : allUuids) { + QByteArray updatedHex = openedDoc->getTimeline(u)->timelineHash().toHex(); + REQUIRE(updatedHex == timelineHashes.value(u)); + } + + QDir dir = QDir::temp(); + QFile::remove(dir.absoluteFilePath(QStringLiteral("test-nest.kdenlive"))); + // binModel->clean(); + // pCore->m_projectManager = nullptr; + pCore->projectManager()->closeCurrentDocument(false, false); + } +} diff --git a/tests/tests_definitions.h.in b/tests/tests_definitions.h.in index 6ca789d31d..c320cefb0a 100644 --- a/tests/tests_definitions.h.in +++ b/tests/tests_definitions.h.in @@ -3,10 +3,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include -<<<<<<< HEAD -======= ->>>>>>> c39fc17768d72eac9e49c6041d2ff5f301efb8c3 static const QString sourcesPath("@TEST_SOURCES@");