File indexing completed on 2024-04-28 08:43:32

0001 /*
0002 SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
0003 SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
0004 This file is part of Kdenlive. See www.kdenlive.org.
0005 
0006 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "projectclip.h"
0010 #include "audio/audioInfo.h"
0011 #include "bin.h"
0012 #include "clipcreator.hpp"
0013 #include "core.h"
0014 #include "doc/docundostack.hpp"
0015 #include "doc/kdenlivedoc.h"
0016 #include "doc/kthumb.h"
0017 #include "effects/effectstack/model/effectstackmodel.hpp"
0018 #include "jobs/audiolevelstask.h"
0019 #include "jobs/cachetask.h"
0020 #include "jobs/cliploadtask.h"
0021 #include "jobs/proxytask.h"
0022 #include "kdenlivesettings.h"
0023 #include "lib/audio/audioStreamInfo.h"
0024 #include "macros.hpp"
0025 #include "mltcontroller/clippropertiescontroller.h"
0026 #include "model/markerlistmodel.hpp"
0027 #include "model/markersortmodel.h"
0028 #include "profiles/profilemodel.hpp"
0029 #include "project/projectmanager.h"
0030 #include "projectfolder.h"
0031 #include "projectitemmodel.h"
0032 #include "projectsubclip.h"
0033 #include "timeline2/model/snapmodel.hpp"
0034 #include "utils/thumbnailcache.hpp"
0035 #include "utils/timecode.h"
0036 #include "xml/xml.hpp"
0037 
0038 #include "kdenlive_debug.h"
0039 #include "utils/KMessageBox_KdenliveCompat.h"
0040 #include <KIO/RenameDialog>
0041 #include <KImageCache>
0042 #include <KLocalizedString>
0043 #include <KMessageBox>
0044 #include <QApplication>
0045 #include <QCryptographicHash>
0046 #include <QDir>
0047 #include <QDomElement>
0048 #include <QFile>
0049 #include <QJsonArray>
0050 #include <QJsonDocument>
0051 #include <QJsonObject>
0052 #include <QMimeDatabase>
0053 #include <QPainter>
0054 #include <QProcess>
0055 #include <QtMath>
0056 
0057 #ifdef CRASH_AUTO_TEST
0058 #include "logger.hpp"
0059 #pragma GCC diagnostic push
0060 #pragma GCC diagnostic ignored "-Wunused-parameter"
0061 #pragma GCC diagnostic ignored "-Wsign-conversion"
0062 #pragma GCC diagnostic ignored "-Wfloat-equal"
0063 #pragma GCC diagnostic ignored "-Wshadow"
0064 #pragma GCC diagnostic ignored "-Wpedantic"
0065 #include <rttr/registration>
0066 
0067 #pragma GCC diagnostic pop
0068 RTTR_REGISTRATION
0069 {
0070     using namespace rttr;
0071     registration::class_<ProjectClip>("ProjectClip");
0072 }
0073 #endif
0074 
0075 ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model, std::shared_ptr<Mlt::Producer> &producer)
0076     : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
0077     , ClipController(id, producer)
0078     , isReloading(false)
0079     , m_resetTimelineOccurences(false)
0080     , m_uuid(QUuid::createUuid())
0081 {
0082     m_markerModel = std::make_shared<MarkerListModel>(id, pCore->projectManager()->undoStack());
0083     m_markerFilterModel.reset(new MarkerSortModel(this));
0084     m_markerFilterModel->setSourceModel(m_markerModel.get());
0085     m_markerFilterModel->setSortRole(MarkerListModel::PosRole);
0086     m_markerFilterModel->sort(0, Qt::AscendingOrder);
0087     if (m_masterProducer->get_int("_placeholder") == 1) {
0088         m_clipStatus = FileStatus::StatusMissing;
0089     } else if (m_masterProducer->get_int("_missingsource") == 1) {
0090         m_clipStatus = FileStatus::StatusProxyOnly;
0091     } else if (m_usesProxy) {
0092         m_clipStatus = FileStatus::StatusProxy;
0093     } else {
0094         m_clipStatus = FileStatus::StatusReady;
0095     }
0096     m_name = clipName();
0097     m_duration = getStringDuration();
0098     m_inPoint = 0;
0099     m_outPoint = 0;
0100     m_date = date;
0101     updateDescription();
0102     if (m_clipType == ClipType::Audio) {
0103         m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
0104     } else {
0105         if (m_clipType == ClipType::Timeline) {
0106             // Initialize path for thumbnails playlist
0107             m_sequenceUuid = QUuid(m_masterProducer->get("kdenlive:uuid"));
0108             if (model->hasSequenceId(m_sequenceUuid)) {
0109                 // OOps we already have a sequence with this uuid, change it
0110                 m_sequenceUuid = QUuid::createUuid();
0111                 m_masterProducer->set("kdenlive:uuid", m_sequenceUuid.toString().toUtf8().constData());
0112                 m_masterProducer->parent().set("kdenlive:uuid", m_sequenceUuid.toString().toUtf8().constData());
0113             }
0114             m_sequenceThumbFile.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("thumbs-%1-XXXXXX.mlt").arg(m_binId)));
0115         }
0116         m_thumbnail = thumb;
0117     }
0118     // Make sure we have a hash for this clip
0119     hash();
0120     m_boundaryTimer.setSingleShot(true);
0121     m_boundaryTimer.setInterval(500);
0122     if (hasLimitedDuration()) {
0123         connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
0124     }
0125     connect(m_markerModel.get(), &MarkerListModel::modelChanged, this,
0126             [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
0127     QString markers = getProducerProperty(QStringLiteral("kdenlive:markers"));
0128     if (!markers.isEmpty()) {
0129         QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(QString, markers), Q_ARG(bool, true), Q_ARG(bool, false));
0130     }
0131     setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
0132     AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
0133     connectEffectStack();
0134     // Timeline clip thumbs will be generated later after the tractor has been updated
0135     if (m_clipType != ClipType::Timeline &&
0136         (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly)) {
0137         // Generate clip thumbnail
0138         ObjectId oid(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid());
0139         ClipLoadTask::start(oid, QDomElement(), true, -1, -1, this);
0140         // Generate audio thumbnail
0141         if (KdenliveSettings::audiothumbnails() &&
0142             (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || (m_hasAudio && m_clipType != ClipType::Timeline))) {
0143             AudioLevelsTask::start(oid, this, false);
0144         }
0145     }
0146 }
0147 
0148 // static
0149 std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model,
0150                                                     std::shared_ptr<Mlt::Producer> &producer)
0151 {
0152     std::shared_ptr<ProjectClip> self(new ProjectClip(id, thumb, model, producer));
0153     baseFinishConstruct(self);
0154     QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(QString, id),
0155                               Q_ARG(QString, self->getProducerProperty(QStringLiteral("kdenlive:clipzones"))), Q_ARG(bool, false));
0156     return self;
0157 }
0158 
0159 void ProjectClip::importEffects(const std::shared_ptr<Mlt::Producer> &producer, const QString &originalDecimalPoint)
0160 {
0161     m_effectStack->importEffects(producer, PlaylistState::Disabled, true, originalDecimalPoint);
0162 }
0163 
0164 ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model)
0165     : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
0166     , ClipController(id)
0167     , isReloading(false)
0168     , m_resetTimelineOccurences(false)
0169     , m_uuid(QUuid::createUuid())
0170 {
0171     m_clipStatus = FileStatus::StatusWaiting;
0172     m_thumbnail = thumb;
0173     if (description.hasAttribute(QStringLiteral("type"))) {
0174         m_clipType = ClipType::ProducerType(description.attribute(QStringLiteral("type")).toInt());
0175         if (m_clipType == ClipType::Audio) {
0176             m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
0177         }
0178     }
0179 
0180     m_markerModel = std::make_shared<MarkerListModel>(m_binId, pCore->projectManager()->undoStack());
0181     m_markerFilterModel.reset(new MarkerSortModel(this));
0182     m_markerFilterModel->setSourceModel(m_markerModel.get());
0183     m_markerFilterModel->setSortRole(MarkerListModel::PosRole);
0184     m_markerFilterModel->sort(0, Qt::AscendingOrder);
0185     if (m_clipType == ClipType::Timeline) {
0186         m_sequenceUuid = QUuid(getXmlProperty(description, QStringLiteral("kdenlive:uuid")));
0187     }
0188 
0189     const QString proxy = getXmlProperty(description, QStringLiteral("kdenlive:proxy"));
0190     if (proxy.length() > 3) {
0191         m_temporaryUrl = getXmlProperty(description, QStringLiteral("kdenlive:originalurl"));
0192     }
0193     if (m_temporaryUrl.isEmpty()) {
0194         m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource"));
0195     }
0196     if (m_name.isEmpty()) {
0197         QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname"));
0198         if (!clipName.isEmpty()) {
0199             m_name = clipName;
0200         } else if (!m_temporaryUrl.isEmpty() && m_clipType != ClipType::Timeline) {
0201             m_name = QFileInfo(m_temporaryUrl).fileName();
0202         } else {
0203             m_name = i18n("Unnamed");
0204         }
0205     }
0206     m_date = QFileInfo(m_temporaryUrl).lastModified();
0207     m_boundaryTimer.setSingleShot(true);
0208     m_boundaryTimer.setInterval(500);
0209     connect(m_markerModel.get(), &MarkerListModel::modelChanged, this,
0210             [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
0211 }
0212 
0213 std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb,
0214                                                     std::shared_ptr<ProjectItemModel> model)
0215 {
0216     std::shared_ptr<ProjectClip> self(new ProjectClip(id, description, thumb, std::move(model)));
0217     baseFinishConstruct(self);
0218     return self;
0219 }
0220 
0221 ProjectClip::~ProjectClip()
0222 {
0223     if (pCore->currentDoc()->closing) {
0224         for (auto &p : m_audioProducers) {
0225             m_effectStack->removeService(p.second);
0226         }
0227         for (auto &p : m_videoProducers) {
0228             m_effectStack->removeService(p.second);
0229         }
0230         for (auto &p : m_timewarpProducers) {
0231             m_effectStack->removeService(p.second);
0232         }
0233         // Release audio producers
0234         m_audioProducers.clear();
0235         m_videoProducers.clear();
0236         m_timewarpProducers.clear();
0237     }
0238 }
0239 
0240 std::shared_ptr<MarkerListModel> ProjectClip::markerModel()
0241 {
0242     return m_markerModel;
0243 }
0244 
0245 void ProjectClip::connectEffectStack()
0246 {
0247     connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, [&]() {
0248         if (auto ptr = m_model.lock()) {
0249             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
0250                                                                            {AbstractProjectItem::IconOverlay});
0251         }
0252     });
0253 }
0254 
0255 QString ProjectClip::getToolTip() const
0256 {
0257     if (m_clipType == ClipType::Color && m_path.contains(QLatin1Char('/'))) {
0258         return m_path.section(QLatin1Char('/'), -1);
0259     }
0260     if (m_clipType == ClipType::Timeline) {
0261         return i18n("Timeline sequence");
0262     }
0263     return m_path;
0264 }
0265 
0266 QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue)
0267 {
0268     QString value = defaultValue;
0269     QDomNodeList props = producer.elementsByTagName(QStringLiteral("property"));
0270     for (int i = 0; i < props.count(); ++i) {
0271         if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) {
0272             value = props.at(i).firstChild().nodeValue();
0273             break;
0274         }
0275     }
0276     return value;
0277 }
0278 
0279 void ProjectClip::updateAudioThumbnail(bool cachedThumb)
0280 {
0281     Q_EMIT audioThumbReady();
0282     if (m_clipType == ClipType::Audio) {
0283         QImage thumb = ThumbnailCache::get()->getThumbnail(m_binId, 0);
0284         if (thumb.isNull() && !pCore->taskManager.hasPendingJob(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), AbstractTask::AUDIOTHUMBJOB)) {
0285             int iconHeight = int(QFontInfo(qApp->font()).pixelSize() * 3.5);
0286             QImage img(QSize(int(iconHeight * pCore->getCurrentDar()), iconHeight), QImage::Format_ARGB32);
0287             img.fill(Qt::darkGray);
0288             QMap<int, QString> streams = audioInfo()->streams();
0289             QMap<int, int> channelsList = audioInfo()->streamChannels();
0290             QPainter painter(&img);
0291             QPen pen = painter.pen();
0292             pen.setColor(Qt::white);
0293             painter.setPen(pen);
0294             int streamCount = 0;
0295             if (streams.count() > 0) {
0296                 double streamHeight = iconHeight / streams.count();
0297                 QMapIterator<int, QString> st(streams);
0298                 while (st.hasNext()) {
0299                     st.next();
0300                     int channels = channelsList.value(st.key());
0301                     double channelHeight = double(streamHeight) / channels;
0302                     const QVector<uint8_t> audioLevels = audioFrameCache(st.key());
0303                     qreal indicesPrPixel = qreal(audioLevels.length()) / img.width();
0304                     int idx;
0305                     for (int channel = 0; channel < channels; channel++) {
0306                         double y = (streamHeight * streamCount) + (channel * channelHeight) + channelHeight / 2;
0307                         for (int i = 0; i <= img.width(); i++) {
0308                             idx = int(ceil(i * indicesPrPixel));
0309                             idx += idx % channels;
0310                             idx += channel;
0311                             if (idx >= audioLevels.length() || idx < 0) {
0312                                 break;
0313                             }
0314                             double level = audioLevels.at(idx) * channelHeight / 510.; // divide height by 510 (2*255) to get height
0315                             painter.drawLine(i, int(y - level), i, int(y + level));
0316                         }
0317                     }
0318                     streamCount++;
0319                 }
0320             }
0321             thumb = img;
0322             // Cache thumbnail
0323             ThumbnailCache::get()->storeThumbnail(m_binId, 0, thumb, true);
0324         }
0325         if (!thumb.isNull()) {
0326             setThumbnail(thumb, -1, -1);
0327         }
0328     }
0329     if (!KdenliveSettings::audiothumbnails()) {
0330         return;
0331     }
0332     m_audioThumbCreated = true;
0333     if (!cachedThumb) {
0334         // Audio was just created
0335         updateTimelineClips({TimelineModel::ReloadAudioThumbRole});
0336     }
0337 }
0338 
0339 bool ProjectClip::audioThumbCreated() const
0340 {
0341     return (m_audioThumbCreated);
0342 }
0343 
0344 ClipType::ProducerType ProjectClip::clipType() const
0345 {
0346     return m_clipType;
0347 }
0348 
0349 bool ProjectClip::hasParent(const QString &id) const
0350 {
0351     std::shared_ptr<AbstractProjectItem> par = parent();
0352     while (par) {
0353         if (par->clipId() == id) {
0354             return true;
0355         }
0356         par = par->parent();
0357     }
0358     return false;
0359 }
0360 
0361 std::shared_ptr<ProjectClip> ProjectClip::clip(const QString &id)
0362 {
0363     if (id == m_binId) {
0364         return std::static_pointer_cast<ProjectClip>(shared_from_this());
0365     }
0366     return std::shared_ptr<ProjectClip>();
0367 }
0368 
0369 std::shared_ptr<ProjectFolder> ProjectClip::folder(const QString &id)
0370 {
0371     Q_UNUSED(id)
0372     return std::shared_ptr<ProjectFolder>();
0373 }
0374 
0375 std::shared_ptr<ProjectSubClip> ProjectClip::getSubClip(int in, int out)
0376 {
0377     for (int i = 0; i < childCount(); ++i) {
0378         std::shared_ptr<ProjectSubClip> clip = std::static_pointer_cast<ProjectSubClip>(child(i))->subClip(in, out);
0379         if (clip) {
0380             return clip;
0381         }
0382     }
0383     return std::shared_ptr<ProjectSubClip>();
0384 }
0385 
0386 QStringList ProjectClip::subClipIds() const
0387 {
0388     QStringList subIds;
0389     for (int i = 0; i < childCount(); ++i) {
0390         std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
0391         if (clip) {
0392             subIds << clip->clipId();
0393         }
0394     }
0395     return subIds;
0396 }
0397 
0398 std::shared_ptr<ProjectClip> ProjectClip::clipAt(int ix)
0399 {
0400     if (ix == row()) {
0401         return std::static_pointer_cast<ProjectClip>(shared_from_this());
0402     }
0403     return std::shared_ptr<ProjectClip>();
0404 }
0405 
0406 bool ProjectClip::hasUrl() const
0407 {
0408     if (m_clipType == ClipType::Color || m_clipType == ClipType::Unknown || m_clipType == ClipType::Timeline) {
0409         return false;
0410     }
0411     return !clipUrl().isEmpty();
0412 }
0413 
0414 const QString ProjectClip::url() const
0415 {
0416     return clipUrl();
0417 }
0418 
0419 const QSize ProjectClip::frameSize() const
0420 {
0421     return getFrameSize();
0422 }
0423 
0424 GenTime ProjectClip::duration() const
0425 {
0426     return getPlaytime();
0427 }
0428 
0429 size_t ProjectClip::frameDuration() const
0430 {
0431     return size_t(getFramePlaytime());
0432 }
0433 
0434 void ProjectClip::resetSequenceThumbnails()
0435 {
0436     QMutexLocker lk(&m_thumbMutex);
0437     pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), AbstractTask::LOADJOB, true);
0438     m_thumbXml.clear();
0439     ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
0440     // Force refeshing thumbs producer
0441     lk.unlock();
0442     m_uuid = QUuid::createUuid();
0443     // Clips will be replanted so no need to refresh thumbs
0444     // updateTimelineClips({TimelineModel::ClipThumbRole});
0445 }
0446 
0447 void ProjectClip::reloadProducer(bool refreshOnly, bool isProxy, bool forceAudioReload)
0448 {
0449     // we find if there are some loading job on that clip
0450     QMutexLocker lock(&m_thumbMutex);
0451     ObjectId oid(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid());
0452     if (refreshOnly) {
0453         // In that case, we only want a new thumbnail.
0454         // We thus set up a thumb job. We must make sure that there is no pending LOADJOB
0455         // Clear cache first
0456         ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
0457         pCore->taskManager.discardJobs(oid, AbstractTask::LOADJOB, true);
0458         pCore->taskManager.discardJobs(oid, AbstractTask::CACHEJOB);
0459         m_thumbXml.clear();
0460         // Reset uuid to enforce reloading thumbnails from qml cache
0461         m_uuid = QUuid::createUuid();
0462         updateTimelineClips({TimelineModel::ClipThumbRole, TimelineModel::ResourceRole});
0463         ClipLoadTask::start(oid, QDomElement(), true, -1, -1, this);
0464     } else {
0465         // If another load job is running?
0466         pCore->taskManager.discardJobs(oid, AbstractTask::LOADJOB, true);
0467         pCore->taskManager.discardJobs(oid, AbstractTask::CACHEJOB);
0468         if (QFile::exists(m_path) && (!isProxy && !hasProxy()) && m_properties) {
0469             clearBackupProperties();
0470         }
0471         QDomDocument doc;
0472         QDomElement xml;
0473         QString resource;
0474         if (m_properties) {
0475             resource = m_properties->get("resource");
0476         }
0477         if (m_service.isEmpty() && !resource.isEmpty()) {
0478             xml = ClipCreator::getXmlFromUrl(resource).documentElement();
0479         } else {
0480             xml = toXml(doc);
0481         }
0482         if (!xml.isNull()) {
0483             bool hashChanged = false;
0484             m_thumbXml.clear();
0485             ClipType::ProducerType type = clipType();
0486             if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) {
0487                 xml.removeAttribute("out");
0488             }
0489             if (type == ClipType::Audio || type == ClipType::AV) {
0490                 // Check if source file was changed and rebuild audio data if necessary
0491                 QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
0492                 if (!clipHash.isEmpty()) {
0493                     if (clipHash != getFileHash()) {
0494                         // Source clip has changed, rebuild data
0495                         hashChanged = true;
0496                     }
0497                 }
0498             }
0499             m_audioThumbCreated = false;
0500             isReloading = true;
0501             // Reset uuid to enforce reloading thumbnails from qml cache
0502             m_uuid = QUuid::createUuid();
0503             if (forceAudioReload || (!isProxy && hashChanged)) {
0504                 discardAudioThumb();
0505             }
0506             if (m_clipStatus != FileStatus::StatusMissing) {
0507                 m_clipStatus = FileStatus::StatusWaiting;
0508             }
0509             m_thumbXml.clear();
0510             ClipLoadTask::start(oid, xml, false, -1, -1, this);
0511         }
0512     }
0513 }
0514 
0515 QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile)
0516 {
0517     getProducerXML(document, includeMeta, includeProfile);
0518     QDomElement prod;
0519     QString tag = document.documentElement().tagName();
0520     if (tag == QLatin1String("producer") || tag == QLatin1String("chain")) {
0521         prod = document.documentElement();
0522     } else {
0523         // Check if this is a sequence clip
0524         if (m_clipType == ClipType::Timeline) {
0525             prod = document.documentElement();
0526             prod.setAttribute(QStringLiteral("kdenlive:id"), m_binId);
0527             prod.setAttribute(QStringLiteral("kdenlive:producer_type"), ClipType::Timeline);
0528             prod.setAttribute(QStringLiteral("kdenlive:uuid"), m_sequenceUuid.toString());
0529             prod.setAttribute(QStringLiteral("kdenlive:duration"), QString::number(frameDuration()));
0530             prod.setAttribute(QStringLiteral("kdenlive:clipname"), clipName());
0531         } else {
0532             prod = document.documentElement().firstChildElement(QStringLiteral("chain"));
0533             if (prod.isNull()) {
0534                 prod = document.documentElement().firstChildElement(QStringLiteral("producer"));
0535             }
0536         }
0537     }
0538     if (m_clipType != ClipType::Unknown) {
0539         prod.setAttribute(QStringLiteral("type"), int(m_clipType));
0540     }
0541     return prod;
0542 }
0543 
0544 void ProjectClip::setThumbnail(const QImage &img, int in, int out, bool inCache)
0545 {
0546     if (img.isNull()) {
0547         return;
0548     }
0549     if (in > -1) {
0550         std::shared_ptr<ProjectSubClip> sub = getSubClip(in, out);
0551         if (sub) {
0552             sub->setThumbnail(img);
0553         }
0554         return;
0555     }
0556     QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
0557     if (hasProxy() && !thumb.isNull()) {
0558         // Overlay proxy icon
0559         QPainter p(&thumb);
0560         QColor c(220, 220, 10, 200);
0561         QRect r(0, 0, int(thumb.height() / 2.5), int(thumb.height() / 2.5));
0562         p.fillRect(r, c);
0563         QFont font = p.font();
0564         font.setPixelSize(r.height());
0565         font.setBold(true);
0566         p.setFont(font);
0567         p.setPen(Qt::black);
0568         p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P"));
0569     }
0570     m_thumbnail = QIcon(thumb);
0571     if (auto ptr = m_model.lock()) {
0572         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
0573                                                                        {AbstractProjectItem::DataThumbnail});
0574     }
0575     if (!inCache && (m_clipType == ClipType::Text || m_clipType == ClipType::TextTemplate)) {
0576         // Title clips always use the same thumb as bin, refresh
0577         updateTimelineClips({TimelineModel::ClipThumbRole});
0578     }
0579 }
0580 
0581 bool ProjectClip::hasAudioAndVideo() const
0582 {
0583     return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0;
0584 }
0585 
0586 bool ProjectClip::isCompatible(PlaylistState::ClipState state) const
0587 {
0588     switch (state) {
0589     case PlaylistState::AudioOnly:
0590         return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0);
0591     case PlaylistState::VideoOnly:
0592         return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0);
0593     default:
0594         return true;
0595     }
0596 }
0597 
0598 QPixmap ProjectClip::thumbnail(int width, int height)
0599 {
0600     return m_thumbnail.pixmap(width, height);
0601 }
0602 
0603 bool ProjectClip::setProducer(std::shared_ptr<Mlt::Producer> producer, bool generateThumb, bool clearTrackProducers)
0604 {
0605     qDebug() << "################### ProjectClip::setproducer #################";
0606     // Discard running tasks for this producer
0607     QMutexLocker locker(&m_producerMutex);
0608     FileStatus::ClipStatus currentStatus = m_clipStatus;
0609     if (producer->property_exists("_reloadName")) {
0610         m_name.clear();
0611     }
0612     bool buildProxy = producer->property_exists("_replaceproxy") && !pCore->currentDoc()->loading;
0613     updateProducer(producer);
0614     producer.reset();
0615     pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), AbstractTask::LOADJOB);
0616     // Abort thumbnail tasks if any
0617     m_thumbMutex.lock();
0618     m_thumbXml.clear();
0619     m_thumbMutex.unlock();
0620 
0621     isReloading = false;
0622     // Make sure we have a hash for this clip
0623     getFileHash();
0624     Q_EMIT producerChanged(m_binId, m_clipType == ClipType::Timeline ? m_masterProducer->parent() : *m_masterProducer.get());
0625     connectEffectStack();
0626 
0627     // Update info
0628     if (m_name.isEmpty()) {
0629         m_name = clipName();
0630     }
0631     QVector<int> updateRoles;
0632     if (m_date != date) {
0633         m_date = date;
0634         updateRoles << AbstractProjectItem::DataDate;
0635     }
0636     updateDescription();
0637     m_temporaryUrl.clear();
0638     if (m_clipType == ClipType::Audio) {
0639         m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
0640     } else if (m_clipType == ClipType::Image) {
0641         if (m_masterProducer->get_int("meta.media.width") < 8 || m_masterProducer->get_int("meta.media.height") < 8) {
0642             KMessageBox::information(QApplication::activeWindow(),
0643                                      i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework."));
0644         }
0645     } else if (m_clipType == ClipType::Timeline) {
0646         if (m_sequenceUuid.isNull()) {
0647             m_sequenceUuid = QUuid::createUuid();
0648             setProducerProperty(QStringLiteral("kdenlive:uuid"), m_sequenceUuid.toString());
0649         }
0650     }
0651     m_duration = getStringDuration();
0652     m_clipStatus = m_usesProxy ? FileStatus::StatusProxy : FileStatus::StatusReady;
0653     locker.unlock();
0654     if (m_clipStatus != currentStatus) {
0655         updateRoles << AbstractProjectItem::ClipStatus << AbstractProjectItem::IconOverlay;
0656         updateTimelineClips({TimelineModel::StatusRole, TimelineModel::ClipThumbRole});
0657     }
0658     setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
0659     AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
0660     if (auto ptr = m_model.lock()) {
0661         updateRoles << AbstractProjectItem::DataDuration;
0662         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()), updateRoles);
0663         std::static_pointer_cast<ProjectItemModel>(ptr)->updateWatcher(std::static_pointer_cast<ProjectClip>(shared_from_this()));
0664         if (currentStatus == FileStatus::StatusMissing) {
0665             std::static_pointer_cast<ProjectItemModel>(ptr)->missingClipTimer.start();
0666         }
0667     }
0668     // set parent again (some info need to be stored in producer)
0669     updateParent(parentItem().lock());
0670     if (generateThumb && m_clipType != ClipType::Audio) {
0671         // Generate video thumb
0672         ClipLoadTask::start(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), QDomElement(), true, -1, -1, this);
0673     }
0674     if (KdenliveSettings::audiothumbnails() &&
0675         (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || (m_hasAudio && m_clipType != ClipType::Timeline))) {
0676         AudioLevelsTask::start(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), this, false);
0677     }
0678     if (pCore->bin()) {
0679         pCore->bin()->reloadMonitorIfActive(clipId());
0680     }
0681     if (clearTrackProducers) {
0682         for (auto &p : m_audioProducers) {
0683             m_effectStack->removeService(p.second);
0684         }
0685         for (auto &p : m_videoProducers) {
0686             m_effectStack->removeService(p.second);
0687         }
0688         for (auto &p : m_timewarpProducers) {
0689             m_effectStack->removeService(p.second);
0690         }
0691         // Release audio producers
0692         m_audioProducers.clear();
0693         m_videoProducers.clear();
0694         if (m_timewarpProducers.size() > 0) {
0695             if (m_clipType == ClipType::Timeline) {
0696                 bool ok;
0697                 QDir sequenceFolder = pCore->currentDoc()->getCacheDir(CacheTmpWorkFiles, &ok);
0698                 if (ok) {
0699                     QString resource = sequenceFolder.absoluteFilePath(QString("sequence-%1.mlt").arg(m_sequenceUuid.toString()));
0700                     QFile::remove(resource);
0701                 }
0702             }
0703         }
0704         m_timewarpProducers.clear();
0705     }
0706     Q_EMIT refreshPropertiesPanel();
0707     if (hasLimitedDuration()) {
0708         connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
0709     } else {
0710         disconnect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
0711     }
0712     replaceInTimeline();
0713     updateTimelineClips({TimelineModel::IsProxyRole});
0714     bool generateProxy = false;
0715     std::shared_ptr<ProjectClip> clipToProxy = nullptr;
0716     if (buildProxy ||
0717         (!m_usesProxy && pCore->currentDoc()->useProxy() && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1)) {
0718         // automatic proxy generation enabled
0719         if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) {
0720             if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() &&
0721                 getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
0722                 clipToProxy = std::static_pointer_cast<ProjectClip>(shared_from_this());
0723             }
0724         } else if ((buildProxy || pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1) &&
0725                    (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
0726             if (m_hasVideo && (buildProxy || getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize())) {
0727                 clipToProxy = std::static_pointer_cast<ProjectClip>(shared_from_this());
0728             }
0729         } else if (m_clipType == ClipType::Playlist && pCore->getCurrentFrameDisplaySize().width() >= KdenliveSettings::proxyminsize() &&
0730                    getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
0731             clipToProxy = std::static_pointer_cast<ProjectClip>(shared_from_this());
0732         }
0733         if (clipToProxy != nullptr) {
0734             generateProxy = true;
0735         }
0736     }
0737     if (!generateProxy && KdenliveSettings::hoverPreview() &&
0738         (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist)) {
0739         QTimer::singleShot(1000, this, [this]() { CacheTask::start(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), 30, 0, 0, this); });
0740     }
0741     if (generateProxy) {
0742         QMetaObject::invokeMethod(pCore->currentDoc(), "slotProxyCurrentItem", Q_ARG(bool, true), Q_ARG(QList<std::shared_ptr<ProjectClip>>, {clipToProxy}),
0743                                   Q_ARG(bool, false));
0744     }
0745     return true;
0746 }
0747 
0748 // static
0749 const QString ProjectClip::getOriginalFromProxy(QString proxyPath)
0750 {
0751     QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';'));
0752     if (externalParams.count() >= 6) {
0753         QFileInfo info(proxyPath);
0754         QDir dir = info.absoluteDir();
0755         dir.cd(externalParams.at(3));
0756         QString fileName = info.fileName();
0757         bool matchFound = false;
0758         while (externalParams.count() >= 6) {
0759             if (fileName.startsWith(externalParams.at(1))) {
0760                 matchFound = true;
0761                 break;
0762             }
0763             externalParams = externalParams.mid(6);
0764         }
0765         if (matchFound) {
0766             fileName.remove(0, externalParams.at(1).size());
0767             fileName.prepend(externalParams.at(4));
0768             if (!externalParams.at(2).isEmpty()) {
0769                 if (!fileName.endsWith(externalParams.at(2))) {
0770                     // File does not match, abort
0771                     return QString();
0772                 }
0773                 fileName.chop(externalParams.at(2).size());
0774             }
0775             fileName.append(externalParams.at(5));
0776             if (fileName != proxyPath && dir.exists(fileName)) {
0777                 return dir.absoluteFilePath(fileName);
0778             }
0779         }
0780     }
0781     return QString();
0782 }
0783 
0784 // static
0785 const QString ProjectClip::getProxyFromOriginal(QString originalPath)
0786 {
0787     QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';'));
0788     if (externalParams.count() >= 6) {
0789         QFileInfo info(originalPath);
0790         QDir dir = info.absoluteDir();
0791         dir.cd(externalParams.at(0));
0792         QString fileName = info.fileName();
0793         bool matchFound = false;
0794         while (externalParams.count() >= 6) {
0795             if (fileName.startsWith(externalParams.at(4))) {
0796                 matchFound = true;
0797                 break;
0798             }
0799             externalParams = externalParams.mid(6);
0800         }
0801         if (matchFound) {
0802             fileName.remove(0, externalParams.at(4).size());
0803             fileName.prepend(externalParams.at(1));
0804             if (!externalParams.at(5).isEmpty()) {
0805                 if (!fileName.endsWith(externalParams.at(5))) {
0806                     // File does not match, abort
0807                     return QString();
0808                 }
0809                 fileName.chop(externalParams.at(5).size());
0810             }
0811             fileName.append(externalParams.at(2));
0812             if (fileName != originalPath && dir.exists(fileName)) {
0813                 return dir.absoluteFilePath(fileName);
0814             }
0815         }
0816     }
0817     return QString();
0818 }
0819 
0820 std::unique_ptr<Mlt::Producer> ProjectClip::getThumbProducer()
0821 {
0822     if (clipType() == ClipType::Unknown || m_masterProducer == nullptr || m_clipStatus == FileStatus::StatusWaiting) {
0823         return nullptr;
0824     }
0825     QMutexLocker lock(&m_thumbMutex);
0826     std::unique_ptr<Mlt::Producer> thumbProd;
0827     if (!m_thumbXml.isEmpty()) {
0828         thumbProd.reset(new Mlt::Producer(pCore->thumbProfile(), "xml-string", m_thumbXml.toUtf8().constData()));
0829         return thumbProd;
0830     }
0831     if (KdenliveSettings::gpu_accel()) {
0832         // TODO: when the original producer changes, we must reload this thumb producer
0833         thumbProd = softClone(ClipController::getPassPropertiesList());
0834     } else if (m_clipType == ClipType::Timeline) {
0835         if (pCore->currentDoc()->loading) {
0836             return nullptr;
0837         }
0838         if (!m_sequenceThumbFile.isOpen() && !m_sequenceThumbFile.open()) {
0839             // Something went wrong
0840             qWarning() << "Cannot write to temporary file: " << m_sequenceThumbFile.fileName();
0841             return nullptr;
0842         }
0843         cloneProducerToFile(m_sequenceThumbFile.fileName(), true);
0844         thumbProd.reset(new Mlt::Producer(pCore->thumbProfile(), "consumer", m_sequenceThumbFile.fileName().toUtf8().constData()));
0845     } else {
0846         QString mltService = m_masterProducer->get("mlt_service");
0847         const QString mltResource = m_masterProducer->get("resource");
0848         if (mltService == QLatin1String("avformat")) {
0849             mltService = QStringLiteral("avformat-novalidate");
0850         }
0851         thumbProd.reset(new Mlt::Producer(pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData()));
0852     }
0853     if (thumbProd->is_valid()) {
0854         Mlt::Properties original(m_masterProducer->get_properties());
0855         Mlt::Properties cloneProps(thumbProd->get_properties());
0856         cloneProps.pass_list(original, ClipController::getPassPropertiesList());
0857         thumbProd->set("audio_index", -1);
0858         thumbProd->set("astream", -1);
0859         // Required to make get_playtime() return > 1
0860         thumbProd->set("out", thumbProd->get_length() - 1);
0861         Mlt::Filter scaler(pCore->thumbProfile(), "swscale");
0862         Mlt::Filter padder(pCore->thumbProfile(), "resize");
0863         Mlt::Filter converter(pCore->thumbProfile(), "avcolor_space");
0864         thumbProd->attach(scaler);
0865         thumbProd->attach(padder);
0866         thumbProd->attach(converter);
0867     }
0868     m_thumbXml = ClipController::producerXml(*thumbProd.get(), true, false);
0869     return thumbProd;
0870 }
0871 
0872 void ProjectClip::createDisabledMasterProducer()
0873 {
0874     if (!m_disabledProducer) {
0875         m_disabledProducer = cloneProducer();
0876         m_disabledProducer->set("set.test_audio", 1);
0877         m_disabledProducer->set("set.test_image", 1);
0878         m_effectStack->addService(m_disabledProducer);
0879     }
0880 }
0881 
0882 int ProjectClip::getRecordTime()
0883 {
0884     if (m_masterProducer && (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio)) {
0885         int recTime = m_masterProducer->get_int("kdenlive:record_date");
0886         if (recTime > 0) {
0887             return recTime;
0888         }
0889         if (recTime < 0) {
0890             // Cannot read record date on this clip, abort
0891             return 0;
0892         }
0893         QString timecode = m_masterProducer->get("meta.attr.timecode.markup");
0894         // First try to get timecode from MLT metadata
0895         if (!timecode.isEmpty()) {
0896             // Timecode Format HH:MM:SS:FF
0897             // Check if we have a different fps
0898             double producerFps = m_masterProducer->get_double("meta.media.frame_rate_num") / m_masterProducer->get_double("meta.media.frame_rate_den");
0899             if (!qFuzzyCompare(producerFps, pCore->getCurrentFps())) {
0900                 // Producer and project have a different fps
0901                 bool ok;
0902                 int frames = timecode.section(QLatin1Char(':'), -1).toInt(&ok);
0903                 if (ok) {
0904                     frames *= int(pCore->getCurrentFps() / producerFps);
0905                     timecode.chop(2);
0906                     timecode.append(QString::number(frames).rightJustified(1, QChar('0')));
0907                 }
0908             }
0909             recTime = int(1000 * pCore->timecode().getFrameCount(timecode) / pCore->getCurrentFps());
0910             m_masterProducer->set("kdenlive:record_date", recTime);
0911             return recTime;
0912         } else {
0913             if (KdenliveSettings::mediainfopath().isEmpty() || !QFileInfo::exists(KdenliveSettings::mediainfopath())) {
0914                 // Try to find binary
0915                 const QStringList mltpath({QFileInfo(KdenliveSettings::meltpath()).canonicalPath(), qApp->applicationDirPath()});
0916                 QString mediainfopath = QStandardPaths::findExecutable(QStringLiteral("mediainfo"), mltpath);
0917                 if (mediainfopath.isEmpty()) {
0918                     mediainfopath = QStandardPaths::findExecutable(QStringLiteral("mediainfo"));
0919                 }
0920                 if (!mediainfopath.isEmpty()) {
0921                     KdenliveSettings::setMediainfopath(mediainfopath);
0922                 }
0923             }
0924             if (!KdenliveSettings::mediainfopath().isEmpty()) {
0925                 // Getting the timecode was not successfull yet,
0926                 // but mediainfo is available so try to get it with mediainfo
0927                 QProcess extractInfo;
0928                 extractInfo.start(KdenliveSettings::mediainfopath(), {url(), QStringLiteral("--output=XML")});
0929                 extractInfo.waitForFinished();
0930                 if (extractInfo.exitStatus() != QProcess::NormalExit || extractInfo.exitCode() != 0) {
0931                     KMessageBox::error(QApplication::activeWindow(),
0932                                        i18n("Cannot extract metadata from %1\n%2", url(), QString(extractInfo.readAllStandardError())));
0933                     return 0;
0934                 }
0935                 QDomDocument doc;
0936                 doc.setContent(extractInfo.readAllStandardOutput());
0937                 bool dateFormat = false;
0938                 QDomNodeList nodes = doc.documentElement().elementsByTagName(QStringLiteral("TimeCode_FirstFrame"));
0939                 if (nodes.isEmpty()) {
0940                     nodes = doc.documentElement().elementsByTagName(QStringLiteral("Recorded_Date"));
0941                     dateFormat = true;
0942                 }
0943                 if (!nodes.isEmpty()) {
0944                     // Parse recorded time (HH:MM:SS)
0945                     QString recInfo = nodes.at(0).toElement().text();
0946                     if (!recInfo.isEmpty()) {
0947                         if (dateFormat) {
0948                             if (recInfo.contains(QLatin1Char('+'))) {
0949                                 recInfo = recInfo.section(QLatin1Char('+'), 0, 0);
0950                             } else if (recInfo.contains(QLatin1Char('-'))) {
0951                                 recInfo = recInfo.section(QLatin1Char('-'), 0, 0);
0952                             }
0953                             QDateTime date = QDateTime::fromString(recInfo, "yyyy-MM-dd hh:mm:ss");
0954                             recTime = date.time().msecsSinceStartOfDay();
0955                         } else {
0956                             // Timecode Format HH:MM:SS:FF
0957                             // Check if we have a different fps
0958                             double producerFps =
0959                                 m_masterProducer->get_double("meta.media.frame_rate_num") / m_masterProducer->get_double("meta.media.frame_rate_den");
0960                             if (!qFuzzyCompare(producerFps, pCore->getCurrentFps())) {
0961                                 // Producer and project have a different fps
0962                                 bool ok;
0963                                 int frames = recInfo.section(QLatin1Char(':'), -1).toInt(&ok);
0964                                 if (ok) {
0965                                     frames *= int(pCore->getCurrentFps() / producerFps);
0966                                     recInfo.chop(2);
0967                                     recInfo.append(QString::number(frames).rightJustified(1, QChar('0')));
0968                                 }
0969                             }
0970                             recTime = int(1000 * pCore->timecode().getFrameCount(recInfo) / pCore->getCurrentFps());
0971                         }
0972                         m_masterProducer->set("kdenlive:record_date", recTime);
0973                         return recTime;
0974                     }
0975                 }
0976                 // set record date to -1 to avoid trying with mediainfo again and again
0977                 m_masterProducer->set("kdenlive:record_date", -1);
0978                 return 0;
0979             }
0980         }
0981     }
0982     return 0;
0983 }
0984 
0985 std::shared_ptr<Mlt::Producer> ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, int audioStream, double speed,
0986                                                                 bool secondPlaylist, const TimeWarpInfo timeremapInfo)
0987 {
0988     if (!m_masterProducer) {
0989         return nullptr;
0990     }
0991     if (qFuzzyCompare(speed, 1.0) && !timeremapInfo.enableRemap) {
0992         // we are requesting a normal speed producer
0993         bool byPassTrackProducer = false;
0994         if (trackId == -1 && (state != PlaylistState::AudioOnly || audioStream == m_masterProducer->get_int("audio_index"))) {
0995             byPassTrackProducer = true;
0996         }
0997         if (byPassTrackProducer ||
0998             (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text ||
0999                                                    m_clipType == ClipType::TextTemplate || m_clipType == ClipType::Qml))) {
1000             // Temporary copy, return clone of master
1001             int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
1002             std::shared_ptr<Mlt::Producer> prod(m_masterProducer->cut(-1, duration > 0 ? duration - 1 : -1));
1003             if (m_clipType == ClipType::Timeline && m_masterProducer->property_exists("kdenlive:maxduration")) {
1004                 prod->set("kdenlive:maxduration", m_masterProducer->get_int("kdenlive:maxduration"));
1005             }
1006             return prod;
1007         }
1008         if (m_timewarpProducers.count(clipId) > 0) {
1009             m_effectStack->removeService(m_timewarpProducers[clipId]);
1010             m_timewarpProducers.erase(clipId);
1011         }
1012         if (state == PlaylistState::AudioOnly) {
1013             // We need to get an audio producer, if none exists
1014             if (audioStream > -1) {
1015                 if (trackId >= 0) {
1016                     trackId += 100 * audioStream;
1017                 } else {
1018                     trackId -= 100 * audioStream;
1019                 }
1020             }
1021             // second playlist producers use negative trackId
1022             if (secondPlaylist) {
1023                 trackId = -trackId;
1024             }
1025             if (m_audioProducers.count(trackId) == 0) {
1026                 if (m_clipType == ClipType::Timeline) {
1027                     std::shared_ptr<Mlt::Producer> prod(m_masterProducer->cut(0, -1));
1028                     m_audioProducers[trackId] = prod;
1029                 } else {
1030                     m_audioProducers[trackId] = cloneProducer(true, true);
1031                 }
1032                 m_audioProducers[trackId]->set("set.test_audio", 0);
1033                 m_audioProducers[trackId]->set("set.test_image", 1);
1034                 if (m_streamEffects.contains(audioStream)) {
1035                     QStringList effects = m_streamEffects.value(audioStream);
1036                     for (const QString &effect : qAsConst(effects)) {
1037                         Mlt::Filter filt(m_audioProducers[trackId]->get_profile(), effect.toUtf8().constData());
1038                         if (filt.is_valid()) {
1039                             // Add stream effect markup
1040                             filt.set("kdenlive:stream", 1);
1041                             m_audioProducers[trackId]->attach(filt);
1042                         }
1043                     }
1044                 }
1045                 if (audioStream > -1) {
1046                     int newAudioStreamIndex = audioStreamIndex(audioStream);
1047                     if (newAudioStreamIndex > -1) {
1048                         /** If the audioStreamIndex is not found, for example when replacing a clip with another one using different indexes,
1049                         default to first audio stream */
1050                         m_audioProducers[trackId]->set("audio_index", audioStream);
1051                     } else {
1052                         newAudioStreamIndex = 0;
1053                     }
1054                     if (newAudioStreamIndex > audioStreamsCount() - 1) {
1055                         newAudioStreamIndex = 0;
1056                     }
1057                     m_audioProducers[trackId]->set("astream", newAudioStreamIndex);
1058                 }
1059                 m_effectStack->addService(m_audioProducers[trackId]);
1060             }
1061             std::shared_ptr<Mlt::Producer> prod(m_audioProducers[trackId]->cut());
1062             if (m_clipType == ClipType::Timeline && m_audioProducers[trackId]->parent().property_exists("kdenlive:maxduration")) {
1063                 int max = m_audioProducers[trackId]->parent().get_int("kdenlive:maxduration");
1064                 prod->set("kdenlive:maxduration", max);
1065                 prod->set("length", max);
1066             }
1067             return prod;
1068         }
1069         if (m_audioProducers.count(trackId) > 0) {
1070             m_effectStack->removeService(m_audioProducers[trackId]);
1071             m_audioProducers.erase(trackId);
1072         }
1073         if (state == PlaylistState::VideoOnly) {
1074             // we return the video producer
1075             // We need to get an video producer, if none exists
1076             // second playlist producers use negative trackId
1077             if (secondPlaylist) {
1078                 trackId = -trackId;
1079             }
1080             if (m_videoProducers.count(trackId) == 0) {
1081                 if (m_clipType == ClipType::Timeline) {
1082                     std::shared_ptr<Mlt::Producer> prod(m_masterProducer->cut(0, -1));
1083                     m_videoProducers[trackId] = prod;
1084                 } else {
1085                     m_videoProducers[trackId] = cloneProducer(true, true);
1086                 }
1087                 if (m_masterProducer->property_exists("kdenlive:maxduration")) {
1088                     m_videoProducers[trackId]->set("kdenlive:maxduration", m_masterProducer->get_int("kdenlive:maxduration"));
1089                 }
1090 
1091                 // Let audio enabled so that we can use audio visualization filters ?
1092                 m_videoProducers[trackId]->set("set.test_audio", 1);
1093                 m_videoProducers[trackId]->set("set.test_image", 0);
1094                 m_effectStack->addService(m_videoProducers[trackId]);
1095             }
1096             int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
1097             return std::shared_ptr<Mlt::Producer>(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration - 1 : -1));
1098         }
1099         if (m_videoProducers.count(trackId) > 0) {
1100             m_effectStack->removeService(m_videoProducers[trackId]);
1101             m_videoProducers.erase(trackId);
1102         }
1103         Q_ASSERT(state == PlaylistState::Disabled);
1104         createDisabledMasterProducer();
1105         int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
1106         std::shared_ptr<Mlt::Producer> prod(m_disabledProducer->cut(-1, duration > 0 ? duration - 1 : -1));
1107         if (m_clipType == ClipType::Timeline && m_videoProducers[trackId]->parent().property_exists("kdenlive:maxduration")) {
1108             int max = m_videoProducers[trackId]->parent().get_int("kdenlive:maxduration");
1109             prod->set("kdenlive:maxduration", max);
1110             prod->set("length", max);
1111         }
1112         return prod;
1113     }
1114 
1115     // For timewarp clips, we keep one separate producer for each clip.
1116     std::shared_ptr<Mlt::Producer> warpProducer;
1117     if (m_timewarpProducers.count(clipId) > 0) {
1118         // remove in all cases, we add it unconditionally anyways
1119         m_effectStack->removeService(m_timewarpProducers[clipId]);
1120         if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) {
1121             // the producer we have is good, use it !
1122             warpProducer = m_timewarpProducers[clipId];
1123             qDebug() << "Reusing timewarp producer!";
1124         } else if (!timeremapInfo.timeMapData.isEmpty()) {
1125             // the producer we have is good, use it !
1126             warpProducer = m_timewarpProducers[clipId];
1127             qDebug() << "Reusing time remap producer for cid: " << clipId;
1128         } else {
1129             m_timewarpProducers.erase(clipId);
1130         }
1131     }
1132     if (!warpProducer) {
1133         QString resource(originalProducer()->get("resource"));
1134         if (resource.isEmpty() || resource == QLatin1String("<producer>")) {
1135             resource = m_service;
1136         }
1137         if (m_clipType == ClipType::Timeline) {
1138             // speed effects of sequence clips have to use an external mlt playslist file
1139             bool ok;
1140             QDir sequenceFolder = pCore->currentDoc()->getCacheDir(CacheTmpWorkFiles, &ok);
1141             if (!ok) {
1142                 qWarning() << "Cannot write to cache folder: " << sequenceFolder.absolutePath();
1143                 return nullptr;
1144             }
1145             resource = sequenceFolder.absoluteFilePath(QString("sequence-%1.mlt").arg(m_sequenceUuid.toString()));
1146             if (!QFileInfo::exists(resource)) {
1147                 cloneProducerToFile(resource);
1148             }
1149         }
1150         if (timeremapInfo.enableRemap) {
1151             Mlt::Chain *chain = new Mlt::Chain(pCore->getProjectProfile(), resource.toUtf8().constData());
1152             Mlt::Link link("timeremap");
1153             if (!timeremapInfo.timeMapData.isEmpty()) {
1154                 link.set("time_map", timeremapInfo.timeMapData.toUtf8().constData());
1155             }
1156             link.set("pitch", timeremapInfo.pitchShift);
1157             link.set("image_mode", timeremapInfo.imageMode.toUtf8().constData());
1158             chain->attach(link);
1159             warpProducer.reset(chain);
1160         } else {
1161             QString url;
1162             QString original_resource;
1163             if (m_clipStatus == FileStatus::StatusMissing) {
1164                 url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed)), QString("qtext"));
1165                 original_resource = originalProducer()->get("resource");
1166 
1167             } else {
1168                 if (resource.endsWith(QLatin1String(":qtext"))) {
1169                     resource.replace(QLatin1String("qtext"), originalProducer()->get("warp_resource"));
1170                 }
1171                 if (m_clipType == ClipType::Timeline || m_clipType == ClipType::Playlist) {
1172                     // We must use the special "consumer" producer for mlt playlist files
1173                     resource.prepend(QStringLiteral("consumer:"));
1174                 }
1175                 url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed)), resource);
1176             }
1177             warpProducer.reset(new Mlt::Producer(pCore->getProjectProfile(), url.toUtf8().constData()));
1178             int original_length = originalProducer()->get_length();
1179             int updated_length = qRound(original_length / std::abs(speed));
1180             warpProducer->set("length", updated_length);
1181             if (!original_resource.isEmpty()) {
1182                 // Don't lose original resource for placeholder clips
1183                 // warpProducer->set("warp_resource", original_resource.toUtf8().constData());
1184                 warpProducer->set("text", i18n("Invalid").toUtf8().constData());
1185             }
1186         }
1187         // this is a workaround to cope with Mlt erroneous rounding
1188         Mlt::Properties original(m_masterProducer->get_properties());
1189         Mlt::Properties cloneProps(warpProducer->get_properties());
1190         cloneProps.pass_list(original, ClipController::getPassPropertiesList(false));
1191 
1192         if (audioStream > -1) {
1193             int newAudioStreamIndex = audioStreamIndex(audioStream);
1194             if (newAudioStreamIndex > -1) {
1195                 /** If the audioStreamIndex is not found, for example when replacing a clip with another one using different indexes,
1196                 default to first audio stream */
1197                 warpProducer->set("audio_index", audioStream);
1198             } else {
1199                 newAudioStreamIndex = 0;
1200             }
1201             if (newAudioStreamIndex > audioStreamsCount() - 1) {
1202                 newAudioStreamIndex = 0;
1203             }
1204             warpProducer->set("astream", newAudioStreamIndex);
1205         } else {
1206             warpProducer->set("audio_index", audioStream);
1207             warpProducer->set("astream", audioStreamIndex(audioStream));
1208         }
1209     }
1210 
1211     // if the producer has a "time-to-live" (frame duration) we need to scale it according to the speed
1212     int ttl = originalProducer()->get_int("ttl");
1213     if (ttl > 0) {
1214         int new_ttl = qRound(ttl / std::abs(speed));
1215         warpProducer->set("ttl", std::max(new_ttl, 1));
1216     }
1217 
1218     qDebug() << "warp LENGTH" << warpProducer->get_length();
1219     warpProducer->set("set.test_audio", 1);
1220     warpProducer->set("set.test_image", 1);
1221     warpProducer->set("kdenlive:id", binId().toUtf8().constData());
1222     if (state == PlaylistState::AudioOnly) {
1223         warpProducer->set("set.test_audio", 0);
1224     }
1225     if (state == PlaylistState::VideoOnly) {
1226         warpProducer->set("set.test_image", 0);
1227     }
1228     m_timewarpProducers[clipId] = warpProducer;
1229     m_effectStack->addService(m_timewarpProducers[clipId]);
1230     return std::shared_ptr<Mlt::Producer>(warpProducer->cut());
1231 }
1232 
1233 std::pair<std::shared_ptr<Mlt::Producer>, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr<Mlt::Producer> master,
1234                                                                                               PlaylistState::ClipState state, int tid, bool secondPlaylist)
1235 {
1236     int in = master->get_in();
1237     int out = master->get_out();
1238     if (master->parent().is_valid()) {
1239         // in that case, we have a cut
1240         // check whether it's a timewarp
1241         double speed = 1.0;
1242         bool timeWarp = false;
1243         ProjectClip::TimeWarpInfo remapInfo;
1244         remapInfo.enableRemap = false;
1245         if (master->parent().property_exists("warp_speed")) {
1246             speed = master->parent().get_double("warp_speed");
1247             timeWarp = true;
1248         } else if (master->parent().type() == mlt_service_chain_type) {
1249             // Check if we have a timeremap link
1250             Mlt::Chain parentChain(master->parent());
1251             if (parentChain.link_count() > 0) {
1252                 for (int i = 0; i < parentChain.link_count(); i++) {
1253                     std::unique_ptr<Mlt::Link> link(parentChain.link(i));
1254                     if (strcmp(link->get("mlt_service"), "timeremap") == 0) {
1255                         if (!link->property_exists("time_map")) {
1256                             link->set("time_map", link->get("map"));
1257                         }
1258                         remapInfo.enableRemap = true;
1259                         remapInfo.timeMapData = link->get("time_map");
1260                         remapInfo.pitchShift = link->get_int("pitch");
1261                         remapInfo.imageMode = link->get("image_mode");
1262                         break;
1263                     }
1264                 }
1265             }
1266         }
1267         if (master->parent().get_int("_loaded") == 1) {
1268             // we already have a clip that shares the same master
1269             if (state != PlaylistState::Disabled || timeWarp || !remapInfo.timeMapData.isEmpty()) {
1270                 // In that case, we must create copies
1271                 std::shared_ptr<Mlt::Producer> prod(
1272                     getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed, secondPlaylist, remapInfo)->cut(in, out));
1273                 return {prod, false};
1274             }
1275             if (state == PlaylistState::Disabled) {
1276                 if (!m_disabledProducer) {
1277                     qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet";
1278                     createDisabledMasterProducer();
1279                 }
1280                 return {std::shared_ptr<Mlt::Producer>(m_disabledProducer->cut(in, out)), false};
1281             }
1282             // We have a good id, this clip can be used
1283             return {master, true};
1284         } else {
1285             master->parent().set("_loaded", 1);
1286             if (timeWarp || !remapInfo.timeMapData.isEmpty()) {
1287                 QString resource = master->parent().get("resource");
1288                 if (master->parent().property_exists("_rebuild") || resource.endsWith(QLatin1String("qtext"))) {
1289                     // This was a placeholder or missing clip, reset producer
1290                     std::shared_ptr<Mlt::Producer> prod(
1291                         getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed, secondPlaylist, remapInfo));
1292                     m_timewarpProducers[clipId] = prod;
1293                 } else {
1294                     m_timewarpProducers[clipId] = std::make_shared<Mlt::Producer>(&master->parent());
1295                 }
1296                 m_effectStack->loadService(m_timewarpProducers[clipId]);
1297                 return {master, true};
1298             }
1299             if (state == PlaylistState::AudioOnly) {
1300                 int audioStream = master->parent().get_int("audio_index");
1301                 if (audioStream > -1) {
1302                     tid += 100 * audioStream;
1303                 }
1304                 if (secondPlaylist) {
1305                     tid = -tid;
1306                 }
1307                 if (m_audioProducers.find(tid) != m_audioProducers.end()) {
1308                     // Buggy project, all clips in a track should use the same track producer, fix
1309                     qDebug() << "/// FOUND INCORRECT PRODUCER ON AUDIO TRACK; FIXING";
1310                     std::shared_ptr<Mlt::Producer> prod(getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed)->cut(in, out));
1311                     return {prod, false};
1312                 }
1313                 m_audioProducers[tid] = std::make_shared<Mlt::Producer>(&master->parent());
1314                 m_effectStack->loadService(m_audioProducers[tid]);
1315                 return {master, true};
1316             }
1317             if (state == PlaylistState::VideoOnly) {
1318                 // good, we found a master video producer, and we didn't have any
1319                 if (m_clipType != ClipType::Color && m_clipType != ClipType::Image && m_clipType != ClipType::Text) {
1320                     // Color, image and text clips always use master producer in timeline
1321                     if (secondPlaylist) {
1322                         tid = -tid;
1323                     }
1324                     if (m_videoProducers.find(tid) != m_videoProducers.end()) {
1325                         qDebug() << "/// FOUND INCORRECT PRODUCER ON VIDEO TRACK; FIXING";
1326                         // Buggy project, all clips in a track should use the same track producer, fix
1327                         std::shared_ptr<Mlt::Producer> prod(
1328                             getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed)->cut(in, out));
1329                         return {prod, false};
1330                     }
1331                     m_videoProducers[tid] = std::make_shared<Mlt::Producer>(&master->parent());
1332                     m_effectStack->loadService(m_videoProducers[tid]);
1333                 } else {
1334                     // Ensure clip out = length - 1 so that effects work correctly
1335                     if (out != master->parent().get_length() - 1) {
1336                         master->parent().set("out", master->parent().get_length() - 1);
1337                     }
1338                 }
1339                 return {master, true};
1340             }
1341             if (state == PlaylistState::Disabled) {
1342                 if (!m_disabledProducer) {
1343                     createDisabledMasterProducer();
1344                 }
1345                 return {std::make_shared<Mlt::Producer>(m_disabledProducer->cut(master->get_in(), master->get_out())), true};
1346             }
1347             qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master";
1348             Q_ASSERT(false);
1349         }
1350     } else if (master->is_valid()) {
1351         // in that case, we have a master
1352         qDebug() << "Warning: weird, we received a master clip in lieue of a cut";
1353         double speed = 1.0;
1354         if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) {
1355             speed = master->get_double("warp_speed");
1356         }
1357         return {getTimelineProducer(-1, clipId, state, master->get_int("audio_index"), speed), false};
1358     }
1359     // we have a problem
1360     return {std::shared_ptr<Mlt::Producer>(ClipController::mediaUnavailable->cut()), false};
1361 }
1362 
1363 void ProjectClip::cloneProducerToFile(const QString &path, bool thumbsProducer)
1364 {
1365     QMutexLocker lk(&m_producerMutex);
1366     Mlt::Consumer c(m_masterProducer->get_profile(), "xml", path.toUtf8().constData());
1367     // Mlt::Service s(m_masterProducer->get_service());
1368     /*int ignore = s.get_int("ignore_points");
1369     if (ignore) {
1370         s.set("ignore_points", 0);
1371     }
1372     c.connect(s);*/
1373     c.set("time_format", "frames");
1374     c.set("no_meta", 1);
1375     c.set("no_root", 1);
1376     if (m_clipType != ClipType::Timeline && m_clipType != ClipType::Playlist && m_clipType != ClipType::Text && m_clipType != ClipType::TextTemplate) {
1377         // Playlist and text clips need to keep their profile info
1378         c.set("no_profile", 1);
1379     }
1380     c.set("root", "/");
1381     if (!thumbsProducer) {
1382         c.set("store", "kdenlive");
1383     }
1384     c.connect(m_masterProducer->parent());
1385     c.run();
1386     /*if (ignore) {
1387         s.set("ignore_points", ignore);
1388     }*/
1389     if (!thumbsProducer && m_usesProxy) {
1390         QFile file(path);
1391         if (file.open(QIODevice::ReadOnly)) {
1392             QTextStream in(&file);
1393             QString content = in.readAll();
1394             file.close();
1395             content.replace(getProducerProperty(QStringLiteral("resource")), getProducerProperty(QStringLiteral("kdenlive:originalurl")));
1396             if (file.open(QIODevice::WriteOnly)) {
1397                 QTextStream out(&file);
1398                 out << content;
1399                 file.close();
1400             }
1401         }
1402     }
1403 }
1404 
1405 void ProjectClip::saveZone(QPoint zone, const QDir &dir)
1406 {
1407     QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt"));
1408     QString fullPath = dir.absoluteFilePath(path);
1409     if (dir.exists(path)) {
1410         QUrl url = QUrl::fromLocalFile(fullPath);
1411         KIO::RenameDialog renameDialog(QApplication::activeWindow(), i18n("File already exists"), url, url, KIO::RenameDialog_Option::RenameDialog_Overwrite);
1412         if (renameDialog.exec() != QDialog::Rejected) {
1413             url = renameDialog.newDestUrl();
1414             if (url.isValid()) {
1415                 fullPath = url.toLocalFile();
1416             }
1417         } else {
1418             return;
1419         }
1420     }
1421     Mlt::Consumer xmlConsumer(pCore->getProjectProfile(), "xml", fullPath.toUtf8().constData());
1422     xmlConsumer.set("terminate_on_pause", 1);
1423     xmlConsumer.set("store", "kdenlive");
1424     xmlConsumer.set("no_meta", 1);
1425     QReadLocker lock(&m_producerLock);
1426     if (m_clipType != ClipType::Timeline) {
1427         Mlt::Producer prod(m_masterProducer->parent());
1428         std::unique_ptr<Mlt::Producer> prod2(prod.cut(zone.x(), zone.y()));
1429         Mlt::Playlist list(pCore->getProjectProfile());
1430         list.insert_at(0, *prod2.get(), 0);
1431         // list.set("title", desc.toUtf8().constData());
1432         xmlConsumer.connect(list);
1433     } else {
1434         xmlConsumer.connect(m_masterProducer->parent());
1435     }
1436     xmlConsumer.run();
1437 }
1438 
1439 std::shared_ptr<Mlt::Producer> ProjectClip::cloneProducer(bool removeEffects, bool timelineProducer)
1440 {
1441     Q_UNUSED(timelineProducer);
1442     QMutexLocker lk(&m_producerMutex);
1443     Mlt::Consumer c(pCore->getProjectProfile(), "xml", "string");
1444     Mlt::Service s(m_masterProducer->get_service());
1445     m_masterProducer->lock();
1446     int ignore = s.get_int("ignore_points");
1447     if (ignore) {
1448         s.set("ignore_points", 0);
1449     }
1450     c.connect(s);
1451     c.set("time_format", "frames");
1452     c.set("no_meta", 1);
1453     c.set("no_root", 1);
1454     c.set("no_profile", 1);
1455     c.set("root", "/");
1456     c.set("store", "kdenlive");
1457     c.run();
1458     if (ignore) {
1459         s.set("ignore_points", ignore);
1460     }
1461     m_masterProducer->unlock();
1462     const QByteArray clipXml = c.get("string");
1463     std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", clipXml.constData()));
1464     if (strcmp(prod->get("mlt_service"), "avformat") == 0) {
1465         prod->set("mlt_service", "avformat-novalidate");
1466         prod->set("mute_on_pause", 0);
1467     }
1468     // TODO: needs more testing, removes clutter from project files
1469     /*if (timelineProducer) {
1470         // Strip the kdenlive: properties, not useful in timeline
1471         const char *prefix = "kdenlive:";
1472         const size_t prefix_len = strlen(prefix);
1473         QStringList propertiesToRemove;
1474         for (int i = prod->count() - 1; i >= 0; --i) {
1475             char *current = prod->get_name(i);
1476             if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) {
1477                 propertiesToRemove << qstrdup(current);
1478             }
1479         }
1480         propertiesToRemove.removeAll(QLatin1String("kdenlive:id"));
1481         qDebug()<<"::: CLEARING PROPERTIES: "<<propertiesToRemove;
1482         Mlt::Properties props(*prod.get());
1483         for (auto &p : propertiesToRemove) {
1484             props.clear(p.toUtf8().constData());
1485         }
1486     } else {*/
1487     // we pass some properties that wouldn't be passed because of the novalidate
1488     const char *prefix = "meta.";
1489     const size_t prefix_len = strlen(prefix);
1490     for (int i = 0; i < m_masterProducer->count(); ++i) {
1491         char *current = m_masterProducer->get_name(i);
1492         if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) {
1493             prod->set(current, m_masterProducer->get(i));
1494         }
1495     }
1496     //}
1497 
1498     if (removeEffects) {
1499         int ct = 0;
1500         Mlt::Filter *filter = prod->filter(ct);
1501         while (filter) {
1502             qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service");
1503             QString ix = QString::fromLatin1(filter->get("kdenlive_id"));
1504             if (!ix.isEmpty()) {
1505                 qDebug() << "/ + + DELETING";
1506                 if (prod->detach(*filter) == 0) {
1507                 } else {
1508                     ct++;
1509                 }
1510             } else {
1511                 ct++;
1512             }
1513             delete filter;
1514             filter = prod->filter(ct);
1515         }
1516     }
1517     prod->set("id", nullptr);
1518     return prod;
1519 }
1520 
1521 std::shared_ptr<Mlt::Producer> ProjectClip::cloneProducer(const std::shared_ptr<Mlt::Producer> &producer)
1522 {
1523     Mlt::Consumer c(pCore->getProjectProfile(), "xml", "string");
1524     Mlt::Service s(producer->get_service());
1525     int ignore = s.get_int("ignore_points");
1526     if (ignore) {
1527         s.set("ignore_points", 0);
1528     }
1529     c.connect(s);
1530     c.set("time_format", "frames");
1531     c.set("no_meta", 1);
1532     c.set("no_root", 1);
1533     c.set("no_profile", 1);
1534     c.set("root", "/");
1535     c.set("store", "kdenlive");
1536     c.start();
1537     if (ignore) {
1538         s.set("ignore_points", ignore);
1539     }
1540     const QByteArray clipXml = c.get("string");
1541     std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", clipXml.constData()));
1542     if (strcmp(prod->get("mlt_service"), "avformat") == 0) {
1543         prod->set("mlt_service", "avformat-novalidate");
1544         prod->set("mute_on_pause", 0);
1545     }
1546     return prod;
1547 }
1548 
1549 std::unique_ptr<Mlt::Producer> ProjectClip::softClone(const char *list)
1550 {
1551     QString service = QString::fromLatin1(m_masterProducer->get("mlt_service"));
1552     QString resource = QString::fromUtf8(m_masterProducer->get("resource"));
1553     std::unique_ptr<Mlt::Producer> clone(new Mlt::Producer(pCore->thumbProfile(), service.toUtf8().constData(), resource.toUtf8().constData()));
1554     Mlt::Filter scaler(pCore->thumbProfile(), "swscale");
1555     Mlt::Filter converter(pCore->getProjectProfile(), "avcolor_space");
1556     clone->attach(scaler);
1557     clone->attach(converter);
1558     Mlt::Properties original(m_masterProducer->get_properties());
1559     Mlt::Properties cloneProps(clone->get_properties());
1560     cloneProps.pass_list(original, list);
1561     return clone;
1562 }
1563 
1564 std::unique_ptr<Mlt::Producer> ProjectClip::getClone()
1565 {
1566     const char *list = ClipController::getPassPropertiesList();
1567     QString service = QString::fromLatin1(m_masterProducer->get("mlt_service"));
1568     QString resource = QString::fromUtf8(m_masterProducer->get("resource"));
1569     std::unique_ptr<Mlt::Producer> clone(new Mlt::Producer(m_masterProducer->get_profile(), service.toUtf8().constData(), resource.toUtf8().constData()));
1570     Mlt::Properties original(m_masterProducer->get_properties());
1571     Mlt::Properties cloneProps(clone->get_properties());
1572     cloneProps.pass_list(original, list);
1573     return clone;
1574 }
1575 
1576 QPoint ProjectClip::zone() const
1577 {
1578     int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in"));
1579     int max = getFramePlaytime();
1580     int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max);
1581     if (out <= in) {
1582         out = max;
1583     }
1584     return QPoint(in, out);
1585 }
1586 
1587 const QString ProjectClip::hash(bool createIfEmpty)
1588 {
1589     if (m_clipStatus == FileStatus::StatusWaiting) {
1590         // Clip is not ready
1591         return QString();
1592     }
1593     QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
1594     if (!clipHash.isEmpty() || !createIfEmpty) {
1595         return clipHash;
1596     }
1597     return getFileHash();
1598 }
1599 
1600 const QString ProjectClip::hashForThumbs()
1601 {
1602     if (m_clipStatus == FileStatus::StatusWaiting) {
1603         // Clip is not ready
1604         return QString();
1605     }
1606     if (m_clipType == ClipType::Timeline) {
1607         return m_sequenceUuid.toString();
1608     }
1609     QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
1610     if (!clipHash.isEmpty() && m_hasMultipleVideoStreams) {
1611         clipHash.append(m_properties->get("video_index"));
1612     }
1613     return clipHash;
1614 }
1615 
1616 const QByteArray ProjectClip::getFolderHash(const QDir &dir, QString fileName)
1617 {
1618     QStringList files = dir.entryList(QDir::Files);
1619     fileName.append(files.join(QLatin1Char(',')));
1620     // Include file hash info in case we have several folders with same file names (can happen for image sequences)
1621     if (!files.isEmpty()) {
1622         QPair<QByteArray, qint64> hashData = calculateHash(dir.absoluteFilePath(files.first()));
1623         fileName.append(hashData.first);
1624         fileName.append(QString::number(hashData.second));
1625         if (files.size() > 1) {
1626             hashData = calculateHash(dir.absoluteFilePath(files.at(files.size() / 2)));
1627             fileName.append(hashData.first);
1628             fileName.append(QString::number(hashData.second));
1629         }
1630     }
1631     QByteArray fileData = fileName.toUtf8();
1632     return QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1633 }
1634 
1635 const QString ProjectClip::getFileHash()
1636 {
1637     QByteArray fileData;
1638     QByteArray fileHash;
1639 
1640     switch (m_clipType) {
1641     case ClipType::SlideShow:
1642         fileHash = getFolderHash(QFileInfo(clipUrl()).absoluteDir(), QFileInfo(clipUrl()).fileName());
1643         break;
1644     case ClipType::Text:
1645         fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8();
1646         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1647         break;
1648     case ClipType::TextTemplate:
1649         fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
1650         fileData.append(getProducerProperty(QStringLiteral("templatetext")).toUtf8());
1651         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1652         break;
1653     case ClipType::QText:
1654         fileData = getProducerProperty(QStringLiteral("text")).toUtf8();
1655         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1656         break;
1657     case ClipType::Color:
1658         fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
1659         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1660         break;
1661     case ClipType::Timeline:
1662         fileData = m_sequenceUuid.toString().toUtf8();
1663         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1664         break;
1665     default:
1666         QPair<QByteArray, qint64> hashData = calculateHash(clipUrl());
1667         fileHash = hashData.first;
1668         ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(hashData.second));
1669         break;
1670     }
1671     if (fileHash.isEmpty()) {
1672         if (m_service == QLatin1String("blipflash")) {
1673             // Used in tests
1674             fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
1675             fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1676         } else {
1677             qDebug() << "// WARNING EMPTY CLIP HASH: ";
1678             return QString();
1679         }
1680     }
1681     QString result = fileHash.toHex();
1682     ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result);
1683     return result;
1684 }
1685 
1686 const QPair<QByteArray, qint64> ProjectClip::calculateHash(const QString &path)
1687 {
1688     QFile file(path);
1689     QByteArray fileHash;
1690     qint64 fSize = 0;
1691     if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file
1692         /*
1693          * 1 MB = 1 second per 450 files (or faster)
1694          * 10 MB = 9 seconds per 450 files (or faster)
1695          */
1696         QByteArray fileData;
1697         fSize = file.size();
1698         if (fSize > 2000000) {
1699             fileData = file.read(1000000);
1700             if (file.seek(file.size() - 1000000)) {
1701                 fileData.append(file.readAll());
1702             }
1703         } else {
1704             fileData = file.readAll();
1705         }
1706         file.close();
1707         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1708     }
1709     return {fileHash, fSize};
1710 }
1711 
1712 double ProjectClip::getOriginalFps() const
1713 {
1714     return originalFps();
1715 }
1716 
1717 void ProjectClip::setProperties(const QMap<QString, QString> &properties, bool refreshPanel)
1718 {
1719     qDebug() << "// SETTING CLIP PROPERTIES: " << properties;
1720     QMapIterator<QString, QString> i(properties);
1721     QMap<QString, QString> passProperties;
1722     bool refreshAnalysis = false;
1723     bool reload = false;
1724     bool refreshOnly = true;
1725     if (properties.contains(QStringLiteral("templatetext"))) {
1726         m_description = properties.value(QStringLiteral("templatetext"));
1727         if (auto ptr = m_model.lock())
1728             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1729                                                                            {AbstractProjectItem::DataDescription});
1730         refreshPanel = true;
1731     }
1732     // Some properties also need to be passed to track producers
1733     QStringList timelineProperties{
1734         QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"),         QStringLiteral("threads"),
1735         QStringLiteral("force_colorspace"),   QStringLiteral("force_tff"),           QStringLiteral("force_progressive"), QStringLiteral("video_delay")};
1736     QStringList forceReloadProperties{QStringLiteral("rotate"),      QStringLiteral("autorotate"),     QStringLiteral("resource"),
1737                                       QStringLiteral("force_fps"),   QStringLiteral("set.test_image"), QStringLiteral("video_index"),
1738                                       QStringLiteral("disable_exif")};
1739     QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"),      QStringLiteral("ttl"),
1740                      QStringLiteral("softness"),      QStringLiteral("crop"),      QStringLiteral("animation"), QStringLiteral("low-pass")};
1741     QVector<int> updateRoles;
1742     while (i.hasNext()) {
1743         i.next();
1744         setProducerProperty(i.key(), i.value());
1745         if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) {
1746             reload = true;
1747             refreshOnly = false;
1748         }
1749         if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) {
1750             refreshAnalysis = true;
1751         }
1752         if (timelineProperties.contains(i.key())) {
1753             passProperties.insert(i.key(), i.value());
1754         }
1755     }
1756     if (m_clipType == ClipType::QText && properties.contains(QStringLiteral("text"))) {
1757         reload = true;
1758         refreshOnly = false;
1759     }
1760     if (m_clipType == ClipType::TextTemplate && properties.contains(QStringLiteral("templatetext"))) {
1761         m_masterProducer->lock();
1762         m_masterProducer->set("force_reload", 1);
1763         m_masterProducer->unlock();
1764         ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
1765         reload = true;
1766         refreshOnly = true;
1767         updateRoles << TimelineModel::ResourceRole;
1768     }
1769     if (properties.contains(QStringLiteral("resource"))) {
1770         // Clip source was changed, update important stuff
1771         refreshPanel = true;
1772         reload = true;
1773         ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
1774         resetProducerProperty(QStringLiteral("kdenlive:file_hash"));
1775         if (m_clipType == ClipType::Color) {
1776             refreshOnly = true;
1777             updateRoles << TimelineModel::ResourceRole;
1778         } else if (properties.contains("_fullreload")) {
1779             // Clip resource changed, update thumbnail, name, clear hash
1780             refreshOnly = false;
1781             // Enforce reloading clip type in case of clip replacement
1782             if (m_clipType == ClipType::Image) {
1783                 // If replacing an image with another one, don't clear type so duration is preserved
1784                 QMimeDatabase db;
1785                 QMimeType type = db.mimeTypeForUrl(QUrl::fromLocalFile(properties.value(QStringLiteral("resource"))));
1786                 if (!type.name().startsWith(QLatin1String("image/"))) {
1787                     m_service.clear();
1788                     m_clipType = ClipType::Unknown;
1789                 }
1790             } else {
1791                 m_service.clear();
1792                 m_clipType = ClipType::Unknown;
1793             }
1794             clearBackupProperties();
1795             updateRoles << TimelineModel::ResourceRole << TimelineModel::MaxDurationRole << TimelineModel::NameRole;
1796         }
1797     }
1798     if (properties.contains(QStringLiteral("kdenlive:proxy")) && !properties.contains("_fullreload")) {
1799         QString value = properties.value(QStringLiteral("kdenlive:proxy"));
1800         // If value is "-", that means user manually disabled proxy on this clip
1801         ObjectId oid(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid());
1802         if (value.isEmpty() || value == QLatin1String("-")) {
1803             // reset proxy
1804             if (pCore->taskManager.hasPendingJob(oid, AbstractTask::PROXYJOB)) {
1805                 // The proxy clip is being created, abort
1806                 pCore->taskManager.discardJobs(oid, AbstractTask::PROXYJOB);
1807             } else {
1808                 reload = true;
1809                 refreshOnly = false;
1810                 // Restore original url
1811                 QString resource = getProducerProperty(QStringLiteral("kdenlive:originalurl"));
1812                 if (!resource.isEmpty()) {
1813                     setProducerProperty(QStringLiteral("resource"), resource);
1814                 }
1815             }
1816         } else {
1817             // A proxy was requested, make sure to keep original url
1818             setProducerProperty(QStringLiteral("kdenlive:originalurl"), url());
1819             backupOriginalProperties();
1820             ProxyTask::start(oid, this);
1821         }
1822     } else if (!reload) {
1823         const QList<QString> propKeys = properties.keys();
1824         for (const QString &k : propKeys) {
1825             if (forceReloadProperties.contains(k)) {
1826                 refreshPanel = true;
1827                 refreshOnly = false;
1828                 reload = true;
1829                 ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
1830                 break;
1831             }
1832         }
1833     }
1834     if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) {
1835         reload = true;
1836         updateRoles << TimelineModel::ResourceRole;
1837     }
1838     if (refreshAnalysis) {
1839         Q_EMIT refreshAnalysisPanel();
1840     }
1841     if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) {
1842         // Make sure length is >= kdenlive:duration
1843         int producerLength = getProducerIntProperty(QStringLiteral("length"));
1844         int kdenliveLength = getFramePlaytime();
1845         if (producerLength < kdenliveLength) {
1846             setProducerProperty(QStringLiteral("length"), kdenliveLength);
1847         }
1848         m_duration = getStringDuration();
1849         if (auto ptr = m_model.lock())
1850             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1851                                                                            {AbstractProjectItem::DataDuration});
1852         refreshOnly = false;
1853         reload = m_clipType != ClipType::Timeline;
1854     }
1855     QVector<int> refreshRoles;
1856     if (properties.contains(QStringLiteral("kdenlive:tags"))) {
1857         setTags(properties.value(QStringLiteral("kdenlive:tags")));
1858         if (auto ptr = m_model.lock()) {
1859             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1860                                                                            {AbstractProjectItem::DataTag});
1861         }
1862         refreshRoles << TimelineModel::TagRole;
1863     }
1864     if (properties.contains(QStringLiteral("kdenlive:clipname"))) {
1865         const QString updatedName = properties.value(QStringLiteral("kdenlive:clipname"));
1866         if (updatedName.isEmpty()) {
1867             if (m_clipType != ClipType::Timeline && m_clipType != ClipType::Text && m_clipType != ClipType::TextTemplate) {
1868                 m_name = QFileInfo(m_path).fileName();
1869             }
1870         } else {
1871             m_name = updatedName;
1872         }
1873         refreshPanel = true;
1874         if (auto ptr = m_model.lock()) {
1875             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1876                                                                            {AbstractProjectItem::DataName});
1877         }
1878         refreshRoles << TimelineModel::NameRole;
1879         if (m_clipType == ClipType::Timeline && !m_sequenceUuid.isNull()) {
1880             // This is a timeline clip, update tab name
1881             Q_EMIT pCore->bin()->updateTabName(m_sequenceUuid, m_name);
1882         }
1883     }
1884     if (properties.contains(QStringLiteral("kdenlive:description"))) {
1885         m_description = properties.value(QStringLiteral("kdenlive:description"));
1886         refreshPanel = true;
1887         if (auto ptr = m_model.lock()) {
1888             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1889                                                                            {AbstractProjectItem::DataDescription});
1890         }
1891     }
1892     // update timeline clips
1893     if (!reload) {
1894         updateTimelineClips(refreshRoles);
1895     }
1896     bool audioStreamChanged = properties.contains(QStringLiteral("audio_index")) || properties.contains(QStringLiteral("astream"));
1897     if (reload) {
1898         // producer has changed, refresh monitor and thumbnail
1899         if (hasProxy()) {
1900             ObjectId oid(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid());
1901             pCore->taskManager.discardJobs(oid, AbstractTask::PROXYJOB);
1902             setProducerProperty(QStringLiteral("_overwriteproxy"), 1);
1903             ProxyTask::start(oid, this);
1904         } else {
1905             reloadProducer(refreshOnly, properties.contains(QStringLiteral("kdenlive:proxy")));
1906         }
1907         if (refreshOnly) {
1908             if (auto ptr = m_model.lock()) {
1909                 Q_EMIT std::static_pointer_cast<ProjectItemModel>(ptr)->refreshClip(m_binId);
1910             }
1911         }
1912         if (!updateRoles.isEmpty()) {
1913             updateTimelineClips(updateRoles);
1914         }
1915     } else {
1916         if (properties.contains(QStringLiteral("kdenlive:active_streams")) && m_audioInfo) {
1917             // Clip is a multi audio stream and currently in clip monitor, update target tracks
1918             m_audioInfo->updateActiveStreams(properties.value(QStringLiteral("kdenlive:active_streams")));
1919             pCore->bin()->updateTargets(clipId());
1920             if (!audioStreamChanged) {
1921                 pCore->bin()->reloadMonitorStreamIfActive(clipId());
1922                 pCore->bin()->checkProjectAudioTracks(clipId(), m_audioInfo->activeStreams().count());
1923                 refreshPanel = true;
1924             }
1925         }
1926         if (audioStreamChanged) {
1927             refreshAudioInfo();
1928             Q_EMIT audioThumbReady();
1929             pCore->bin()->reloadMonitorStreamIfActive(clipId());
1930             refreshPanel = true;
1931         }
1932     }
1933     if (refreshPanel && m_properties) {
1934         // Some of the clip properties have changed through a command, update properties panel
1935         Q_EMIT refreshPropertiesPanel();
1936     }
1937     if (!passProperties.isEmpty() && (!reload || refreshOnly)) {
1938         for (auto &p : m_audioProducers) {
1939             QMapIterator<QString, QString> pr(passProperties);
1940             while (pr.hasNext()) {
1941                 pr.next();
1942                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1943             }
1944         }
1945         for (auto &p : m_videoProducers) {
1946             QMapIterator<QString, QString> pr(passProperties);
1947             while (pr.hasNext()) {
1948                 pr.next();
1949                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1950             }
1951         }
1952         for (auto &p : m_timewarpProducers) {
1953             QMapIterator<QString, QString> pr(passProperties);
1954             while (pr.hasNext()) {
1955                 pr.next();
1956                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1957             }
1958         }
1959     }
1960 }
1961 
1962 void ProjectClip::refreshTracksState(int tracksCount)
1963 {
1964     if (tracksCount > -1) {
1965         setProducerProperty(QStringLiteral("kdenlive:sequenceproperties.tracksCount"), tracksCount);
1966     }
1967     if (m_clipStatus == FileStatus::StatusReady) {
1968         checkAudioVideo();
1969         Q_EMIT refreshPropertiesPanel();
1970     }
1971 }
1972 
1973 ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent)
1974 {
1975     auto ptr = m_model.lock();
1976     Q_ASSERT(ptr);
1977     auto *panel = new ClipPropertiesController(clipName(), static_cast<ClipController *>(this), parent);
1978     connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties);
1979     connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData);
1980     connect(this, &ProjectClip::updateStreamInfo, panel, &ClipPropertiesController::updateStreamInfo);
1981     connect(panel, &ClipPropertiesController::requestProxy, this, [this](bool doProxy) {
1982         QList<std::shared_ptr<ProjectClip>> clipList{std::static_pointer_cast<ProjectClip>(shared_from_this())};
1983         pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList);
1984     });
1985     connect(panel, &ClipPropertiesController::deleteProxy, [this]() { deleteProxy(); });
1986     return panel;
1987 }
1988 
1989 void ProjectClip::deleteProxy(bool reloadClip)
1990 {
1991     // Disable proxy file
1992     QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy"));
1993     QList<std::shared_ptr<ProjectClip>> clipList{std::static_pointer_cast<ProjectClip>(shared_from_this())};
1994     if (reloadClip) {
1995         pCore->currentDoc()->slotProxyCurrentItem(false, clipList);
1996     }
1997     // Delete
1998     bool ok;
1999     QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok);
2000     if (ok && proxy.length() > 2) {
2001         proxy = QFileInfo(proxy).fileName();
2002         if (dir.exists(proxy)) {
2003             dir.remove(proxy);
2004         }
2005     }
2006 }
2007 
2008 void ProjectClip::updateParent(std::shared_ptr<TreeItem> parent)
2009 {
2010     if (parent) {
2011         auto item = std::static_pointer_cast<AbstractProjectItem>(parent);
2012         ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId());
2013     }
2014     AbstractProjectItem::updateParent(parent);
2015 }
2016 
2017 bool ProjectClip::matches(const QString &condition)
2018 {
2019     // TODO
2020     Q_UNUSED(condition)
2021     return true;
2022 }
2023 
2024 QString ProjectClip::clipName()
2025 {
2026     if (m_name.isEmpty()) {
2027         m_name = getProducerProperty(QStringLiteral("kdenlive:clipname"));
2028         if (m_name.isEmpty()) {
2029             m_name = m_path.isEmpty() || m_clipType == ClipType::Timeline ? i18n("Unnamed") : QFileInfo(m_path).fileName();
2030         }
2031     }
2032     return m_name;
2033 }
2034 
2035 bool ProjectClip::rename(const QString &name, int column)
2036 {
2037     QMap<QString, QString> newProperties;
2038     QMap<QString, QString> oldProperties;
2039     bool edited = false;
2040     switch (column) {
2041     case 0:
2042         if (m_name == name || ((m_clipType == ClipType::Timeline || m_clipType == ClipType::Text) && name.isEmpty())) {
2043             return false;
2044         }
2045         // Rename clip
2046         oldProperties.insert(QStringLiteral("kdenlive:clipname"), m_name);
2047         newProperties.insert(QStringLiteral("kdenlive:clipname"), name);
2048         edited = true;
2049         break;
2050     case 2:
2051         if (m_description == name) {
2052             return false;
2053         }
2054         // Rename clip
2055         if (m_clipType == ClipType::TextTemplate) {
2056             oldProperties.insert(QStringLiteral("templatetext"), m_description);
2057             newProperties.insert(QStringLiteral("templatetext"), name);
2058         } else {
2059             oldProperties.insert(QStringLiteral("kdenlive:description"), m_description);
2060             newProperties.insert(QStringLiteral("kdenlive:description"), name);
2061         }
2062         edited = true;
2063         break;
2064     }
2065     if (edited) {
2066         pCore->bin()->slotEditClipCommand(m_binId, oldProperties, newProperties);
2067     }
2068     return edited;
2069 }
2070 
2071 const QVariant ProjectClip::getData(DataType type) const
2072 {
2073     switch (type) {
2074     case AbstractProjectItem::IconOverlay:
2075         if (m_clipStatus == FileStatus::StatusMissing) {
2076             return QVariant("window-close");
2077         }
2078         if (m_clipStatus == FileStatus::StatusWaiting) {
2079             return QVariant("view-refresh");
2080         }
2081         if (m_properties && m_properties->get_int("meta.media.variable_frame_rate")) {
2082             return QVariant("emblem-warning");
2083         }
2084         return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant();
2085     default:
2086         return AbstractProjectItem::getData(type);
2087     }
2088 }
2089 
2090 bool ProjectClip::hasVariableFps()
2091 {
2092     if (m_properties && m_properties->get_int("meta.media.variable_frame_rate")) {
2093         return true;
2094     }
2095     return false;
2096 }
2097 
2098 int ProjectClip::audioChannels() const
2099 {
2100     if (!audioInfo()) {
2101         return 0;
2102     }
2103     return audioInfo()->channels();
2104 }
2105 
2106 void ProjectClip::discardAudioThumb()
2107 {
2108     if (!m_audioInfo) {
2109         return;
2110     }
2111     pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), AbstractTask::AUDIOTHUMBJOB);
2112     QString audioThumbPath;
2113     QList<int> streams = m_audioInfo->streams().keys();
2114     // Delete audio thumbnail data
2115     for (int &st : streams) {
2116         audioThumbPath = getAudioThumbPath(st);
2117         if (!audioThumbPath.isEmpty()) {
2118             QFile::remove(audioThumbPath);
2119         }
2120         // Clear audio cache
2121         QString key = QString("%1:%2").arg(m_binId).arg(st);
2122         pCore->audioThumbCache.insert(key, QByteArray("-"));
2123     }
2124     // Delete thumbnail
2125     for (int &st : streams) {
2126         audioThumbPath = getAudioThumbPath(st);
2127         if (!audioThumbPath.isEmpty()) {
2128             QFile::remove(audioThumbPath);
2129         }
2130     }
2131 
2132     resetProducerProperty(QStringLiteral("kdenlive:audio_max"));
2133     m_audioThumbCreated = false;
2134     refreshAudioInfo();
2135 }
2136 
2137 int ProjectClip::getAudioStreamFfmpegIndex(int mltStream)
2138 {
2139     if (!m_masterProducer || !audioInfo()) {
2140         return -1;
2141     }
2142     QList<int> audioStreams = audioInfo()->streams().keys();
2143     if (audioStreams.contains(mltStream)) {
2144         return audioStreams.indexOf(mltStream);
2145     }
2146     return -1;
2147 }
2148 
2149 const QString ProjectClip::getAudioThumbPath(int stream)
2150 {
2151     if (audioInfo() == nullptr) {
2152         return QString();
2153     }
2154     bool ok;
2155     QDir thumbFolder = pCore->projectManager()->cacheDir(true, &ok);
2156     if (!ok) {
2157         qWarning() << "Cannot write to cache folder: " << thumbFolder.absolutePath();
2158         return QString();
2159     }
2160     const QString clipHash = hash(false);
2161     if (clipHash.isEmpty()) {
2162         return QString();
2163     }
2164     QString audioPath = thumbFolder.absoluteFilePath(clipHash);
2165     audioPath.append(QLatin1Char('_') + QString::number(stream));
2166     int roundedFps = int(pCore->getCurrentFps());
2167     audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps));
2168     return audioPath;
2169 }
2170 
2171 QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset)
2172 {
2173     if (data.isEmpty()) {
2174         // Remove data
2175         return QStringList() << QString("kdenlive:clipanalysis." + name) << QString();
2176         // m_controller->resetProperty("kdenlive:clipanalysis." + name);
2177     }
2178     QString current = getProducerProperty("kdenlive:clipanalysis." + name);
2179     if (!current.isEmpty()) {
2180         // TODO
2181         /*if (KMessageBox::questionTwoActions(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(),
2182                                             KGuiItem(i18n("Merge")), KStandardGuiItem::add()) == KMessageBox::PrimaryAction) {
2183             // Merge data
2184             // TODO MLT7: convert to Mlt::Animation
2185             auto &profile = pCore->getCurrentProfile();
2186             Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
2187             Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
2188             Mlt::GeometryItem item;
2189             int pos = 0;
2190             while (newGeometry.next_key(&item, pos) == 0) {
2191                 pos = item.frame();
2192                 item.frame(pos + offset);
2193                 pos++;
2194                 geometry.insert(item);
2195             }
2196             return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise();
2197             // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise());
2198         }*/
2199         // Add data with another name
2200         int i = 1;
2201         QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i));
2202         while (!previous.isEmpty()) {
2203             ++i;
2204             previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i));
2205         }
2206         return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset);
2207         // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset));
2208     }
2209     return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset);
2210     // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset));
2211 }
2212 
2213 QMap<QString, QString> ProjectClip::analysisData(bool withPrefix)
2214 {
2215     return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix);
2216 }
2217 
2218 const QString ProjectClip::geometryWithOffset(const QString &data, int offset)
2219 {
2220     if (offset == 0) {
2221         return data;
2222     }
2223     // TODO MLT7: port to Mlt::Animation
2224     /*auto &profile = pCore->getCurrentProfile();
2225     Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
2226     Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height());
2227     Mlt::GeometryItem item;
2228     int pos = 0;
2229     while (geometry.next_key(&item, pos) == 0) {
2230         pos = item.frame();
2231         item.frame(pos + offset);
2232         pos++;
2233         newgeometry.insert(item);
2234     }
2235     return newgeometry.serialise();
2236     */
2237     return QString();
2238 }
2239 
2240 bool ProjectClip::isSplittable() const
2241 {
2242     return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist || m_clipType == ClipType::Timeline);
2243 }
2244 
2245 void ProjectClip::setBinEffectsEnabled(bool enabled)
2246 {
2247     ClipController::setBinEffectsEnabled(enabled);
2248 }
2249 
2250 void ProjectClip::registerService(std::weak_ptr<TimelineModel> timeline, int clipId, const std::shared_ptr<Mlt::Producer> &service, bool forceRegister)
2251 {
2252     if (!service->is_cut() || forceRegister) {
2253         int hasAudio = service->get_int("set.test_audio") == 0;
2254         int hasVideo = service->get_int("set.test_image") == 0;
2255         if (hasVideo && m_videoProducers.count(clipId) == 0) {
2256             // This is an undo producer, register it!
2257             m_videoProducers[clipId] = service;
2258             m_effectStack->addService(m_videoProducers[clipId]);
2259         } else if (hasAudio && m_audioProducers.count(clipId) == 0) {
2260             // This is an undo producer, register it!
2261             m_audioProducers[clipId] = service;
2262             m_effectStack->addService(m_audioProducers[clipId]);
2263         }
2264     }
2265     registerTimelineClip(std::move(timeline), clipId);
2266 }
2267 
2268 void ProjectClip::registerTimelineClip(std::weak_ptr<TimelineModel> timeline, int clipId)
2269 {
2270     Q_ASSERT(!timeline.expired());
2271     uint currentCount = 0;
2272     if (auto ptr = timeline.lock()) {
2273         if (m_hasAudio) {
2274             if (ptr->getClipState(clipId) == PlaylistState::AudioOnly) {
2275                 m_AudioUsage++;
2276             }
2277         }
2278         const QUuid uuid = ptr->uuid();
2279         if (m_registeredClipsByUuid.contains(uuid)) {
2280             QList<int> values = m_registeredClipsByUuid.value(uuid);
2281             Q_ASSERT(values.contains(clipId) == false);
2282             values << clipId;
2283             currentCount = values.size();
2284             m_registeredClipsByUuid[uuid] = values;
2285         } else {
2286             m_registeredClipsByUuid.insert(uuid, {clipId});
2287             currentCount = 1;
2288         }
2289     }
2290     uint totalCount = 0;
2291     QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2292     while (i.hasNext()) {
2293         i.next();
2294         totalCount += i.value().size();
2295     }
2296     setRefCount(currentCount, totalCount);
2297     Q_EMIT registeredClipChanged();
2298 }
2299 
2300 void ProjectClip::checkClipBounds()
2301 {
2302     m_boundaryTimer.start();
2303 }
2304 
2305 void ProjectClip::refreshBounds()
2306 {
2307     QVector<QPoint> boundaries;
2308     uint currentCount = 0;
2309     const QUuid uuid = pCore->currentTimelineId();
2310     if (m_registeredClipsByUuid.contains(uuid)) {
2311         const QList<int> clips = m_registeredClipsByUuid.value(uuid);
2312         currentCount = clips.size();
2313         auto timeline = pCore->currentDoc()->getTimeline(uuid);
2314         for (auto &c : clips) {
2315             QPoint point = timeline->getClipInDuration(c);
2316             if (!boundaries.contains(point)) {
2317                 boundaries << point;
2318             }
2319         }
2320     }
2321     uint totalCount = 0;
2322     QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2323     while (i.hasNext()) {
2324         i.next();
2325         totalCount += i.value().size();
2326     }
2327     setRefCount(currentCount, totalCount);
2328     Q_EMIT boundsChanged(boundaries);
2329 }
2330 
2331 void ProjectClip::deregisterTimelineClip(int clipId, bool audioClip, const QUuid &uuid)
2332 {
2333     if (m_hasAudio && audioClip) {
2334         m_AudioUsage--;
2335     }
2336     if (m_videoProducers.count(clipId) > 0) {
2337         m_effectStack->removeService(m_videoProducers[clipId]);
2338         m_videoProducers.erase(clipId);
2339     }
2340     if (m_audioProducers.count(clipId) > 0) {
2341         m_effectStack->removeService(m_audioProducers[clipId]);
2342         m_audioProducers.erase(clipId);
2343     }
2344     // Clip might already have been deregistered
2345     if (m_registeredClipsByUuid.contains(uuid)) {
2346         QList<int> clips = m_registeredClipsByUuid.value(uuid);
2347         Q_ASSERT(clips.contains(clipId));
2348         clips.removeAll(clipId);
2349         if (clips.isEmpty()) {
2350             m_registeredClipsByUuid.remove(uuid);
2351         } else {
2352             m_registeredClipsByUuid[uuid] = clips;
2353         }
2354         uint currentCount = 0;
2355         uint totalCount = 0;
2356         QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2357         while (i.hasNext()) {
2358             i.next();
2359             totalCount += i.value().size();
2360             if (i.key() == pCore->currentTimelineId()) {
2361                 currentCount = uint(i.value().size());
2362             }
2363         }
2364         setRefCount(currentCount, totalCount);
2365         Q_EMIT registeredClipChanged();
2366     }
2367 }
2368 
2369 QList<int> ProjectClip::timelineInstances(QUuid activeUuid) const
2370 {
2371     if (activeUuid.isNull()) {
2372         activeUuid = pCore->currentTimelineId();
2373     }
2374     if (!m_registeredClipsByUuid.contains(activeUuid)) {
2375         return {};
2376     }
2377     return m_registeredClipsByUuid.value(activeUuid);
2378 }
2379 
2380 QMap<QUuid, QList<int>> ProjectClip::getAllTimelineInstances() const
2381 {
2382     return m_registeredClipsByUuid;
2383 }
2384 
2385 QStringList ProjectClip::timelineSequenceExtraResources() const
2386 {
2387     QStringList urls;
2388     if (m_clipType != ClipType::Timeline) {
2389         return urls;
2390     }
2391     for (auto &warp : m_timewarpProducers) {
2392         urls << warp.second->get("warp_resource");
2393     }
2394     urls.removeDuplicates();
2395     return urls;
2396 }
2397 
2398 const QString ProjectClip::isReferenced(const QUuid &activeUuid) const
2399 {
2400     if (m_registeredClipsByUuid.contains(activeUuid) && !m_registeredClipsByUuid.value(activeUuid).isEmpty()) {
2401         return m_binId;
2402     }
2403     return QString();
2404 }
2405 
2406 void ProjectClip::purgeReferences(const QUuid &activeUuid, bool deleteClip)
2407 {
2408     if (!m_registeredClipsByUuid.contains(activeUuid)) {
2409         return;
2410     }
2411     if (deleteClip) {
2412         QList<int> toDelete = m_registeredClipsByUuid.value(activeUuid);
2413         auto timeline = pCore->currentDoc()->getTimeline(activeUuid);
2414         while (!toDelete.isEmpty()) {
2415             int id = toDelete.takeFirst();
2416             if (m_hasAudio) {
2417                 if (timeline->getClipState(id) == PlaylistState::AudioOnly) {
2418                     m_AudioUsage--;
2419                 }
2420             }
2421         }
2422     }
2423     m_registeredClipsByUuid.remove(activeUuid);
2424     uint currentCount = 0;
2425     uint totalCount = 0;
2426     QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2427     while (i.hasNext()) {
2428         i.next();
2429         totalCount += i.value().size();
2430         if (i.key() == pCore->currentTimelineId()) {
2431             currentCount = uint(i.value().size());
2432         }
2433     }
2434     setRefCount(currentCount, totalCount);
2435     Q_EMIT registeredClipChanged();
2436 }
2437 
2438 bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo)
2439 {
2440     Fun operation = [this]() {
2441         // Free audio thumb data and timeline producers
2442         pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()));
2443         m_audioLevels.clear();
2444         m_disabledProducer.reset();
2445         m_audioProducers.clear();
2446         m_videoProducers.clear();
2447         if (m_timewarpProducers.size() > 0 && pCore->window() && pCore->bin()->isEnabled()) {
2448             // If the clip is deleted, remove timewarp producers. Don't delete if Bin is disabled because this is when we are closing a project
2449             if (m_clipType == ClipType::Timeline) {
2450                 bool ok;
2451                 QDir sequenceFolder = pCore->currentDoc()->getCacheDir(CacheTmpWorkFiles, &ok);
2452                 if (ok) {
2453                     QString resource = sequenceFolder.absoluteFilePath(QString("sequence-%1.mlt").arg(m_sequenceUuid.toString()));
2454                     QFile::remove(resource);
2455                 }
2456             }
2457         }
2458         m_timewarpProducers.clear();
2459         return true;
2460     };
2461     operation();
2462     QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2463     while (i.hasNext()) {
2464         i.next();
2465         const QUuid uuid = i.key();
2466         QList<int> instances = i.value();
2467         if (!instances.isEmpty()) {
2468             auto timeline = pCore->currentDoc()->getTimeline(uuid, pCore->projectItemModel()->closing);
2469             if (!timeline) {
2470                 if (pCore->projectItemModel()->closing) {
2471                     break;
2472                 }
2473                 qDebug() << "Error while deleting clip: timeline unavailable";
2474                 Q_ASSERT(false);
2475                 return false;
2476             }
2477             for (int cid : instances) {
2478                 if (!timeline->isClip(cid)) {
2479                     // clip already deleted, was probably grouped with another one
2480                     continue;
2481                 }
2482                 timeline->requestClipUngroup(cid, undo, redo);
2483                 if (!timeline->requestItemDeletion(cid, undo, redo, true)) {
2484                     return false;
2485                 }
2486             }
2487             if (timeline->isClosed) {
2488                 // Refresh timeline occurences
2489                 pCore->currentDoc()->setModified(true);
2490                 pCore->currentDoc()->setSequenceThumbRequiresUpdate(uuid);
2491                 pCore->projectManager()->doSyncTimeline(timeline, false);
2492             }
2493         }
2494     }
2495     m_registeredClipsByUuid.clear();
2496     PUSH_LAMBDA(operation, redo);
2497     return AbstractProjectItem::selfSoftDelete(undo, redo);
2498 }
2499 
2500 void ProjectClip::copyTimeWarpProducers(const QDir sequenceFolder, bool copy)
2501 {
2502     if (m_clipType == ClipType::Timeline) {
2503         for (auto &warp : m_timewarpProducers) {
2504             const QString service(warp.second->get("mlt_service"));
2505             QString path;
2506             bool isTimeWarp = false;
2507             const QString resource(warp.second->get("resource"));
2508             if (service == QLatin1String("timewarp")) {
2509                 path = warp.second->get("warp_resource");
2510                 isTimeWarp = true;
2511             } else {
2512                 path = resource;
2513             }
2514             bool consumerProducer = false;
2515             if (resource.contains(QLatin1String("consumer:"))) {
2516                 consumerProducer = true;
2517             }
2518             if (path.startsWith(QLatin1String("consumer:"))) {
2519                 path = path.section(QLatin1Char(':'), 1);
2520             }
2521             if (QFileInfo(path).isRelative()) {
2522                 path.prepend(pCore->currentDoc()->documentRoot());
2523             }
2524             QString destFile = sequenceFolder.absoluteFilePath(QFileInfo(path).fileName());
2525             if (copy) {
2526                 if (!destFile.endsWith(QLatin1String(".mlt")) || destFile == path) {
2527                     continue;
2528                 }
2529                 QFile::remove(destFile);
2530                 QFile::copy(path, destFile);
2531             }
2532             if (isTimeWarp) {
2533                 warp.second->set("warp_resource", destFile.toUtf8().constData());
2534                 QString speed(warp.second->get("warp_speed"));
2535                 speed.append(QStringLiteral(":"));
2536                 if (consumerProducer) {
2537                     destFile.prepend(QStringLiteral("consumer:"));
2538                 }
2539                 destFile.prepend(speed);
2540                 warp.second->set("resource", destFile.toUtf8().constData());
2541 
2542             } else {
2543                 if (consumerProducer) {
2544                     destFile.prepend(QStringLiteral("consumer:"));
2545                 }
2546                 warp.second->set("resource", destFile.toUtf8().constData());
2547             }
2548         }
2549     }
2550 }
2551 
2552 void ProjectClip::reloadTimeline(std::shared_ptr<EffectStackModel> stack)
2553 {
2554     if (pCore->bin()) {
2555         pCore->bin()->reloadMonitorIfActive(m_binId);
2556     }
2557     for (auto &p : m_audioProducers) {
2558         m_effectStack->removeService(p.second);
2559     }
2560     for (auto &p : m_videoProducers) {
2561         m_effectStack->removeService(p.second);
2562     }
2563     for (auto &p : m_timewarpProducers) {
2564         m_effectStack->removeService(p.second);
2565     }
2566     // Release audio producers
2567     m_audioProducers.clear();
2568     m_videoProducers.clear();
2569     if (m_timewarpProducers.size() > 0) {
2570         if (m_clipType == ClipType::Timeline) {
2571             bool ok;
2572             QDir sequenceFolder = pCore->currentDoc()->getCacheDir(CacheTmpWorkFiles, &ok);
2573             if (ok) {
2574                 QString resource = sequenceFolder.absoluteFilePath(QString("sequence-%1.mlt").arg(m_sequenceUuid.toString()));
2575                 QFile::remove(resource);
2576             }
2577         }
2578     }
2579     m_timewarpProducers.clear();
2580     Q_EMIT refreshPropertiesPanel();
2581     replaceInTimeline();
2582     updateTimelineClips({TimelineModel::IsProxyRole});
2583     if (stack) {
2584         m_effectStack = stack;
2585     }
2586 }
2587 
2588 Fun ProjectClip::getAudio_lambda()
2589 {
2590     return [this]() {
2591         if (KdenliveSettings::audiothumbnails() &&
2592             (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || (m_clipType == ClipType::Playlist && m_hasAudio)) && m_audioLevels.isEmpty()) {
2593             // Generate audio levels
2594             AudioLevelsTask::start(ObjectId(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid()), this, false);
2595         }
2596         return true;
2597     };
2598 }
2599 
2600 bool ProjectClip::isIncludedInTimeline()
2601 {
2602     return !m_registeredClipsByUuid.isEmpty();
2603 }
2604 
2605 void ProjectClip::replaceInTimeline()
2606 {
2607     int updatedDuration = m_resetTimelineOccurences ? getFramePlaytime() : -1;
2608     Fun undo = []() { return true; };
2609     Fun redo = []() { return true; };
2610     bool pushUndo = false;
2611     QMapIterator<QUuid, QList<int>> i(m_registeredClipsByUuid);
2612     while (i.hasNext()) {
2613         i.next();
2614         QList<int> instances = i.value();
2615         if (!instances.isEmpty()) {
2616             auto timeline = pCore->currentDoc()->getTimeline(i.key());
2617             if (!timeline) {
2618                 if (pCore->projectItemModel()->closing) {
2619                     break;
2620                 }
2621                 qDebug() << "Error while reloading clip: timeline unavailable";
2622                 Q_ASSERT(false);
2623             }
2624             for (auto &cid : instances) {
2625                 if (timeline->requestClipReload(cid, updatedDuration, undo, redo)) {
2626                     pushUndo = true;
2627                 }
2628             }
2629         }
2630     }
2631     if (pushUndo && !m_resetTimelineOccurences) {
2632         pCore->pushUndo(undo, redo, i18n("Adjust timeline clips"));
2633     }
2634     m_resetTimelineOccurences = false;
2635 }
2636 
2637 void ProjectClip::updateTimelineClips(const QVector<int> &roles)
2638 {
2639     const QUuid uuid = pCore->currentTimelineId();
2640     if (m_registeredClipsByUuid.contains(uuid)) {
2641         QList<int> instances = m_registeredClipsByUuid.value(uuid);
2642         if (!instances.isEmpty()) {
2643             auto timeline = pCore->currentDoc()->getTimeline(uuid);
2644             if (!timeline) {
2645                 if (pCore->projectItemModel()->closing) {
2646                     return;
2647                 }
2648                 qDebug() << "Error while reloading clip: timeline unavailable";
2649                 Q_ASSERT(false);
2650             }
2651             for (auto &cid : instances) {
2652                 timeline->requestClipUpdate(cid, roles);
2653             }
2654         }
2655     }
2656 }
2657 
2658 void ProjectClip::updateZones()
2659 {
2660     int zonesCount = childCount();
2661     if (zonesCount == 0) {
2662         resetProducerProperty(QStringLiteral("kdenlive:clipzones"));
2663         return;
2664     }
2665     QJsonArray list;
2666     for (int i = 0; i < zonesCount; ++i) {
2667         std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
2668         if (clip) {
2669             QJsonObject currentZone;
2670             currentZone.insert(QLatin1String("name"), QJsonValue(clip->name()));
2671             QPoint zone = clip->zone();
2672             currentZone.insert(QLatin1String("in"), QJsonValue(zone.x()));
2673             currentZone.insert(QLatin1String("out"), QJsonValue(zone.y()));
2674             if (clip->rating() > 0) {
2675                 currentZone.insert(QLatin1String("rating"), QJsonValue(int(clip->rating())));
2676             }
2677             if (!clip->tags().isEmpty()) {
2678                 currentZone.insert(QLatin1String("tags"), QJsonValue(clip->tags()));
2679             }
2680             list.push_back(currentZone);
2681         }
2682     }
2683     QJsonDocument json(list);
2684     setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson()));
2685 }
2686 
2687 int ProjectClip::getThumbFrame() const
2688 {
2689     if (m_clipType == ClipType::Timeline) {
2690         return qMax(0, pCore->currentDoc()->getSequenceProperty(m_sequenceUuid, QStringLiteral("thumbnailFrame")).toInt());
2691     }
2692     return qMax(0, getProducerIntProperty(QStringLiteral("kdenlive:thumbnailFrame")));
2693 }
2694 
2695 void ProjectClip::getThumbFromPercent(int percent, bool storeFrame)
2696 {
2697     // extract a maximum of 30 frames for bin preview
2698     if (percent < 0) {
2699         int framePos = getThumbFrame();
2700         if (framePos > 0) {
2701             QImage thumb = ThumbnailCache::get()->getThumbnail(hashForThumbs(), m_binId, framePos);
2702             if (!thumb.isNull()) {
2703                 setThumbnail(thumb, -1, -1);
2704             }
2705         }
2706         return;
2707     }
2708     int duration = getFramePlaytime();
2709     int steps = qCeil(qMax(pCore->getCurrentFps(), double(duration) / 30));
2710     int framePos = duration * percent / 100;
2711     framePos -= framePos % steps;
2712     QImage thumb = ThumbnailCache::get()->getThumbnail(hashForThumbs(), m_binId, framePos);
2713     if (!thumb.isNull()) {
2714         setThumbnail(thumb, -1, -1);
2715     } else {
2716         // Generate percent thumbs
2717         ObjectId oid(KdenliveObjectType::BinClip, m_binId.toInt(), QUuid());
2718         if (!pCore->taskManager.hasPendingJob(oid, AbstractTask::CACHEJOB)) {
2719             CacheTask::start(oid, 30, 0, 0, this);
2720         }
2721     }
2722     if (storeFrame) {
2723         if (m_clipType == ClipType::Timeline) {
2724             pCore->currentDoc()->setSequenceProperty(m_sequenceUuid, QStringLiteral("thumbnailFrame"), framePos);
2725         } else {
2726             setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), framePos);
2727         }
2728     }
2729 }
2730 
2731 void ProjectClip::setRating(uint rating)
2732 {
2733     AbstractProjectItem::setRating(rating);
2734     setProducerProperty(QStringLiteral("kdenlive:rating"), int(rating));
2735     pCore->currentDoc()->setModified(true);
2736 }
2737 
2738 int ProjectClip::getAudioMax(int stream)
2739 {
2740     const QString key = QString("kdenlive:audio_max%1").arg(stream);
2741     if (m_masterProducer->property_exists(key.toUtf8().constData())) {
2742         return m_masterProducer->get_int(key.toUtf8().constData());
2743     }
2744     // Process audio max for the stream
2745     const QString key2 = QString("_kdenlive:audio%1").arg(stream);
2746     if (!m_masterProducer->property_exists(key2.toUtf8().constData())) {
2747         return 0;
2748     }
2749     const QVector<uint8_t> audioData = *static_cast<QVector<uint8_t> *>(m_masterProducer->get_data(key2.toUtf8().constData()));
2750     if (audioData.isEmpty()) {
2751         return 0;
2752     }
2753     uint max = *std::max_element(audioData.constBegin(), audioData.constEnd());
2754     m_masterProducer->set(key.toUtf8().constData(), int(max));
2755     return int(max);
2756 }
2757 
2758 const QVector<uint8_t> ProjectClip::audioFrameCache(int stream)
2759 {
2760     QVector<uint8_t> audioLevels;
2761     if (stream == -1) {
2762         if (m_audioInfo) {
2763             stream = m_audioInfo->ffmpeg_audio_index();
2764         } else {
2765             return audioLevels;
2766         }
2767     }
2768     const QString key = QString("_kdenlive:audio%1").arg(stream);
2769     if (m_masterProducer->get_data(key.toUtf8().constData())) {
2770         const QVector<uint8_t> audioData = *static_cast<QVector<uint8_t> *>(m_masterProducer->get_data(key.toUtf8().constData()));
2771         return audioData;
2772     } else {
2773         qDebug() << "=== AUDIO NOT FOUND ";
2774     }
2775     return QVector<uint8_t>();
2776 
2777     // TODO
2778     /*QString key = QString("%1:%2").arg(m_binId).arg(stream);
2779     QByteArray audioData;
2780     if (pCore->audioThumbCache.find(key, &audioData)) {
2781         if (audioData != QByteArray("-")) {
2782             QDataStream in(audioData);
2783             in >> audioLevels;
2784             return audioLevels;
2785         }
2786     }
2787     // convert cached image
2788     const QString cachePath = getAudioThumbPath(stream);
2789     // checking for cached thumbs
2790     QImage image(cachePath);
2791     if (!image.isNull()) {
2792         int channels = m_audioInfo->channelsForStream(stream);
2793         int n = image.width() * image.height();
2794         for (int i = 0; i < n; i++) {
2795             QRgb p = image.pixel(i / channels, i % channels);
2796             audioLevels << uint8_t(qRed(p));
2797             audioLevels << uint8_t(qGreen(p));
2798             audioLevels << uint8_t(qBlue(p));
2799             audioLevels << uint8_t(qAlpha(p));
2800         }
2801         // populate vector
2802         QDataStream st(&audioData, QIODevice::WriteOnly);
2803         st << audioLevels;
2804         pCore->audioThumbCache.insert(key, audioData);
2805     }
2806     return audioLevels;*/
2807 }
2808 
2809 void ProjectClip::setClipStatus(FileStatus::ClipStatus status)
2810 {
2811     FileStatus::ClipStatus previousStatus = m_clipStatus;
2812     AbstractProjectItem::setClipStatus(status);
2813     updateTimelineClips({TimelineModel::StatusRole});
2814     if (auto ptr = m_model.lock()) {
2815         std::shared_ptr<ProjectItemModel> model = std::static_pointer_cast<ProjectItemModel>(ptr);
2816         model->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()), {AbstractProjectItem::IconOverlay});
2817         if (status == FileStatus::StatusMissing || previousStatus == FileStatus::StatusMissing) {
2818             model->missingClipTimer.start();
2819         }
2820     }
2821 }
2822 
2823 void ProjectClip::renameAudioStream(int id, const QString &name)
2824 {
2825     if (m_audioInfo) {
2826         m_audioInfo->renameStream(id, name);
2827         QString prop = QString("kdenlive:streamname.%1").arg(id);
2828         m_masterProducer->set(prop.toUtf8().constData(), name.toUtf8().constData());
2829         if (m_audioInfo->activeStreams().keys().contains(id)) {
2830             pCore->bin()->updateTargets(clipId());
2831         }
2832         pCore->bin()->reloadMonitorStreamIfActive(clipId());
2833     }
2834 }
2835 
2836 void ProjectClip::requestAddStreamEffect(int streamIndex, const QString effectName)
2837 {
2838     QStringList readEffects = m_streamEffects.value(streamIndex);
2839     QString oldEffect;
2840     // Remove effect if present (parameters might have changed
2841     for (const QString &effect : qAsConst(readEffects)) {
2842         if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
2843             oldEffect = effect;
2844             break;
2845         }
2846     }
2847     Fun redo = [this, streamIndex, effectName]() {
2848         addAudioStreamEffect(streamIndex, effectName);
2849         Q_EMIT updateStreamInfo(streamIndex);
2850         return true;
2851     };
2852     Fun undo = [this, streamIndex, effectName, oldEffect]() {
2853         if (!oldEffect.isEmpty()) {
2854             // restore previous parameter value
2855             addAudioStreamEffect(streamIndex, oldEffect);
2856         } else {
2857             removeAudioStreamEffect(streamIndex, effectName);
2858         }
2859         Q_EMIT updateStreamInfo(streamIndex);
2860         return true;
2861     };
2862     addAudioStreamEffect(streamIndex, effectName);
2863     pCore->pushUndo(undo, redo, i18n("Add stream effect"));
2864 }
2865 
2866 void ProjectClip::requestRemoveStreamEffect(int streamIndex, const QString effectName)
2867 {
2868     QStringList readEffects = m_streamEffects.value(streamIndex);
2869     QString oldEffect = effectName;
2870     // Remove effect if present (parameters might have changed
2871     for (const QString &effect : qAsConst(readEffects)) {
2872         if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
2873             oldEffect = effect;
2874             break;
2875         }
2876     }
2877     Fun undo = [this, streamIndex, effectName, oldEffect]() {
2878         addAudioStreamEffect(streamIndex, oldEffect);
2879         Q_EMIT updateStreamInfo(streamIndex);
2880         return true;
2881     };
2882     Fun redo = [this, streamIndex, effectName]() {
2883         removeAudioStreamEffect(streamIndex, effectName);
2884         Q_EMIT updateStreamInfo(streamIndex);
2885         return true;
2886     };
2887     removeAudioStreamEffect(streamIndex, effectName);
2888     pCore->pushUndo(undo, redo, i18n("Remove stream effect"));
2889 }
2890 
2891 void ProjectClip::addAudioStreamEffect(int streamIndex, const QString effectName)
2892 {
2893     QString addedEffectName;
2894     QMap<QString, QString> effectParams;
2895     if (effectName.contains(QLatin1Char(' '))) {
2896         // effect has parameters
2897         QStringList params = effectName.split(QLatin1Char(' '));
2898         addedEffectName = params.takeFirst();
2899         for (const QString &p : qAsConst(params)) {
2900             QStringList paramValue = p.split(QLatin1Char('='));
2901             if (paramValue.size() == 2) {
2902                 effectParams.insert(paramValue.at(0), paramValue.at(1));
2903             }
2904         }
2905     } else {
2906         addedEffectName = effectName;
2907     }
2908     QStringList effects;
2909     if (m_streamEffects.contains(streamIndex)) {
2910         QStringList readEffects = m_streamEffects.value(streamIndex);
2911         // Remove effect if present (parameters might have changed
2912         for (const QString &effect : qAsConst(readEffects)) {
2913             if (effect == addedEffectName || effect.startsWith(addedEffectName + QStringLiteral(" "))) {
2914                 continue;
2915             }
2916             effects << effect;
2917         }
2918         effects << effectName;
2919     } else {
2920         effects = QStringList({effectName});
2921     }
2922     m_streamEffects.insert(streamIndex, effects);
2923     setProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex), effects.join(QLatin1Char('#')));
2924     for (auto &p : m_audioProducers) {
2925         int stream = p.first / 100;
2926         if (stream == streamIndex) {
2927             // Remove existing effects with same name
2928             int max = p.second->filter_count();
2929             for (int i = 0; i < max; i++) {
2930                 QScopedPointer<Mlt::Filter> f(p.second->filter(i));
2931                 if (f->get("mlt_service") == addedEffectName) {
2932                     p.second->detach(*f.get());
2933                     break;
2934                 }
2935             }
2936             Mlt::Filter filt(p.second->get_profile(), addedEffectName.toUtf8().constData());
2937             if (filt.is_valid()) {
2938                 // Add stream effect markup
2939                 filt.set("kdenlive:stream", 1);
2940                 // Set parameters
2941                 QMapIterator<QString, QString> i(effectParams);
2942                 while (i.hasNext()) {
2943                     i.next();
2944                     filt.set(i.key().toUtf8().constData(), i.value().toUtf8().constData());
2945                 }
2946                 p.second->attach(filt);
2947             }
2948         }
2949     }
2950 }
2951 
2952 void ProjectClip::removeAudioStreamEffect(int streamIndex, QString effectName)
2953 {
2954     QStringList effects;
2955     if (effectName.contains(QLatin1Char(' '))) {
2956         effectName = effectName.section(QLatin1Char(' '), 0, 0);
2957     }
2958     if (m_streamEffects.contains(streamIndex)) {
2959         QStringList readEffects = m_streamEffects.value(streamIndex);
2960         // Remove effect if present (parameters might have changed
2961         for (const QString &effect : qAsConst(readEffects)) {
2962             if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
2963                 continue;
2964             }
2965             effects << effect;
2966         }
2967         if (effects.isEmpty()) {
2968             m_streamEffects.remove(streamIndex);
2969             resetProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex));
2970         } else {
2971             m_streamEffects.insert(streamIndex, effects);
2972             setProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex), effects.join(QLatin1Char('#')));
2973         }
2974     } else {
2975         // No effects for this stream, this is not expected, abort
2976         return;
2977     }
2978     for (auto &p : m_audioProducers) {
2979         int stream = p.first / 100;
2980         if (stream == streamIndex) {
2981             int max = p.second->filter_count();
2982             for (int i = 0; i < max; i++) {
2983                 std::shared_ptr<Mlt::Filter> fl(p.second->filter(i));
2984                 if (!fl->is_valid()) {
2985                     continue;
2986                 }
2987                 if (fl->get_int("kdenlive:stream") != 1) {
2988                     // This is not an audio stream effect
2989                     continue;
2990                 }
2991                 if (fl->get("mlt_service") == effectName) {
2992                     p.second->detach(*fl.get());
2993                     break;
2994                 }
2995             }
2996         }
2997     }
2998 }
2999 
3000 QStringList ProjectClip::getAudioStreamEffect(int streamIndex) const
3001 {
3002     QStringList effects;
3003     if (m_streamEffects.contains(streamIndex)) {
3004         effects = m_streamEffects.value(streamIndex);
3005     }
3006     return effects;
3007 }
3008 
3009 void ProjectClip::updateTimelineOnReload()
3010 {
3011     const QUuid uuid = pCore->currentTimelineId();
3012     if (m_registeredClipsByUuid.contains(uuid)) {
3013         QList<int> instances = m_registeredClipsByUuid.value(uuid);
3014         if (!instances.isEmpty() && instances.size() < 3) {
3015             auto timeline = pCore->currentDoc()->getTimeline(uuid);
3016             if (timeline) {
3017                 for (auto &cid : instances) {
3018                     if (timeline->getClipPlaytime(cid) > static_cast<int>(frameDuration())) {
3019                         // reload producer
3020                         m_resetTimelineOccurences = true;
3021                         break;
3022                     }
3023                 }
3024             }
3025         }
3026     }
3027 }
3028 
3029 void ProjectClip::updateJobProgress()
3030 {
3031     if (auto ptr = m_model.lock()) {
3032         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(m_binId, AbstractProjectItem::JobProgress);
3033     }
3034 }
3035 
3036 void ProjectClip::setInvalid()
3037 {
3038     m_isInvalid = true;
3039     m_producerLock.unlock();
3040 }
3041 
3042 void ProjectClip::updateProxyProducer(const QString &path)
3043 {
3044     resetProducerProperty(QStringLiteral("_overwriteproxy"));
3045     setProducerProperty(QStringLiteral("resource"), path);
3046     reloadProducer(false, true);
3047 }
3048 
3049 void ProjectClip::importJsonMarkers(const QString &json)
3050 {
3051     getMarkerModel()->importFromJson(json, true);
3052 }
3053 
3054 const QStringList ProjectClip::enforcedParams() const
3055 {
3056     QStringList params;
3057     QStringList paramNames = {QStringLiteral("rotate"), QStringLiteral("autorotate")};
3058     for (auto &name : paramNames) {
3059         if (hasProducerProperty(name)) {
3060             params << QString("%1=%2").arg(name, getProducerProperty(name));
3061         }
3062     }
3063     return params;
3064 }
3065 
3066 const QString ProjectClip::baseThumbPath()
3067 {
3068     return QString("%1/%2/#").arg(m_binId).arg(m_uuid.toString());
3069 }
3070 
3071 bool ProjectClip::canBeDropped(const QUuid &uuid) const
3072 {
3073     if (m_sequenceUuid == uuid) {
3074         return false;
3075     }
3076     if (auto ptr = m_model.lock()) {
3077         return std::static_pointer_cast<ProjectItemModel>(ptr)->canBeEmbeded(uuid, m_sequenceUuid);
3078     } else {
3079         qDebug() << "..... ERROR CANNOT LOCK MODEL";
3080     }
3081     return true;
3082 }
3083 
3084 const QList<QUuid> ProjectClip::registeredUuids() const
3085 {
3086     return m_registeredClipsByUuid.keys();
3087 }
3088 
3089 const QUuid &ProjectClip::getSequenceUuid() const
3090 {
3091     return m_sequenceUuid;
3092 }
3093 
3094 void ProjectClip::updateDescription()
3095 {
3096     if (m_clipType == ClipType::TextTemplate) {
3097         m_description = getProducerProperty(QStringLiteral("templatetext"));
3098     } else {
3099         m_description = getProducerProperty(QStringLiteral("kdenlive:description"));
3100         if (m_description.isEmpty()) {
3101             m_description = getProducerProperty(QStringLiteral("meta.attr.comment.markup"));
3102         }
3103     }
3104 }
3105 
3106 QPixmap ProjectClip::pixmap(int framePosition, int width, int height)
3107 {
3108     // TODO refac this should use the new thumb infrastructure
3109     QReadLocker lock(&m_producerLock);
3110     std::unique_ptr<Mlt::Producer> thumbProducer = getThumbProducer();
3111     if (thumbProducer == nullptr) {
3112         return QPixmap();
3113     }
3114     thumbProducer->seek(framePosition);
3115     QScopedPointer<Mlt::Frame> frame(thumbProducer->get_frame());
3116     if (frame == nullptr || !frame->is_valid()) {
3117         QPixmap p(width, height);
3118         p.fill(QColor(Qt::red).rgb());
3119         return p;
3120     }
3121     frame->set("consumer.deinterlacer", "onefield");
3122     frame->set("consumer.top_field_first", -1);
3123     frame->set("consumer.rescale", "nearest");
3124     QImage img = KThumb::getFrame(frame.data());
3125     return QPixmap::fromImage(img /*.scaled(height, width, Qt::KeepAspectRatio)*/);
3126 }