diff --git a/src/doc/documentchecker.cpp b/src/doc/documentchecker.cpp index ed1741f15..313d24655 100644 --- a/src/doc/documentchecker.cpp +++ b/src/doc/documentchecker.cpp @@ -1,1302 +1,1303 @@ /*************************************************************************** * Copyright (C) 2008 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 "documentchecker.h" #include "bin/binplaylist.hpp" #include "effects/effectsrepository.hpp" #include "kdenlivesettings.h" #include "kthumb.h" #include "titler/titlewidget.h" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include const int hashRole = Qt::UserRole; const int sizeRole = Qt::UserRole + 1; const int idRole = Qt::UserRole + 2; const int statusRole = Qt::UserRole + 3; const int typeRole = Qt::UserRole + 4; const int typeOriginalResource = Qt::UserRole + 5; const int clipTypeRole = Qt::UserRole + 6; const int CLIPMISSING = 0; const int CLIPOK = 1; const int CLIPPLACEHOLDER = 2; const int PROXYMISSING = 4; const int SOURCEMISSING = 5; const int LUMAMISSING = 10; const int LUMAOK = 11; const int LUMAPLACEHOLDER = 12; enum MISSINGTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 }; DocumentChecker::DocumentChecker(QUrl url, const QDomDocument &doc) : m_url(std::move(url)) , m_doc(doc) , m_dialog(nullptr) { } QMap DocumentChecker::getLumaPairs() const { QMap lumaSearchPairs; lumaSearchPairs.insert(QStringLiteral("luma"), QStringLiteral("resource")); lumaSearchPairs.insert(QStringLiteral("movit.luma_mix"), QStringLiteral("resource")); lumaSearchPairs.insert(QStringLiteral("composite"), QStringLiteral("luma")); lumaSearchPairs.insert(QStringLiteral("region"), QStringLiteral("composite.luma")); return lumaSearchPairs; } bool DocumentChecker::hasErrorInClips() { int max; QDomElement baseElement = m_doc.documentElement(); QString root = baseElement.attribute(QStringLiteral("root")); if (!root.isEmpty()) { QDir dir(root); if (!dir.exists()) { // Looks like project was moved, try recovering root from current project url m_rootReplacement.first = dir.absolutePath() + QDir::separator(); root = m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); baseElement.setAttribute(QStringLiteral("root"), root); root = QDir::cleanPath(root) + QDir::separator(); m_rootReplacement.second = root; } else { root = QDir::cleanPath(root) + QDir::separator(); } } // Check if strorage folder for temp files exists QString storageFolder; QDir projectDir(m_url.adjusted(QUrl::RemoveFilename).toLocalFile()); QString documentid; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { documentid = Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid")); if (documentid.isEmpty()) { // invalid document id, recreate one documentid = QString::number(QDateTime::currentMSecsSinceEpoch()); // TODO: Warn on invalid doc id Xml::setXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid"), documentid); } storageFolder = Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder")); if (!storageFolder.isEmpty() && QFileInfo(storageFolder).isRelative()) { storageFolder.prepend(root); } if (!storageFolder.isEmpty() && !QFile::exists(storageFolder) && projectDir.exists(documentid)) { storageFolder = projectDir.absolutePath(); Xml::setXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder"), projectDir.absoluteFilePath(documentid)); m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } break; } } QDomNodeList documentProducers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomElement profile = baseElement.firstChildElement(QStringLiteral("profile")); bool hdProfile = true; if (!profile.isNull()) { if (profile.attribute(QStringLiteral("width")).toInt() < 1000) { hdProfile = false; } } // List clips whose proxy is missing QList missingProxies; // List clips who have a working proxy but no source clip QList missingSources; m_safeImages.clear(); m_safeFonts.clear(); m_missingFonts.clear(); max = documentProducers.count(); QStringList verifiedPaths; QStringList missingPaths; QStringList serviceToCheck; serviceToCheck << QStringLiteral("kdenlivetitle") << QStringLiteral("qimage") << QStringLiteral("pixbuf") << QStringLiteral("timewarp") << QStringLiteral("framebuffer") << QStringLiteral("xml") << QStringLiteral("qtext"); for (int i = 0; i < max; ++i) { QDomElement e = documentProducers.item(i).toElement(); QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); if (!service.startsWith(QLatin1String("avformat")) && !serviceToCheck.contains(service)) { continue; } if (service == QLatin1String("qtext")) { QString text = Xml::getXmlProperty(e, QStringLiteral("text")); if (text == QLatin1String("INVALID")) { // Warning, this is an invalid clip (project saved with missing source) // Check if source clip is now available QString resource = Xml::getXmlProperty(e, QStringLiteral("warp_resource")); if (resource.isEmpty()) { resource = Xml::getXmlProperty(e, QStringLiteral("resource")); } // Make sure to have absolute paths if (QFileInfo(resource).isRelative()) { resource.prepend(root); } if (QFile::exists(resource)) { // Reset to original service Xml::removeXmlProperty(e, QStringLiteral("text")); QString original_service = Xml::getXmlProperty(e, QStringLiteral("kdenlive:orig_service")); if (!original_service.isEmpty()) { Xml::setXmlProperty(e, QStringLiteral("mlt_service"), original_service); } else { // Try to guess service if (Xml::hasXmlProperty(e, QStringLiteral("ttl"))) { Xml::setXmlProperty(e, QStringLiteral("mlt_service"), QStringLiteral("qimage")); } else if (resource.endsWith(QLatin1String(".kdenlivetitle"))) { Xml::setXmlProperty(e, QStringLiteral("mlt_service"), QStringLiteral("kdenlivetitle")); } else if (resource.endsWith(QLatin1String(".kdenlive")) || resource.endsWith(QLatin1String(".mlt"))) { Xml::setXmlProperty(e, QStringLiteral("mlt_service"), QStringLiteral("xml")); } else { Xml::setXmlProperty(e, QStringLiteral("mlt_service"), QStringLiteral("avformat")); } } } continue; } checkMissingImagesAndFonts(QStringList(), QStringList(Xml::getXmlProperty(e, QStringLiteral("family"))), e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } if (service == QLatin1String("kdenlivetitle")) { // TODO: Check is clip template is missing (xmltemplate) or hash changed QString xml = Xml::getXmlProperty(e, QStringLiteral("xmldata")); QStringList images = TitleWidget::extractImageList(xml); QStringList fonts = TitleWidget::extractFontList(xml); checkMissingImagesAndFonts(images, fonts, e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); if (resource.isEmpty()) { continue; } if (service == QLatin1String("timewarp")) { // slowmotion clip, trim speed info resource = Xml::getXmlProperty(e, QStringLiteral("warp_resource")); } else if (service == QLatin1String("framebuffer")) { // slowmotion clip, trim speed info resource = resource.section(QLatin1Char('?'), 0, 0); } // Make sure to have absolute paths if (QFileInfo(resource).isRelative()) { resource.prepend(root); } if (verifiedPaths.contains(resource)) { // Don't check same url twice (for example track producers) if (missingPaths.contains(resource)) { m_missingClips.append(e); } continue; } QString proxy = Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")); if (proxy.length() > 1) { if (QFileInfo(proxy).isRelative()) { proxy.prepend(root); } if (!QFile::exists(proxy)) { // Missing clip found // Check if proxy exists in current storage folder bool fixed = false; if (!storageFolder.isEmpty()) { QDir dir(storageFolder + QStringLiteral("/proxy/")); if (dir.exists(QFileInfo(proxy).fileName())) { QString updatedPath = dir.absoluteFilePath(QFileInfo(proxy).fileName()); fixProxyClip(e.attribute(QStringLiteral("id")), Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")), updatedPath, documentProducers); fixed = true; } } if (!fixed) { missingProxies.append(e); } } QString original = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); if (QFileInfo(original).isRelative()) { original.prepend(root); } // Check for slideshows bool slideshow = original.contains(QStringLiteral("/.all.")) || original.contains(QLatin1Char('?')) || original.contains(QLatin1Char('%')); if (slideshow && !Xml::getXmlProperty(e, QStringLiteral("ttl")).isEmpty()) { original = QFileInfo(original).absolutePath(); } if (!QFile::exists(original)) { // clip has proxy but original clip is missing missingSources.append(e); - missingPaths.append(resource); + missingPaths.append(original); } verifiedPaths.append(resource); continue; } // Check for slideshows bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if ((service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) && slideshow) { resource = QFileInfo(resource).absolutePath(); } if (!QFile::exists(resource)) { // Missing clip found, make sure to omit timeline preview if (QFileInfo(resource).absolutePath().endsWith(QString("/%1/preview").arg(documentid))) { // This is a timeline preview missing chunk, ignore } else { m_missingClips.append(e); missingPaths.append(resource); } } // Make sure we don't query same path twice verifiedPaths.append(resource); } // Get list of used Luma files QStringList missingLumas; QStringList filesToCheck; QString filePath; QMap lumaSearchPairs = getLumaPairs(); QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); max = trans.count(); for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(transition, lumaSearchPairs.value(service)); } if (!luma.isEmpty() && !filesToCheck.contains(luma)) { filesToCheck.append(luma); } } QMap autoFixLuma; QString lumaPath; QString lumaMltPath; // Check existence of luma files for (const QString &lumafile : filesToCheck) { filePath = lumafile; if (QFileInfo(filePath).isRelative()) { filePath.prepend(root); } if (!QFile::exists(filePath)) { QString lumaName = filePath.section(QLatin1Char('/'), -1); // check if this was an old format luma, not in correct folder QString fixedLuma = filePath.section(QLatin1Char('/'), 0, -2); lumaName.prepend(hdProfile ? QStringLiteral("/HD/") : QStringLiteral("/PAL/")); fixedLuma.append(lumaName); if (QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); continue; } // Check Kdenlive folder if (lumaPath.isEmpty()) { QDir dir(QCoreApplication::applicationDirPath()); dir.cdUp(); dir.cd(QStringLiteral("share/kdenlive/lumas/")); lumaPath = dir.absolutePath() + QStringLiteral("/"); } lumaName = filePath.section(QLatin1Char('/'), -2); lumaName.prepend(lumaPath); if (QFile::exists(lumaName)) { autoFixLuma.insert(filePath, lumaName); continue; } // Check MLT folder if (lumaMltPath.isEmpty()) { QDir dir(KdenliveSettings::mltpath()); dir.cd(QStringLiteral("../lumas/")); lumaMltPath = dir.absolutePath() + QStringLiteral("/"); } lumaName = filePath.section(QLatin1Char('/'), -2); lumaName.prepend(lumaMltPath); if (QFile::exists(lumaName)) { autoFixLuma.insert(filePath, lumaName); continue; } if (filePath.endsWith(QLatin1String(".pgm"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".png"); } else if (filePath.endsWith(QLatin1String(".png"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".pgm"); } if (!fixedLuma.isEmpty() && QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); } else { missingLumas.append(lumafile); } } } if (!autoFixLuma.isEmpty()) { for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(transition, lumaSearchPairs.value(service)); } if (!luma.isEmpty() && autoFixLuma.contains(luma)) { updateProperty(transition, lumaSearchPairs.value(service), autoFixLuma.value(luma)); } } } // Check for missing effects QDomNodeList effs = m_doc.elementsByTagName(QStringLiteral("filter")); max = effs.count(); QStringList filters; for (int i = 0; i < max; ++i) { QDomElement transition = effs.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("kdenlive_id")); filters << service; } QStringList processed; for (const QString &id : filters) { if (!processed.contains(id) && !EffectsRepository::get()->exists(id)) { m_missingFilters << id; } processed << id; } if (!m_missingFilters.isEmpty()) { // Delete missing effects for (int i = 0; i < effs.count(); ++i) { QDomElement e = effs.item(i).toElement(); if (m_missingFilters.contains(getProperty(e, QStringLiteral("kdenlive_id")))) { // Remove clip e.parentNode().removeChild(e); --i; } } } if (m_missingClips.isEmpty() && missingLumas.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty() && m_missingFonts.isEmpty() && m_missingFilters.isEmpty()) { return false; } m_dialog = new QDialog(); m_dialog->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_ui.setupUi(m_dialog); for (const QString &l : missingLumas) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); item->setData(0, idRole, l); item->setData(0, statusRole, LUMAMISSING); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_missingClips.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty()); max = m_missingClips.count(); m_missingProxyIds.clear(); QStringList processedIds; for (int i = 0; i < max; ++i) { QDomElement e = m_missingClips.at(i).toElement(); QString clipType; ClipType::ProducerType type; int status = CLIPMISSING; const QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); QString resource = service == QLatin1String("timewarp") ? Xml::getXmlProperty(e, QStringLiteral("warp_resource")) : Xml::getXmlProperty(e, QStringLiteral("resource")); bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if (service == QLatin1String("avformat") || service == QLatin1String("avformat-novalidate") || service == QLatin1String("framebuffer") || service == QLatin1String("timewarp")) { clipType = i18n("Video clip"); type = ClipType::AV; } else if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { if (slideshow) { clipType = i18n("Slideshow clip"); type = ClipType::SlideShow; } else { clipType = i18n("Image clip"); type = ClipType::Image; } } else if (service == QLatin1String("mlt") || service == QLatin1String("xml")) { clipType = i18n("Playlist clip"); type = ClipType::Playlist; } else if (e.tagName() == QLatin1String("missingtitle")) { clipType = i18n("Title Image"); status = TITLE_IMAGE_ELEMENT; type = ClipType::Text; } else { clipType = i18n("Unknown"); type = ClipType::Unknown; } // Newer project format QString clipId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (!clipId.isEmpty()) { if (processedIds.contains(clipId)) { continue; } processedIds << clipId; } else { // Older project file format clipId = e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (processedIds.contains(clipId)) { continue; } processedIds << clipId; } QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPMISSING); item->setData(0, clipTypeRole, (int)type); item->setData(0, idRole, e.attribute(QStringLiteral("id"))); item->setToolTip(0, i18n("Missing item")); if (status == TITLE_IMAGE_ELEMENT) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setToolTip(1, e.attribute(QStringLiteral("name"))); QString imageResource = e.attribute(QStringLiteral("resource")); item->setData(0, typeRole, status); item->setData(0, typeOriginalResource, e.attribute(QStringLiteral("resource"))); if (!m_rootReplacement.first.isEmpty()) { if (imageResource.startsWith(m_rootReplacement.first)) { imageResource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(imageResource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, imageResource); } else { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); if (QFileInfo(resource).isRelative()) { resource.prepend(root); } item->setData(0, hashRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash"))); item->setData(0, sizeRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size"))); if (!m_rootReplacement.first.isEmpty()) { if (resource.startsWith(m_rootReplacement.first)) { resource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(resource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, resource); } } for (const QString &font : m_missingFonts) { QString clipType = i18n("Title Font"); QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPPLACEHOLDER); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); QString newft = QFontInfo(QFont(font)).family(); item->setText(1, i18n("%1 will be replaced by %2", font, newft)); item->setData(0, typeRole, TITLE_FONT_ELEMENT); } QString infoLabel; if (!m_missingClips.isEmpty()) { infoLabel = i18n("The project file contains missing clips or files."); } if (!m_missingFilters.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18np("Missing effect: %2 will be removed from project.", "Missing effects: %2 will be removed from project.", m_missingFilters.count(), m_missingFilters.join(","))); } if (!missingProxies.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18n("Missing proxies will be recreated after opening.")); } if (!missingSources.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18np("The project file contains a missing clip, you can still work with its proxy.", "The project file contains %1 missing clips, you can still work with their proxies.", missingSources.count())); } if (!infoLabel.isEmpty()) { m_ui.infoLabel->setText(infoLabel); } else { m_ui.infoLabel->setVisible(false); } m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty()); m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty() || !missingSources.isEmpty()); m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty()); // Check missing proxies max = missingProxies.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip")); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setText( 1, i18np("%1 missing proxy clip, will be recreated on project opening", "%1 missing proxy clips, will be recreated on project opening", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, PROXYMISSING); item->setToolTip(0, i18n("Missing proxy")); } for (int i = 0; i < max; ++i) { QDomElement e = missingProxies.at(i).toElement(); QString realPath = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); - QString id = e.attribute(QStringLiteral("id")); + QString id = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); m_missingProxyIds << id; // Tell Kdenlive to recreate proxy e.setAttribute(QStringLiteral("_replaceproxy"), QStringLiteral("1")); // Replace proxy url with real clip in MLT producers QDomElement mltProd; int prodsCount = documentProducers.count(); for (int j = 0; j < prodsCount; ++j) { mltProd = documentProducers.at(j).toElement(); QString prodId = mltProd.attribute(QStringLiteral("id")); - QString parentId = prodId; + QString parentId = Xml::getXmlProperty(mltProd, QStringLiteral("kdenlive:id")); bool slowmotion = false; if (parentId.startsWith(QLatin1String("slowmotion"))) { slowmotion = true; parentId = parentId.section(QLatin1Char(':'), 1, 1); } if (parentId.contains(QLatin1Char('_'))) { parentId = parentId.section(QLatin1Char('_'), 0, 0); } if (parentId == id) { // Hit, we must replace url QString suffix; - QString resource = Xml::getXmlProperty(mltProd, QStringLiteral("resource")); if (slowmotion) { + QString resource = Xml::getXmlProperty(mltProd, QStringLiteral("resource")); suffix = QLatin1Char('?') + resource.section(QLatin1Char('?'), -1); } Xml::setXmlProperty(mltProd, QStringLiteral("resource"), realPath + suffix); - if (prodId == id) { - // Only set proxy property on master producer - Xml::setXmlProperty(mltProd, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); + Xml::setXmlProperty(mltProd, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); + if (missingPaths.contains(realPath)) { + // Proxy AND source missing + setProperty(mltProd, QStringLiteral("_placeholder"), QStringLiteral("1")); } } } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } // Check clips with available proxies but missing original source clips max = missingSources.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip")); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, SOURCEMISSING); item->setToolTip(0, i18n("Missing source clip")); for (int i = 0; i < max; ++i) { QDomElement e = missingSources.at(i).toElement(); QString realPath = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); QString id = e.attribute(QStringLiteral("id")); // Tell Kdenlive the source is missing e.setAttribute(QStringLiteral("_missingsource"), QStringLiteral("1")); QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip")); // qCDebug(KDENLIVE_LOG)<<"// Adding missing source clip: "<setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); subitem->setText(1, realPath); subitem->setData(0, hashRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash"))); subitem->setData(0, sizeRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size"))); subitem->setData(0, statusRole, CLIPMISSING); // int t = e.attribute("type").toInt(); subitem->setData(0, typeRole, Xml::getXmlProperty(e, QStringLiteral("mlt_service"))); subitem->setData(0, idRole, id); } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } m_ui.treeWidget->resizeColumnToContents(0); connect(m_ui.recursiveSearch, &QAbstractButton::pressed, this, &DocumentChecker::slotSearchClips); connect(m_ui.usePlaceholders, &QAbstractButton::pressed, this, &DocumentChecker::slotPlaceholders); connect(m_ui.removeSelected, &QAbstractButton::pressed, this, &DocumentChecker::slotDeleteSelected); connect(m_ui.treeWidget, &QTreeWidget::itemDoubleClicked, this, &DocumentChecker::slotEditItem); connect(m_ui.treeWidget, &QTreeWidget::itemSelectionChanged, this, &DocumentChecker::slotCheckButtons); // adjustSize(); if (m_ui.treeWidget->topLevelItem(0)) { m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0)); } checkStatus(); int acceptMissing = m_dialog->exec(); if (acceptMissing == QDialog::Accepted) { acceptDialog(); } return (acceptMissing != QDialog::Accepted); } DocumentChecker::~DocumentChecker() { delete m_dialog; } QString DocumentChecker::getProperty(const QDomElement &effect, const QString &name) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { return e.firstChild().nodeValue(); } } return QString(); } void DocumentChecker::updateProperty(const QDomElement &effect, const QString &name, const QString &value) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { e.firstChild().setNodeValue(value); break; } } } void DocumentChecker::setProperty(QDomElement &effect, const QString &name, const QString &value) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); bool found = false; for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { e.firstChild().setNodeValue(value); found = true; break; } } if (!found) { // create property QDomDocument doc = effect.ownerDocument(); QDomElement e = doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), name); QDomText val = doc.createTextNode(value); e.appendChild(val); effect.appendChild(e); } } void DocumentChecker::slotSearchClips() { // QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QString clipFolder = m_url.adjusted(QUrl::RemoveFilename).toLocalFile(); QString newpath = QFileDialog::getExistingDirectory(qApp->activeWindow(), i18n("Clips folder"), clipFolder); if (newpath.isEmpty()) { return; } int ix = 0; bool fixed = false; m_ui.recursiveSearch->setChecked(true); // TODO: make non modal QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); QDir searchDir(newpath); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { QTreeWidgetItem *subchild = child->child(j); QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString(), subchild->text(1)); if (!clipPath.isEmpty()) { fixed = true; subchild->setText(1, clipPath); subchild->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); subchild->setData(0, statusRole, CLIPOK); } } } else if (child->data(0, statusRole).toInt() == CLIPMISSING) { bool perfectMatch = true; ClipType::ProducerType type = (ClipType::ProducerType)child->data(0, clipTypeRole).toInt(); QString clipPath; if (type != ClipType::SlideShow) { // Slideshows cannot be found with hash / size clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString(), child->text(1)); } if (clipPath.isEmpty()) { clipPath = searchPathRecursively(searchDir, QUrl::fromLocalFile(child->text(1)).fileName(), type); perfectMatch = false; } if (!clipPath.isEmpty()) { fixed = true; child->setText(1, clipPath); child->setIcon(0, perfectMatch ? QIcon::fromTheme(QStringLiteral("dialog-ok")) : QIcon::fromTheme(QStringLiteral("dialog-warning"))); child->setData(0, statusRole, CLIPOK); } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { QString fileName = searchLuma(searchDir, child->data(0, idRole).toString()); if (!fileName.isEmpty()) { fixed = true; child->setText(1, fileName); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, LUMAOK); } } else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) { // Search missing title images QString missingFileName = QUrl::fromLocalFile(child->text(1)).fileName(); QString newPath = searchPathRecursively(searchDir, missingFileName); if (!newPath.isEmpty()) { // File found fixed = true; child->setText(1, newPath); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, CLIPOK); } } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.recursiveSearch->setChecked(false); m_ui.recursiveSearch->setEnabled(true); if (fixed) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } checkStatus(); } QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const { QDir searchPath(KdenliveSettings::mltpath()); QString fname = QUrl::fromLocalFile(file).fileName(); if (file.contains(QStringLiteral("PAL"))) { searchPath.cd(QStringLiteral("../lumas/PAL")); } else { searchPath.cd(QStringLiteral("../lumas/NTSC")); } QFileInfo result(searchPath, fname); if (result.exists()) { return result.filePath(); } // try to find luma in application path searchPath.setPath(QCoreApplication::applicationDirPath()); #ifdef Q_OS_WIN searchPath.cd(QStringLiteral("data/lumas")); #else searchPath.cd(QStringLiteral("../share/apps/kdenlive/lumas")); #endif result.setFile(searchPath, fname); if (result.exists()) { return result.filePath(); } // Try in Kdenlive's standard KDE path QString res = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("lumas/") + fname); if (!res.isEmpty()) { return res; } // Try in user's chosen folder return searchPathRecursively(dir, fname); } QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName, ClipType::ProducerType type) const { QString foundFileName; QStringList filters; if (type == ClipType::SlideShow) { if (fileName.contains(QLatin1Char('%'))) { filters << fileName.section(QLatin1Char('%'), 0, -2) + QLatin1Char('*'); } else { return QString(); } } else { filters << fileName; } QDir searchDir(dir); searchDir.setNameFilters(filters); QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable); if (!filesAndDirs.isEmpty()) { // File Found if (type == ClipType::SlideShow) { return searchDir.absoluteFilePath(fileName); } return searchDir.absoluteFilePath(filesAndDirs.at(0)); } searchDir.setNameFilters(QStringList()); filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName, type); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) const { if (matchSize.isEmpty() && matchHash.isEmpty()) { return searchPathRecursively(dir, QUrl::fromLocalFile(fileName).fileName()); } QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (QString::number(file.size()) == matchSize) { if (file.open(QIODevice::ReadOnly)) { /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString::fromLatin1(fileHash.toHex()) == matchHash) { return file.fileName(); } } } ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash, fileName); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int) { int t = item->data(0, typeRole).toInt(); if (t == TITLE_FONT_ELEMENT) { return; } //|| t == TITLE_IMAGE_ELEMENT) { QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(item->text(1)), m_dialog, i18n("Enter new location for file")); if (!url.isValid()) { return; } item->setText(1, url.toLocalFile()); ClipType::ProducerType type = (ClipType::ProducerType)item->data(0, clipTypeRole).toInt(); bool fixed = false; if (type == ClipType::SlideShow && QFile::exists(url.adjusted(QUrl::RemoveFilename).toLocalFile())) { fixed = true; } if (fixed || QFile::exists(url.toLocalFile())) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPOK); } else { item->setData(0, statusRole, LUMAOK); } checkStatus(); } else { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPMISSING); } else { item->setData(0, statusRole, LUMAMISSING); } checkStatus(); } } void DocumentChecker::acceptDialog() { QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int ix = 0; // prepare transitions QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); // prepare filters QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); // Mark document as modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { fixSourceClipItem(child->child(j), producers); } } else { fixClipItem(child, producers, trans); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } // QDialog::accept(); } void DocumentChecker::fixProxyClip(const QString &id, const QString &oldUrl, const QString &newUrl, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); // TODO: Slowmmotion clips if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { // fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (resource == oldUrl) { Xml::setXmlProperty(e, QStringLiteral("resource"), newUrl); } if (!Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")).isEmpty()) { // Only set originalurl on master producer Xml::setXmlProperty(e, QStringLiteral("kdenlive:proxy"), newUrl); } } } } void DocumentChecker::fixSourceClipItem(QTreeWidgetItem *child, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; // int t = child->data(0, typeRole).toInt(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString id = child->data(0, idRole).toString(); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); QString fixedResource = child->text(1); if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (!Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")).isEmpty()) { // Only set originalurl on master producer Xml::setXmlProperty(e, QStringLiteral("kdenlive:originalurl"), fixedResource); } if (m_missingProxyIds.contains(parentId)) { // Proxy is also missing, replace resource Xml::setXmlProperty(e, QStringLiteral("resource"), fixedResource); } } } } } void DocumentChecker::fixClipItem(QTreeWidgetItem *child, const QDomNodeList &producers, const QDomNodeList &trans) { QDomElement e, property; QDomNodeList properties; int t = child->data(0, typeRole).toInt(); QString id = child->data(0, idRole).toString(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString fixedResource = child->text(1); if (t == TITLE_IMAGE_ELEMENT) { // edit images embedded in titles for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip properties = e.childNodes(); for (int j = 0; j < properties.count(); ++j) { property = properties.item(j).toElement(); if (property.attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { QString xml = property.firstChild().nodeValue(); xml.replace(child->data(0, typeOriginalResource).toString(), fixedResource); property.firstChild().setNodeValue(xml); break; } } } } } else { // edit clip url /*for (int i = 0; i < infoproducers.count(); ++i) { e = infoproducers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip e.setAttribute("resource", child->text(1)); e.setAttribute("name", QUrl(child->text(1)).fileName()); e.removeAttribute("_missingsource"); break; } }*/ for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0) == id || e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1) == id || e.attribute(QStringLiteral("id")) == id) { // Fix clip QString resource = getProperty(e, QStringLiteral("resource")); QString service = getProperty(e, QStringLiteral("mlt_service")); QString updatedResource = fixedResource; if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { updatedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (service == QLatin1String("timewarp")) { updateProperty(e, QStringLiteral("warp_resource"), updatedResource); updatedResource.prepend(getProperty(e, QStringLiteral("warp_speed")) + QLatin1Char(':')); } updateProperty(e, QStringLiteral("resource"), updatedResource); } } } } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) { // QString id = child->data(0, idRole).toString(); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip setProperty(e, QStringLiteral("_placeholder"), QStringLiteral("1")); setProperty(e, QStringLiteral("kdenlive:orig_service"), getProperty(e, QStringLiteral("mlt_service"))); break; } } } else if (child->data(0, statusRole).toInt() == LUMAOK) { QMap lumaSearchPairs = getLumaPairs(); for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(trans.at(i).toElement(), lumaSearchPairs.value(service)); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { updateProperty(trans.at(i).toElement(), lumaSearchPairs.value(service), child->text(1)); // qCDebug(KDENLIVE_LOG) << "replace with; " << child->text(1); } } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { QMap lumaSearchPairs = getLumaPairs(); for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(trans.at(i).toElement(), lumaSearchPairs.value(service)); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { updateProperty(trans.at(i).toElement(), lumaSearchPairs.value(service), QString()); } } } } void DocumentChecker::slotPlaceholders() { int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == CLIPMISSING) { child->setData(0, statusRole, CLIPPLACEHOLDER); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { child->setData(0, statusRole, LUMAPLACEHOLDER); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } checkStatus(); } void DocumentChecker::checkStatus() { bool status = true; int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { int childStatus = child->data(0, statusRole).toInt(); if (childStatus == CLIPMISSING) { status = false; break; } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status); } void DocumentChecker::slotDeleteSelected() { if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) { return; } QStringList deletedIds; QStringList deletedLumas; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (QTreeWidgetItem *child : m_ui.treeWidget->selectedItems()) { int id = child->data(0, statusRole).toInt(); if (id == CLIPMISSING) { deletedIds.append(child->data(0, idRole).toString()); delete child; } else if (id == LUMAMISSING) { deletedLumas.append(child->data(0, idRole).toString()); delete child; } } if (!deletedLumas.isEmpty()) { QDomElement e; QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); QMap lumaSearchPairs = getLumaPairs(); for (const QString &lumaPath : deletedLumas) { for (int i = 0; i < transitions.count(); ++i) { e = transitions.item(i).toElement(); QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); QString resource; if (lumaSearchPairs.contains(service)) { resource = getProperty(e, lumaSearchPairs.value(service)); } if (!resource.isEmpty() && resource == lumaPath) { Xml::removeXmlProperty(e, lumaSearchPairs.value(service)); } } } } if (!deletedIds.isEmpty()) { QDomElement e; QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomNode mlt = m_doc.elementsByTagName(QStringLiteral("mlt")).at(0); QDomNode kdenlivedoc = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Remove clip mlt.removeChild(e); --i; } } for (int i = 0; i < playlists.count(); ++i) { QDomNodeList entries = playlists.at(i).toElement().elementsByTagName(QStringLiteral("entry")); for (int j = 0; j < entries.count(); ++j) { e = entries.item(j).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Replace clip with blank while (e.childNodes().count() > 0) { e.removeChild(e.firstChild()); } e.setTagName(QStringLiteral("blank")); e.removeAttribute(QStringLiteral("producer")); int length = e.attribute(QStringLiteral("out")).toInt() - e.attribute(QStringLiteral("in")).toInt(); e.setAttribute(QStringLiteral("length"), length); j--; } } } m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); checkStatus(); } } void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip) { QDomDocument doc; for (const QString &img : images) { if (m_safeImages.contains(img)) { continue; } if (!QFile::exists(img)) { QDomElement e = doc.createElement(QStringLiteral("missingtitle")); e.setAttribute(QStringLiteral("type"), TITLE_IMAGE_ELEMENT); e.setAttribute(QStringLiteral("resource"), img); e.setAttribute(QStringLiteral("id"), id); e.setAttribute(QStringLiteral("name"), baseClip); m_missingClips.append(e); } else { m_safeImages.append(img); } } for (const QString &fontelement : fonts) { if (m_safeFonts.contains(fontelement)) { continue; } QFont f(fontelement); ////qCDebug(KDENLIVE_LOG) << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family(); if (fontelement != QFontInfo(f).family()) { m_missingFonts << fontelement; } else { m_safeFonts.append(fontelement); } } } void DocumentChecker::slotCheckButtons() { if (m_ui.treeWidget->currentItem()) { QTreeWidgetItem *item = m_ui.treeWidget->currentItem(); int t = item->data(0, typeRole).toInt(); int s = item->data(0, statusRole).toInt(); if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) { m_ui.removeSelected->setEnabled(false); } else { m_ui.removeSelected->setEnabled(true); } } } diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index 5b849692d..ca3e196ff 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,970 +1,978 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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, see . */ #include "clipcontroller.h" #include "bin/model/markerlistmodel.hpp" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "profiles/profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include #include #include std::shared_ptr ClipController::mediaUnavailable; ClipController::ClipController(const QString &clipId, const std::shared_ptr &producer) : selectedEffectIndex(1) , m_audioThumbCreated(false) , m_masterProducer(producer) , m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr) , m_usesProxy(false) , m_audioInfo(nullptr) , m_videoIndex(0) , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_hasAudio(false) , m_hasVideo(false) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_properties) { setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); m_service = m_properties->get("mlt_service"); QString proxy = m_properties->get("kdenlive:proxy"); QString path = m_properties->get("resource"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); m_properties->set("resource", path.toUtf8().constData()); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative() && path != QLatin1String("")) { path.prepend(pCore->currentDoc()->documentRoot()); m_properties->set("resource", path.toUtf8().constData()); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); checkAudioVideo(); } else { m_producerLock.lock(); } } ClipController::~ClipController() { delete m_properties; m_masterProducer.reset(); } const QString ClipController::binId() const { return m_controllerBinId; } const std::unique_ptr &ClipController::audioInfo() const { return m_audioInfo; } void ClipController::addMasterProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::addmasterproducer"; QString documentRoot = pCore->currentDoc()->documentRoot(); m_masterProducer = producer; m_properties = new Mlt::Properties(m_masterProducer->get_properties()); int id = m_controllerBinId.toInt(); m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack()); if (!m_masterProducer->is_valid()) { m_masterProducer = ClipController::mediaUnavailable; m_producerLock.unlock(); qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { checkAudioVideo(); m_producerLock.unlock(); QString proxy = m_properties->get("kdenlive:proxy"); m_service = m_properties->get("mlt_service"); QString path = m_properties->get("resource"); m_usesProxy = false; if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); emitProducerChanged(m_controllerBinId, producer); setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); } connectEffectStack(); } namespace { QString producerXml(const std::shared_ptr &producer, bool includeMeta, bool includeProfile) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); if (!s.is_valid()) { return QString(); } int ignore = s.get_int("ignore_points"); if (ignore != 0) { s.set("ignore_points", 0); } c.set("time_format", "frames"); if (!includeMeta) { c.set("no_meta", 1); } if (!includeProfile) { c.set("no_profile", 1); } c.set("store", "kdenlive"); c.set("no_root", 1); c.set("root", "/"); c.connect(s); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return QString::fromUtf8(c.get("string")); } } // namespace void ClipController::getProducerXML(QDomDocument &document, bool includeMeta, bool includeProfile) { // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = producerXml(m_masterProducer, includeMeta, includeProfile); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { date = QFileInfo(m_path).lastModified(); m_videoIndex = -1; int audioIndex = -1; // special case: playlist with a proxy clip have to be detected separately if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) { audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_videoIndex == -1) { m_clipType = ClipType::Audio; } else { if (audioIndex == -1) { m_clipType = ClipType::Video; } else { m_clipType = ClipType::AV; } if (m_service == QLatin1String("avformat")) { m_properties->set("mlt_service", "avformat-novalidate"); } } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { m_clipType = ClipType::SlideShow; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Image; m_hasLimitedDuration = false; } } else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) { m_clipType = ClipType::Color; m_hasLimitedDuration = false; } else if (m_service == QLatin1String("kdenlivetitle")) { if (!m_path.isEmpty()) { m_clipType = ClipType::TextTemplate; } else { m_clipType = ClipType::Text; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("webvfx")) { m_clipType = ClipType::WebVfx; } else if (m_service == QLatin1String("qtext")) { m_clipType = ClipType::QText; } else if (m_service == QLatin1String("blipflash")) { // Mostly used for testing m_clipType = ClipType::AV; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Unknown; } if (audioIndex > -1 || m_clipType == ClipType::Playlist) { m_audioInfo = std::make_unique(m_masterProducer, audioIndex); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->frames_to_time(m_masterProducer->get_playtime(), mlt_time_clock)); m_masterProducer->set("out", m_masterProducer->frames_to_time(m_masterProducer->get_length() - 1, mlt_time_clock)); } } } bool ClipController::hasLimitedDuration() const { return m_hasLimitedDuration; } void ClipController::forceLimitedDuration() { m_hasLimitedDuration = true; } std::shared_ptr ClipController::originalProducer() { QMutexLocker lock(&m_producerLock); return m_masterProducer; } Mlt::Producer *ClipController::masterProducer() { return new Mlt::Producer(*m_masterProducer); } bool ClipController::isValid() { if (m_masterProducer == nullptr) { return false; } return m_masterProducer->is_valid(); } // static const char *ClipController::getPassPropertiesList(bool passLength) { if (!passLength) { return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,file_hash,autorotate,xmldata,video_index,audio_index,set.test_image,set.test_audio"; } return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length,video_index,audio_index,set.test_image,set.test_audio"; } QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, prefix.toUtf8().constData()); QMap subclipsData; for (int i = 0; i < subProperties.count(); i++) { subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i)); } return subclipsData; } void ClipController::updateProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::updateProducer"; // TODO replace all track producers if (!m_properties) { // producer has not been initialized return addMasterProducer(producer); } Mlt::Properties passProperties; // Keep track of necessary properties QString proxy = producer->get("kdenlive:proxy"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property m_usesProxy = true; } else { m_usesProxy = false; } // When resetting profile, duration can change so we invalidate it to 0 in that case int length = m_properties->get_int("length"); const char *passList = getPassPropertiesList(m_usesProxy && length > 0); // This is necessary as some properties like set.test_audio are reset on producer creation passProperties.pass_list(*m_properties, passList); delete m_properties; *m_masterProducer = producer.get(); checkAudioVideo(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); // Pass properties from previous producer m_properties->pass_list(passProperties, passList); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_effectStack->resetService(m_masterProducer); emitProducerChanged(m_controllerBinId, producer); // URL and name should not be updated otherwise when proxying a clip we cannot find back the original url /*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource")); if (m_url.isValid()) { m_name = m_url.fileName(); } */ } qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource"); } const QString ClipController::getStringDuration() { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime > 0) { return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df)); } return m_properties->frames_to_time(m_masterProducer->get_length(), mlt_time_smpte_df); } return i18n("Unknown"); } int ClipController::getProducerDuration() const { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { return playtime = m_masterProducer->get_length(); } return playtime; } return -1; } char *ClipController::framesToTime(int frames) const { if (m_masterProducer) { return m_masterProducer->frames_to_time(frames, mlt_time_clock); } return nullptr; } GenTime ClipController::getPlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return {m_masterProducer->get_playtime(), fps}; } int ClipController::getFramePlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return 0; } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return playtime == 0 ? m_masterProducer->get_playtime() : playtime; } return m_masterProducer->get_playtime(); } QString ClipController::getProducerProperty(const QString &name) const { if (!m_properties) { return QString(); } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get(correctedName.toUtf8().constData()); } return QString(m_properties->get(name.toUtf8().constData())); } int ClipController::getProducerIntProperty(const QString &name) const { if (!m_properties) { return 0; } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get_int(correctedName.toUtf8().constData()); } return m_properties->get_int(name.toUtf8().constData()); } qint64 ClipController::getProducerInt64Property(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_int64(name.toUtf8().constData()); } double ClipController::getProducerDoubleProperty(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_double(name.toUtf8().constData()); } QColor ClipController::getProducerColorProperty(const QString &name) const { if (!m_properties) { return {}; } mlt_color color = m_properties->get_color(name.toUtf8().constData()); return QColor::fromRgb(color.r, color.g, color.b); } QMap ClipController::currentProperties(const QMap &props) { QMap currentProps; QMap::const_iterator i = props.constBegin(); while (i != props.constEnd()) { currentProps.insert(i.key(), getProducerProperty(i.key())); ++i; } return currentProps; } double ClipController::originalFps() const { if (!m_properties) { return 0; } QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex); return m_properties->get_double(propertyName.toUtf8().constData()); } QString ClipController::videoCodecProperty(const QString &property) const { if (!m_properties) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::codec(bool audioCodec) const { if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_properties->get_int("audio_index") : m_videoIndex); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::clipUrl() const { return m_path; } bool ClipController::sourceExists() const { if (m_clipType == ClipType::Color || m_clipType == ClipType::Text) { return true; } if (m_clipType == ClipType::SlideShow) { //TODO return true; } return QFile::exists(m_path); } QString ClipController::clipName() const { QString name = getProducerProperty(QStringLiteral("kdenlive:clipname")); if (!name.isEmpty()) { return name; } return QFileInfo(m_path).fileName(); } QString ClipController::description() const { if (m_clipType == ClipType::TextTemplate) { QString name = getProducerProperty(QStringLiteral("templatetext")); return name; } QString name = getProducerProperty(QStringLiteral("kdenlive:description")); if (!name.isEmpty()) { return name; } return getProducerProperty(QStringLiteral("meta.attr.comment.markup")); } QString ClipController::serviceName() const { return m_service; } void ClipController::setProducerProperty(const QString &name, int value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, double value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, const QString &value) { if (!m_masterProducer) return; // TODO: also set property on all track producers if (value.isEmpty()) { m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } else { m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData()); } } void ClipController::resetProducerProperty(const QString &name) { // TODO: also set property on all track producers if (!m_masterProducer) return; m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType::ProducerType ClipController::clipType() const { return m_clipType; } const QSize ClipController::getFrameSize() const { if (m_masterProducer == nullptr) { return QSize(); } int width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } int height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } return QSize(width, height); } bool ClipController::hasAudio() const { return m_hasAudio; } void ClipController::checkAudioVideo() { m_masterProducer->seek(0); if (m_masterProducer->get_int("_placeholder") == 1 || m_masterProducer->get("text") == QLatin1String("INVALID")) { // This is a placeholder file, try to guess from its properties QString orig_service = m_masterProducer->get("kdenlive:orig_service"); if (orig_service.startsWith(QStringLiteral("avformat")) || (m_masterProducer->get_int("audio_index") + m_masterProducer->get_int("video_index") > 0)) { m_hasAudio = m_masterProducer->get_int("audio_index") >= 0; m_hasVideo = m_masterProducer->get_int("video_index") >= 0; } else { // Assume image or text producer m_hasAudio = false; m_hasVideo = true; } return; } QScopedPointer frame(m_masterProducer->get_frame()); if (frame->is_valid()) { // test_audio returns 1 if there is NO audio (strange but true at the time this code is written) m_hasAudio = frame->get_int("test_audio") == 0; m_hasVideo = frame->get_int("test_image") == 0; } else { qDebug()<<"* * * *ERROR INVALID FRAME On test"; } } bool ClipController::hasVideo() const { return m_hasVideo; } PlaylistState::ClipState ClipController::defaultState() const { if (hasVideo()) { return PlaylistState::VideoOnly; } if (hasAudio()) { return PlaylistState::AudioOnly; } return PlaylistState::Disabled; } QPixmap ClipController::pixmap(int framePosition, int width, int height) { // TODO refac this should use the new thumb infrastructure m_masterProducer->seek(framePosition); Mlt::Frame *frame = m_masterProducer->get_frame(); if (frame == nullptr || !frame->is_valid()) { QPixmap p(width, height); p.fill(QColor(Qt::red).rgb()); return p; } frame->set("rescale.interp", "bilinear"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (width == 0) { width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } } if (height == 0) { height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } } // int ow = frameWidth; // int oh = height; mlt_image_format format = mlt_image_rgb24a; width += width % 2; height += height % 2; const uchar *imagedata = frame->get_image(format, width, height); QImage image(imagedata, width, height, QImage::Format_RGBA8888); QPixmap pixmap; pixmap.convertFromImage(image); delete frame; return pixmap; } void ClipController::setZone(const QPoint &zone) { setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x()); setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y()); } QPoint ClipController::zone() const { int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int max = getFramePlaytime() - 1; int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max); if (out <= in) { out = max; } QPoint zone(in, out); return zone; } const QString ClipController::getClipHash() const { return getProducerProperty(QStringLiteral("kdenlive:file_hash")); } Mlt::Properties &ClipController::properties() { return *m_properties; } void ClipController::backupOriginalProperties() { if (m_properties->get_int("kdenlive:original.backup") == 1) { return; } int propsCount = m_properties->count(); // store original props + QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")}; for (int j = 0; j < propsCount; j++) { QString propName = m_properties->get_name(j); + if (doNotPass.contains(propName)) { + continue; + } if (!propName.startsWith(QLatin1Char('_'))) { propName.prepend(QStringLiteral("kdenlive:original.")); m_properties->set(propName.toUtf8().constData(), m_properties->get(j)); } } m_properties->set("kdenlive:original.backup", 1); } void ClipController::clearBackupProperties() { if (m_properties->get_int("kdenlive:original.backup") == 0) { return; } int propsCount = m_properties->count(); // clear original props QStringList passProps; for (int j = 0; j < propsCount; j++) { QString propName = m_properties->get_name(j); if (propName.startsWith(QLatin1String("kdenlive:original."))) { passProps << propName; } } for (const QString &p : passProps) { m_properties->set(p.toUtf8().constData(), (char *)nullptr); } m_properties->set("kdenlive:original.backup", (char *)nullptr); } void ClipController::mirrorOriginalProperties(Mlt::Properties &props) { if (m_usesProxy && QFileInfo(m_properties->get("resource")).fileName() == QFileInfo(m_properties->get("kdenlive:proxy")).fileName()) { // This is a proxy, we need to use the real source properties if (m_properties->get_int("kdenlive:original.backup") == 0) { // We have a proxy clip, load original source producer std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData()); // Get frame to make sure we retrieve all original props std::shared_ptr fr(prod->get_frame()); if (!prod->is_valid()) { return; } int width = 0; int height = 0; mlt_image_format format = mlt_image_none; fr->get_image(format, width, height); Mlt::Properties sourceProps(prod->get_properties()); props.inherit(sourceProps); int propsCount = sourceProps.count(); // store original props + QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")}; for (int i = 0; i < propsCount; i++) { QString propName = sourceProps.get_name(i); + if (doNotPass.contains(propName)) { + continue; + } if (!propName.startsWith(QLatin1Char('_'))) { propName.prepend(QStringLiteral("kdenlive:original.")); m_properties->set(propName.toUtf8().constData(), sourceProps.get(i)); } } m_properties->set("kdenlive:original.backup", 1); } // Properties were fetched in the past, reuse Mlt::Properties sourceProps; sourceProps.pass_values(*m_properties, "kdenlive:original."); props.inherit(sourceProps); } else { if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio) { // Make sure that a frame / image was fetched to initialize all meta properties QString progressive = m_properties->get("meta.media.progressive"); if (progressive.isEmpty()) { // Fetch a frame to initialize required properties QScopedPointer tmpProd(nullptr); if (KdenliveSettings::gpu_accel()) { QString service = m_masterProducer->get("mlt_service"); tmpProd.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), m_masterProducer->get("resource"))); } std::shared_ptr fr(tmpProd ? tmpProd->get_frame() : m_masterProducer->get_frame()); mlt_image_format format = mlt_image_none; int width = 0; int height = 0; fr->get_image(format, width, height); } } props.inherit(*m_properties); } } void ClipController::addEffect(QDomElement &xml) { Q_UNUSED(xml) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); // Add effect to list and setup a kdenlive_ix value int kdenlive_ix = 0; for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); int ix = effect->get_int("kdenlive_ix"); if (ix > kdenlive_ix) { kdenlive_ix = ix; } } kdenlive_ix++; xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix); EffectsParameterList params = EffectsController::getEffectArgs(xml); EffectManager effect(service); effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps())); if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::removeEffect(int effectIndex, bool delayRefresh) { Q_UNUSED(effectIndex) Q_UNUSED(delayRefresh) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.removeEffect(effectIndex, true); if (!delayRefresh) { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } */ } void ClipController::moveEffect(int oldPos, int newPos) { Q_UNUSED(oldPos) Q_UNUSED(newPos) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.moveEffect(oldPos, newPos); */ } int ClipController::effectsCount() { int count = 0; Mlt::Service service(m_masterProducer->parent()); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QString id = effect->get("kdenlive_id"); if (!id.isEmpty()) { count++; } } return count; } void ClipController::changeEffectState(const QList &indexes, bool disable) { Q_UNUSED(indexes) Q_UNUSED(disable) // TODO refac : this must be rewritten /* Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) { effect->set("disable", (int)disable); } } if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::updateEffect(const QDomElement &e, int ix) { Q_UNUSED(e) Q_UNUSED(ix) // TODO refac : this must be rewritten /* QString tag = e.attribute(QStringLiteral("id")); if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) { // this filters cannot be edited, remove and re-add it removeEffect(ix, true); QDomElement clone = e.cloneNode().toElement(); addEffect(clone); return; } EffectsParameterList params = EffectsController::getEffectArgs(e); Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) { continue; } service.lock(); QString prefix; QString ser = effect->get("mlt_service"); if (ser == QLatin1String("region")) { prefix = QStringLiteral("filter0."); } for (int j = 0; j < params.count(); ++j) { effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData()); // qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId); // slotRefreshTracks(); */ } bool ClipController::hasEffects() const { return m_effectStack->rowCount() > 0; } void ClipController::setBinEffectsEnabled(bool enabled) { m_effectStack->setEffectStackEnabled(enabled); } void ClipController::saveZone(QPoint zone, const QDir &dir) { QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt")); if (dir.exists(path)) { // TODO ask for overwrite } Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); Mlt::Producer prod(m_masterProducer->get_producer()); Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y()); Mlt::Playlist list(pCore->getCurrentProfile()->profile()); list.insert_at(0, *prod2, 0); // list.set("title", desc.toUtf8().constData()); xmlConsumer.connect(list); xmlConsumer.run(); delete prod2; } std::shared_ptr ClipController::getEffectStack() const { return m_effectStack; } bool ClipController::addEffect(const QString &effectId) { return m_effectStack->appendEffect(effectId, true); } bool ClipController::copyEffect(const std::shared_ptr &stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), !m_hasAudio ? PlaylistState::VideoOnly : !m_hasVideo ? PlaylistState::AudioOnly : PlaylistState::Disabled); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } void ClipController::refreshAudioInfo() { if (m_audioInfo && m_masterProducer) { m_audioInfo->setAudioIndex(m_masterProducer, m_properties->get_int("audio_index")); } }