1 /*
2 SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
3 SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
4 This file is part of Kdenlive. See www.kdenlive.org.
5 
6 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7 */
8 
9 #include "projectclip.h"
10 #include "bin.h"
11 #include "core.h"
12 #include "doc/docundostack.hpp"
13 #include "doc/kdenlivedoc.h"
14 #include "doc/kthumb.h"
15 #include "effects/effectstack/model/effectstackmodel.hpp"
16 #include "jobs/audiolevelstask.h"
17 #include "jobs/cliploadtask.h"
18 #include "jobs/proxytask.h"
19 #include "jobs/cachetask.h"
20 #include "kdenlivesettings.h"
21 #include "lib/audio/audioStreamInfo.h"
22 #include "mltcontroller/clipcontroller.h"
23 #include "mltcontroller/clippropertiescontroller.h"
24 #include "model/markerlistmodel.hpp"
25 #include "profiles/profilemodel.hpp"
26 #include "project/projectcommands.h"
27 #include "project/projectmanager.h"
28 #include "projectfolder.h"
29 #include "projectitemmodel.h"
30 #include "projectsubclip.h"
31 #include "clipcreator.hpp"
32 #include "timecode.h"
33 #include "timeline2/model/snapmodel.hpp"
34 #include "macros.hpp"
35 
36 #include "utils/thumbnailcache.hpp"
37 #include "xml/xml.hpp"
38 #include <QPainter>
39 #include <kimagecache.h>
40 
41 #include "kdenlive_debug.h"
42 #include <KLocalizedString>
43 #include <KMessageBox>
44 #include <QApplication>
45 #include <QCryptographicHash>
46 #include <QDir>
47 #include <QDomElement>
48 #include <QFile>
49 #include <memory>
50 
51 #ifdef CRASH_AUTO_TEST
52 #include "logger.hpp"
53 #pragma GCC diagnostic push
54 #pragma GCC diagnostic ignored "-Wunused-parameter"
55 #pragma GCC diagnostic ignored "-Wsign-conversion"
56 #pragma GCC diagnostic ignored "-Wfloat-equal"
57 #pragma GCC diagnostic ignored "-Wshadow"
58 #pragma GCC diagnostic ignored "-Wpedantic"
59 #include <rttr/registration>
60 
61 #pragma GCC diagnostic pop
62 RTTR_REGISTRATION
63 {
64     using namespace rttr;
65     registration::class_<ProjectClip>("ProjectClip");
66 }
67 #endif
68 
69 
70 ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model, std::shared_ptr<Mlt::Producer> producer)
71     : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
72     , ClipController(id, std::move(producer))
73     , m_resetTimelineOccurences(false)
74     , m_audioCount(0)
75 {
76     m_markerModel = std::make_shared<MarkerListModel>(id, pCore->projectManager()->undoStack());
77     if (producer->get_int("_placeholder") == 1) {
78         m_clipStatus = FileStatus::StatusMissing;
79     } else if (producer->get_int("_missingsource") == 1) {
80         m_clipStatus = FileStatus::StatusProxyOnly;
81     } else if (m_usesProxy) {
82         m_clipStatus = FileStatus::StatusProxy;
83     } else {
84         m_clipStatus = FileStatus::StatusReady;
85     }
86     m_name = clipName();
87     m_duration = getStringDuration();
88     m_inPoint = 0;
89     m_outPoint = 0;
90     m_date = date;
91     m_description = ClipController::description();
92     if (m_clipType == ClipType::Audio) {
93         m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
94     } else {
95         m_thumbnail = thumb;
96     }
97     // Make sure we have a hash for this clip
98     hash();
99     m_boundaryTimer.setSingleShot(true);
100     m_boundaryTimer.setInterval(500);
101     if (m_hasLimitedDuration) {
102         connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
103     }
__anonc1dd28370102() 104     connect(m_markerModel.get(), &MarkerListModel::modelChanged, this, [&]() {
105         setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson());
106     });
107     QString markers = getProducerProperty(QStringLiteral("kdenlive:markers"));
108     if (!markers.isEmpty()) {
109         QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(QString, markers), Q_ARG(bool, true),
110                                   Q_ARG(bool, false));
111     }
112     setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
113     AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
114     connectEffectStack();
115     if (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly) {
116         // Generate clip thumbnail
117         ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, QDomElement(), true, -1, -1, this);
118         // Generate audio thumbnail
119         if (KdenliveSettings::audiothumbnails() && (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Playlist || m_clipType == ClipType::Unknown)) {
120             AudioLevelsTask::start({ObjectType::BinClip, m_binId.toInt()}, this, false);
121         }
122     }
123 }
124 
125 // static
construct(const QString & id,const QIcon & thumb,const std::shared_ptr<ProjectItemModel> & model,const std::shared_ptr<Mlt::Producer> & producer)126 std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model,
127                                                     const std::shared_ptr<Mlt::Producer> &producer)
128 {
129     std::shared_ptr<ProjectClip> self(new ProjectClip(id, thumb, model, producer));
130     baseFinishConstruct(self);
131     QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(QString, id), Q_ARG(QString, self->getProducerProperty(QStringLiteral("kdenlive:clipzones"))));
132     return self;
133 }
134 
importEffects(const std::shared_ptr<Mlt::Producer> & producer,QString originalDecimalPoint)135 void ProjectClip::importEffects(const std::shared_ptr<Mlt::Producer> &producer, QString originalDecimalPoint)
136 {
137     m_effectStack->importEffects(producer, PlaylistState::Disabled, true, originalDecimalPoint);
138 }
139 
ProjectClip(const QString & id,const QDomElement & description,const QIcon & thumb,const std::shared_ptr<ProjectItemModel> & model)140 ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model)
141     : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
142     , ClipController(id)
143     , m_resetTimelineOccurences(false)
144     , m_audioCount(0)
145 {
146     m_clipStatus = FileStatus::StatusWaiting;
147     m_thumbnail = thumb;
148     m_markerModel = std::make_shared<MarkerListModel>(m_binId, pCore->projectManager()->undoStack());
149     if (description.hasAttribute(QStringLiteral("type"))) {
150         m_clipType = ClipType::ProducerType(description.attribute(QStringLiteral("type")).toInt());
151         if (m_clipType == ClipType::Audio) {
152             m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
153         }
154     }
155     m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource"));
156     QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname"));
157     if (!clipName.isEmpty()) {
158         m_name = clipName;
159     } else if (!m_temporaryUrl.isEmpty()) {
160         m_name = QFileInfo(m_temporaryUrl).fileName();
161     } else {
162         m_name = i18n("Untitled");
163     }
164     m_boundaryTimer.setSingleShot(true);
165     m_boundaryTimer.setInterval(500);
166     connect(m_markerModel.get(), &MarkerListModel::modelChanged, this, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
167 }
168 
construct(const QString & id,const QDomElement & description,const QIcon & thumb,std::shared_ptr<ProjectItemModel> model)169 std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb,
170                                                     std::shared_ptr<ProjectItemModel> model)
171 {
172     std::shared_ptr<ProjectClip> self(new ProjectClip(id, description, thumb, std::move(model)));
173     baseFinishConstruct(self);
174     return self;
175 }
176 
~ProjectClip()177 ProjectClip::~ProjectClip()
178 {
179 }
180 
connectEffectStack()181 void ProjectClip::connectEffectStack()
182 {
183     connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, [&]() {
184         if (auto ptr = m_model.lock()) {
185             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
186                                                                            AbstractProjectItem::IconOverlay);
187         }
188     });
189 }
190 
getToolTip() const191 QString ProjectClip::getToolTip() const
192 {
193     if (m_clipType == ClipType::Color && m_path.contains(QLatin1Char('/'))) {
194         return m_path.section(QLatin1Char('/'), -1);
195     }
196     return m_path;
197 }
198 
getXmlProperty(const QDomElement & producer,const QString & propertyName,const QString & defaultValue)199 QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue)
200 {
201     QString value = defaultValue;
202     QDomNodeList props = producer.elementsByTagName(QStringLiteral("property"));
203     for (int i = 0; i < props.count(); ++i) {
204         if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) {
205             value = props.at(i).firstChild().nodeValue();
206             break;
207         }
208     }
209     return value;
210 }
211 
updateAudioThumbnail()212 void ProjectClip::updateAudioThumbnail()
213 {
214     emit audioThumbReady();
215     if (m_clipType == ClipType::Audio) {
216         QImage thumb = ThumbnailCache::get()->getThumbnail(m_binId, 0);
217         if (thumb.isNull() && !pCore->taskManager.hasPendingJob({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::AUDIOTHUMBJOB)) {
218             int iconHeight = int(QFontInfo(qApp->font()).pixelSize() * 3.5);
219             QImage img(QSize(int(iconHeight * pCore->getCurrentDar()), iconHeight), QImage::Format_ARGB32);
220             img.fill(Qt::darkGray);
221             QMap <int, QString> streams = audioInfo()->streams();
222             QMap <int, int> channelsList = audioInfo()->streamChannels();
223             QPainter painter(&img);
224             QPen pen = painter.pen();
225             pen.setColor(Qt::white);
226             painter.setPen(pen);
227             int streamCount = 0;
228             if (streams.count() > 0) {
229                 double streamHeight = iconHeight / streams.count();
230                 QMapIterator<int, QString> st(streams);
231                 while (st.hasNext()) {
232                     st.next();
233                     int channels = channelsList.value(st.key());
234                     double channelHeight = double(streamHeight) / channels;
235                     const QVector <uint8_t> audioLevels = audioFrameCache(st.key());
236                     qreal indicesPrPixel = qreal(audioLevels.length()) / img.width();
237                     int idx;
238                     for (int channel = 0; channel < channels; channel++) {
239                         double y = (streamHeight * streamCount) + (channel * channelHeight) + channelHeight / 2;
240                         for (int i = 0; i <= img.width(); i++) {
241                             idx = int(ceil(i * indicesPrPixel));
242                             idx += idx % channels;
243                             idx += channel;
244                             if (idx >= audioLevels.length() || idx < 0) {
245                                 break;
246                             }
247                             double level = audioLevels.at(idx) * channelHeight / 510.; // divide height by 510 (2*255) to get height
248                             painter.drawLine(i, int(y - level), i, int(y + level));
249                         }
250                     }
251                     streamCount++;
252                 }
253             }
254             thumb = img;
255             // Cache thumbnail
256             ThumbnailCache::get()->storeThumbnail(m_binId, 0, thumb, true);
257         }
258         if (!thumb.isNull()) {
259             setThumbnail(thumb, -1, -1);
260         }
261     }
262     if (!KdenliveSettings::audiothumbnails()) {
263         return;
264     }
265     m_audioThumbCreated = true;
266     updateTimelineClips({TimelineModel::ReloadThumbRole});
267 }
268 
audioThumbCreated() const269 bool ProjectClip::audioThumbCreated() const
270 {
271     return (m_audioThumbCreated);
272 }
273 
clipType() const274 ClipType::ProducerType ProjectClip::clipType() const
275 {
276     return m_clipType;
277 }
278 
hasParent(const QString & id) const279 bool ProjectClip::hasParent(const QString &id) const
280 {
281     std::shared_ptr<AbstractProjectItem> par = parent();
282     while (par) {
283         if (par->clipId() == id) {
284             return true;
285         }
286         par = par->parent();
287     }
288     return false;
289 }
290 
clip(const QString & id)291 std::shared_ptr<ProjectClip> ProjectClip::clip(const QString &id)
292 {
293     if (id == m_binId) {
294         return std::static_pointer_cast<ProjectClip>(shared_from_this());
295     }
296     return std::shared_ptr<ProjectClip>();
297 }
298 
folder(const QString & id)299 std::shared_ptr<ProjectFolder> ProjectClip::folder(const QString &id)
300 {
301     Q_UNUSED(id)
302     return std::shared_ptr<ProjectFolder>();
303 }
304 
getSubClip(int in,int out)305 std::shared_ptr<ProjectSubClip> ProjectClip::getSubClip(int in, int out)
306 {
307     for (int i = 0; i < childCount(); ++i) {
308         std::shared_ptr<ProjectSubClip> clip = std::static_pointer_cast<ProjectSubClip>(child(i))->subClip(in, out);
309         if (clip) {
310             return clip;
311         }
312     }
313     return std::shared_ptr<ProjectSubClip>();
314 }
315 
subClipIds() const316 QStringList ProjectClip::subClipIds() const
317 {
318     QStringList subIds;
319     for (int i = 0; i < childCount(); ++i) {
320         std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
321         if (clip) {
322             subIds << clip->clipId();
323         }
324     }
325     return subIds;
326 }
327 
clipAt(int ix)328 std::shared_ptr<ProjectClip> ProjectClip::clipAt(int ix)
329 {
330     if (ix == row()) {
331         return std::static_pointer_cast<ProjectClip>(shared_from_this());
332     }
333     return std::shared_ptr<ProjectClip>();
334 }
335 
336 /*bool ProjectClip::isValid() const
337 {
338     return m_controller->isValid();
339 }*/
340 
hasUrl() const341 bool ProjectClip::hasUrl() const
342 {
343     if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) {
344         return (!clipUrl().isEmpty());
345     }
346     return false;
347 }
348 
url() const349 const QString ProjectClip::url() const
350 {
351     return clipUrl();
352 }
353 
frameSize() const354 const QSize ProjectClip::frameSize() const
355 {
356     return getFrameSize();
357 }
358 
duration() const359 GenTime ProjectClip::duration() const
360 {
361     return getPlaytime();
362 }
363 
frameDuration() const364 size_t ProjectClip::frameDuration() const
365 {
366     return size_t(getFramePlaytime());
367 }
368 
reloadProducer(bool refreshOnly,bool isProxy,bool forceAudioReload)369 void ProjectClip::reloadProducer(bool refreshOnly, bool isProxy, bool forceAudioReload)
370 {
371     // we find if there are some loading job on that clip
372     QMutexLocker lock(&m_thumbMutex);
373     if (refreshOnly) {
374         // In that case, we only want a new thumbnail.
375         // We thus set up a thumb job. We must make sure that there is no pending LOADJOB
376         // Clear cache first
377         ThumbnailCache::get()->invalidateThumbsForClip(clipId());
378         pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::LOADJOB, true);
379         pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::CACHEJOB);
380         m_thumbsProducer.reset();
381         ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, QDomElement(), true, -1, -1, this);
382     } else {
383         // If another load job is running?
384         pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::LOADJOB, true);
385         pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::CACHEJOB);
386         if (QFile::exists(m_path) && (!isProxy && !hasProxy()) && m_properties) {
387             clearBackupProperties();
388         }
389         QDomDocument doc;
390         QDomElement xml;
391         QString resource(m_properties->get("resource"));
392         if (m_service.isEmpty() && !resource.isEmpty()) {
393             xml = ClipCreator::getXmlFromUrl(resource).documentElement();
394         } else {
395             xml = toXml(doc);
396         }
397         if (!xml.isNull()) {
398             bool hashChanged = false;
399             m_thumbsProducer.reset();
400             ClipType::ProducerType type = clipType();
401             if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) {
402                 xml.removeAttribute("out");
403             }
404             if (type == ClipType::Audio || type == ClipType::AV) {
405                 // Check if source file was changed and rebuild audio data if necessary
406                 QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
407                 if (!clipHash.isEmpty()) {
408                     if (clipHash != getFileHash()) {
409                         // Source clip has changed, rebuild data
410                         hashChanged = true;
411                     }
412                 }
413             }
414             m_audioThumbCreated = false;
415             ThumbnailCache::get()->invalidateThumbsForClip(clipId());
416             if (forceAudioReload || (!isProxy && hashChanged)) {
417                 discardAudioThumb();
418             }
419             ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, xml, false, -1, -1, this);
420         }
421     }
422 }
423 
toXml(QDomDocument & document,bool includeMeta,bool includeProfile)424 QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile)
425 {
426     getProducerXML(document, includeMeta, includeProfile);
427     QDomElement prod;
428     if (document.documentElement().tagName() == QLatin1String("producer")) {
429         prod = document.documentElement();
430     } else {
431         prod = document.documentElement().firstChildElement(QStringLiteral("producer"));
432     }
433     if (m_clipType != ClipType::Unknown) {
434         prod.setAttribute(QStringLiteral("type"), int(m_clipType));
435     }
436     return prod;
437 }
438 
setThumbnail(const QImage & img,int in,int out)439 void ProjectClip::setThumbnail(const QImage &img, int in, int out)
440 {
441     if (img.isNull()) {
442         return;
443     }
444     if (in > -1) {
445         std::shared_ptr<ProjectSubClip> sub = getSubClip(in, out);
446         if (sub) {
447             sub->setThumbnail(img);
448         }
449         return;
450     }
451     QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
452     if (hasProxy() && !thumb.isNull()) {
453         // Overlay proxy icon
454         QPainter p(&thumb);
455         QColor c(220, 220, 10, 200);
456         QRect r(0, 0, int(thumb.height() / 2.5), int(thumb.height() / 2.5));
457         p.fillRect(r, c);
458         QFont font = p.font();
459         font.setPixelSize(r.height());
460         font.setBold(true);
461         p.setFont(font);
462         p.setPen(Qt::black);
463         p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P"));
464     }
465     m_thumbnail = QIcon(thumb);
466     if (auto ptr = m_model.lock()) {
467         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
468                                                                        AbstractProjectItem::DataThumbnail);
469     }
470 }
471 
hasAudioAndVideo() const472 bool ProjectClip::hasAudioAndVideo() const
473 {
474     return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0;
475 }
476 
isCompatible(PlaylistState::ClipState state) const477 bool ProjectClip::isCompatible(PlaylistState::ClipState state) const
478 {
479     switch (state) {
480     case PlaylistState::AudioOnly:
481         return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0);
482     case PlaylistState::VideoOnly:
483         return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0);
484     default:
485         return true;
486     }
487 }
488 
thumbnail(int width,int height)489 QPixmap ProjectClip::thumbnail(int width, int height)
490 {
491     return m_thumbnail.pixmap(width, height);
492 }
493 
setProducer(std::shared_ptr<Mlt::Producer> producer)494 bool ProjectClip::setProducer(std::shared_ptr<Mlt::Producer> producer)
495 {
496     qDebug() << "################### ProjectClip::setproducer";
497     QMutexLocker locker(&m_producerMutex);
498     FileStatus::ClipStatus currentStatus = m_clipStatus;
499     updateProducer(producer);
500     emit producerChanged(m_binId, producer);
501     if (producer->get_int("kdenlive:transcodingrequired") == 1) {
502         pCore->bin()->requestTranscoding(clipUrl(), clipId());
503         producer->set("kdenlive:transcodingrequired", nullptr);
504     }
505     m_thumbsProducer.reset();
506     connectEffectStack();
507 
508     // Update info
509     if (m_name.isEmpty()) {
510         m_name = clipName();
511     }
512     m_date = date;
513     m_description = ClipController::description();
514     m_temporaryUrl.clear();
515     if (m_clipType == ClipType::Audio) {
516         m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
517     } else if (m_clipType == ClipType::Image) {
518         if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) {
519             KMessageBox::information(QApplication::activeWindow(),
520                                      i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework."));
521         }
522     }
523     m_duration = getStringDuration();
524     m_clipStatus = m_usesProxy ? FileStatus::StatusProxy : FileStatus::StatusReady;
525     if (m_clipStatus != currentStatus) {
526         updateTimelineClips({TimelineModel::StatusRole});
527     }
528     setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
529     AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
530     if (auto ptr = m_model.lock()) {
531         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
532                                                                        AbstractProjectItem::DataDuration);
533         std::static_pointer_cast<ProjectItemModel>(ptr)->updateWatcher(std::static_pointer_cast<ProjectClip>(shared_from_this()));
534     }
535     // Make sure we have a hash for this clip
536     getFileHash();
537     // set parent again (some info need to be stored in producer)
538     updateParent(parentItem().lock());
539     if (KdenliveSettings::audiothumbnails() && (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Playlist || m_clipType == ClipType::Unknown)) {
540         AudioLevelsTask::start({ObjectType::BinClip, m_binId.toInt()}, this, false);
541     }
542     pCore->bin()->reloadMonitorIfActive(clipId());
543     for (auto &p : m_audioProducers) {
544         m_effectStack->removeService(p.second);
545     }
546     for (auto &p : m_videoProducers) {
547         m_effectStack->removeService(p.second);
548     }
549     for (auto &p : m_timewarpProducers) {
550         m_effectStack->removeService(p.second);
551     }
552     // Release audio producers
553     m_audioProducers.clear();
554     m_videoProducers.clear();
555     m_timewarpProducers.clear();
556     emit refreshPropertiesPanel();
557     if (m_hasLimitedDuration) {
558         connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
559     } else {
560         disconnect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
561     }
562     replaceInTimeline();
563     updateTimelineClips({TimelineModel::IsProxyRole});
564     bool generateProxy = false;
565     QList<std::shared_ptr<ProjectClip>> clipList;
566     if (pCore->currentDoc()->useProxy() && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1) {
567         // automatic proxy generation enabled
568         if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) {
569             if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() &&
570                 getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
571                 clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
572             }
573         } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 &&
574                    (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
575             bool skipProducer = false;
576             if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) {
577                 QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';'));
578                 // We have a camcorder profile, check if we have opened a proxy clip
579                 if (externalParams.count() >= 6) {
580                     QFileInfo info(m_path);
581                     QDir dir = info.absoluteDir();
582                     dir.cd(externalParams.at(3));
583                     QString fileName = info.fileName();
584                     if (fileName.startsWith(externalParams.at(1))) {
585                         fileName.remove(0, externalParams.at(1).size());
586                         fileName.prepend(externalParams.at(4));
587                     }
588                     if (!externalParams.at(2).isEmpty()) {
589                         fileName.chop(externalParams.at(2).size());
590                     }
591                     fileName.append(externalParams.at(5));
592                     if (dir.exists(fileName)) {
593                         setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path);
594                         m_path = dir.absoluteFilePath(fileName);
595                         setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path);
596                         getFileHash();
597                         skipProducer = true;
598                     }
599                 }
600             }
601             if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) {
602                 clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
603             }
604         } else if (m_clipType == ClipType::Playlist && pCore->getCurrentFrameDisplaySize().width() >= KdenliveSettings::proxyminsize() &&
605                 getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
606             clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
607         }
608         if (!clipList.isEmpty()) {
609             generateProxy = true;
610         }
611     }
612     if (!generateProxy && KdenliveSettings::hoverPreview() && (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist)) {
613         QTimer::singleShot(1000, this, [this]() {
614             CacheTask::start({ObjectType::BinClip,m_binId.toInt()}, 30, 0, 0, this);
615         });
616     }
617     if (generateProxy) {
618         QMetaObject::invokeMethod(pCore->currentDoc(), "slotProxyCurrentItem", Q_ARG(bool,true), Q_ARG(QList<std::shared_ptr<ProjectClip> >,clipList), Q_ARG(bool,false));
619     }
620     return true;
621 }
622 
setThumbProducer(std::shared_ptr<Mlt::Producer> prod)623 void ProjectClip::setThumbProducer(std::shared_ptr<Mlt::Producer>prod)
624 {
625     m_thumbsProducer = std::move(prod);
626 }
627 
thumbProducer()628 std::shared_ptr<Mlt::Producer> ProjectClip::thumbProducer()
629 {
630     if (m_thumbsProducer) {
631         return m_thumbsProducer;
632     }
633     if (clipType() == ClipType::Unknown || m_masterProducer == nullptr) {
634         return nullptr;
635     }
636     QMutexLocker lock(&m_thumbMutex);
637     if (KdenliveSettings::gpu_accel()) {
638         // TODO: when the original producer changes, we must reload this thumb producer
639         m_thumbsProducer = softClone(ClipController::getPassPropertiesList());
640     } else {
641         QString mltService = m_masterProducer->get("mlt_service");
642         const QString mltResource = m_masterProducer->get("resource");
643         if (mltService == QLatin1String("avformat")) {
644             mltService = QStringLiteral("avformat-novalidate");
645         }
646         Mlt::Profile *profile = pCore->thumbProfile();
647         if (mltService.startsWith(QLatin1String("xml"))) {
648             // Xml producers can corrupt the profile, so enforce width/height again after loading
649             int profileWidth = profile->width();
650             int profileHeight= profile->height();
651             m_thumbsProducer.reset(new Mlt::Producer(*profile, "consumer", mltResource.toUtf8().constData()));
652             profile->set_width(profileWidth);
653             profile->set_height(profileHeight);
654         } else {
655             m_thumbsProducer.reset(new Mlt::Producer(*profile, mltService.toUtf8().constData(), mltResource.toUtf8().constData()));
656         }
657         if (m_thumbsProducer->is_valid()) {
658             Mlt::Properties original(m_masterProducer->get_properties());
659             Mlt::Properties cloneProps(m_thumbsProducer->get_properties());
660             cloneProps.pass_list(original, ClipController::getPassPropertiesList());
661             Mlt::Filter scaler(*pCore->thumbProfile(), "swscale");
662             Mlt::Filter padder(*pCore->thumbProfile(), "resize");
663             Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space");
664             m_thumbsProducer->set("audio_index", -1);
665             // Required to make get_playtime() return > 1
666             m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1);
667             m_thumbsProducer->attach(scaler);
668             m_thumbsProducer->attach(padder);
669             m_thumbsProducer->attach(converter);
670         }
671     }
672     return m_thumbsProducer;
673 }
674 
createDisabledMasterProducer()675 void ProjectClip::createDisabledMasterProducer()
676 {
677     if (!m_disabledProducer) {
678         m_disabledProducer = cloneProducer();
679         m_disabledProducer->set("set.test_audio", 1);
680         m_disabledProducer->set("set.test_image", 1);
681         m_effectStack->addService(m_disabledProducer);
682     }
683 }
684 
getRecordTime()685 int ProjectClip::getRecordTime()
686 {
687     if (m_masterProducer && (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio)) {
688         int recTime = m_masterProducer->get_int("kdenlive:record_date");
689         if (recTime > 0) {
690             return recTime;
691         }
692         if (recTime < 0) {
693             // Cannot read record date on this clip, abort
694             return 0;
695         }
696         // Try to get record date metadata
697         if (KdenliveSettings::mediainfopath().isEmpty()) {
698         }
699         QProcess extractInfo;
700         extractInfo.start(KdenliveSettings::mediainfopath(), {url(),QStringLiteral("--output=XML")});
701         extractInfo.waitForFinished();
702         if(extractInfo.exitStatus() != QProcess::NormalExit || extractInfo.exitCode() != 0) {
703             KMessageBox::error(QApplication::activeWindow(), i18n("Cannot extract metadata from %1\n%2", url(),
704                         QString(extractInfo.readAllStandardError())));
705             return 0;
706         }
707         QDomDocument doc;
708         doc.setContent(extractInfo.readAllStandardOutput());
709         bool dateFormat = false;
710         QDomNodeList nodes = doc.documentElement().elementsByTagName(QStringLiteral("TimeCode_FirstFrame"));
711         if (nodes.isEmpty()) {
712             nodes = doc.documentElement().elementsByTagName(QStringLiteral("Recorded_Date"));
713             dateFormat = true;
714         }
715         if (!nodes.isEmpty()) {
716             // Parse recorded time (HH:MM:SS)
717             QString recInfo = nodes.at(0).toElement().text();
718             if (!recInfo.isEmpty()) {
719                 if (dateFormat) {
720                     if (recInfo.contains(QLatin1Char('+'))) {
721                         recInfo = recInfo.section(QLatin1Char('+'), 0, 0);
722                     } else if (recInfo.contains(QLatin1Char('-'))) {
723                         recInfo = recInfo.section(QLatin1Char('-'), 0, 0);
724                     }
725                     QDateTime date = QDateTime::fromString(recInfo, "yyyy-MM-dd hh:mm:ss");
726                     recTime = date.time().msecsSinceStartOfDay();
727                 } else {
728                     // Timecode Format HH:MM:SS:FF
729                     // Check if we have a different fps
730                     double producerFps = m_masterProducer->get_double("meta.media.frame_rate_num") / m_masterProducer->get_double("meta.media.frame_rate_den");
731                     if (!qFuzzyCompare(producerFps, pCore->getCurrentFps())) {
732                         // Producer and project have a different fps
733                         bool ok;
734                         int frames = recInfo.section(QLatin1Char(':'), -1).toInt(&ok);
735                         if (ok) {
736                             frames *= int(pCore->getCurrentFps() / producerFps);
737                             recInfo.chop(2);
738                             recInfo.append(QString::number(frames).rightJustified(1, QChar('0')));
739                         }
740                     }
741                     recTime = int(1000 * pCore->timecode().getFrameCount(recInfo) / pCore->getCurrentFps());
742                 }
743                 m_masterProducer->set("kdenlive:record_date", recTime);
744                 return recTime;
745             }
746         } else {
747             m_masterProducer->set("kdenlive:record_date", -1);
748             return 0;
749         }
750     }
751     return 0;
752 }
753 
getTimelineProducer(int trackId,int clipId,PlaylistState::ClipState state,int audioStream,double speed,bool secondPlaylist,bool timeremap)754 std::shared_ptr<Mlt::Producer> ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, int audioStream, double speed, bool secondPlaylist, bool timeremap)
755 {
756     if (!m_masterProducer) {
757         return nullptr;
758     }
759     if (qFuzzyCompare(speed, 1.0) && !timeremap) {
760         // we are requesting a normal speed producer
761         bool byPassTrackProducer = false;
762         if (trackId == -1 && (state != PlaylistState::AudioOnly || audioStream == m_masterProducer->get_int("audio_index"))) {
763             byPassTrackProducer = true;
764         }
765         if (byPassTrackProducer ||
766             (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text|| m_clipType == ClipType::TextTemplate || m_clipType == ClipType::Qml))) {
767             // Temporary copy, return clone of master
768             int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
769             return std::shared_ptr<Mlt::Producer>(m_masterProducer->cut(-1, duration > 0 ? duration - 1 : -1));
770         }
771         if (m_timewarpProducers.count(clipId) > 0) {
772             m_effectStack->removeService(m_timewarpProducers[clipId]);
773             m_timewarpProducers.erase(clipId);
774         }
775         if (state == PlaylistState::AudioOnly) {
776             // We need to get an audio producer, if none exists
777             if (audioStream > -1) {
778                 if (trackId >= 0) {
779                     trackId += 100 * audioStream;
780                 } else {
781                     trackId -= 100 * audioStream;
782                 }
783             }
784             // second playlist producers use negative trackId
785             if (secondPlaylist) {
786                 trackId = -trackId;
787             }
788             if (m_audioProducers.count(trackId) == 0) {
789                 m_audioProducers[trackId] = cloneProducer(true);
790                 m_audioProducers[trackId]->set("set.test_audio", 0);
791                 m_audioProducers[trackId]->set("set.test_image", 1);
792                 if (m_streamEffects.contains(audioStream)) {
793                     QStringList effects = m_streamEffects.value(audioStream);
794                     for (const QString &effect : qAsConst(effects)) {
795                         Mlt::Filter filt(*m_audioProducers[trackId]->profile(), effect.toUtf8().constData());
796                         if (filt.is_valid()) {
797                             // Add stream effect markup
798                             filt.set("kdenlive:stream", 1);
799                             m_audioProducers[trackId]->attach(filt);
800                         }
801                     }
802                 }
803                 if (audioStream > -1) {
804                     m_audioProducers[trackId]->set("audio_index", audioStream);
805                 }
806                 m_effectStack->addService(m_audioProducers[trackId]);
807             }
808             return std::shared_ptr<Mlt::Producer>(m_audioProducers[trackId]->cut());
809         }
810         if (m_audioProducers.count(trackId) > 0) {
811             m_effectStack->removeService(m_audioProducers[trackId]);
812             m_audioProducers.erase(trackId);
813         }
814         if (state == PlaylistState::VideoOnly) {
815             // we return the video producer
816             // We need to get an video producer, if none exists
817             // second playlist producers use negative trackId
818             if (secondPlaylist) {
819                 trackId = -trackId;
820             }
821             if (m_videoProducers.count(trackId) == 0) {
822                 m_videoProducers[trackId] = cloneProducer(true);
823                 // Let audio enabled so that we can use audio visualization filters ?
824                 m_videoProducers[trackId]->set("set.test_audio", 1);
825                 m_videoProducers[trackId]->set("set.test_image", 0);
826                 m_effectStack->addService(m_videoProducers[trackId]);
827             }
828             int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
829             return std::shared_ptr<Mlt::Producer>(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration - 1: -1));
830         }
831         if (m_videoProducers.count(trackId) > 0) {
832             m_effectStack->removeService(m_videoProducers[trackId]);
833             m_videoProducers.erase(trackId);
834         }
835         Q_ASSERT(state == PlaylistState::Disabled);
836         createDisabledMasterProducer();
837         int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
838         return std::shared_ptr<Mlt::Producer>(m_disabledProducer->cut(-1, duration > 0 ? duration - 1: -1));
839     }
840 
841     // For timewarp clips, we keep one separate producer for each clip.
842     std::shared_ptr<Mlt::Producer> warpProducer;
843     if (m_timewarpProducers.count(clipId) > 0) {
844         // remove in all cases, we add it unconditionally anyways
845         m_effectStack->removeService(m_timewarpProducers[clipId]);
846         if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) {
847             // the producer we have is good, use it !
848             warpProducer = m_timewarpProducers[clipId];
849             qDebug() << "Reusing timewarp producer!";
850         } else if (timeremap && qFuzzyIsNull(m_timewarpProducers[clipId]->get_double("warp_speed"))) {
851             // the producer we have is good, use it !
852             qDebug() << "Reusing time remap producer!";
853             warpProducer = m_timewarpProducers[clipId];
854         } else {
855             m_timewarpProducers.erase(clipId);
856         }
857     }
858     if (!warpProducer) {
859         QString resource(originalProducer()->get("resource"));
860         if (resource.isEmpty() || resource == QLatin1String("<producer>")) {
861             resource = m_service;
862         }
863         if (timeremap) {
864             Mlt::Chain *chain = new Mlt::Chain(*originalProducer()->profile(), resource.toUtf8().constData());
865             Mlt::Link link("timeremap");
866             chain->attach(link);
867             warpProducer.reset(chain);
868         } else {
869             QString url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed)), resource);
870             warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData()));
871             int original_length = originalProducer()->get_length();
872             warpProducer->set("length", int(original_length / std::abs(speed) + 0.5));
873             qDebug() << "new producer: " << url;
874             qDebug() << "warp LENGTH before" << warpProducer->get_length();
875         }
876         // this is a workaround to cope with Mlt erroneous rounding
877         Mlt::Properties original(m_masterProducer->get_properties());
878         Mlt::Properties cloneProps(warpProducer->get_properties());
879         cloneProps.pass_list(original, ClipController::getPassPropertiesList(false));
880         warpProducer->set("audio_index", audioStream);
881     }
882 
883     //if the producer has a "time-to-live" (frame duration) we need to scale it according to the speed
884     int ttl = originalProducer()->get_int("ttl");
885     if(ttl > 0) {
886         int new_ttl = ttl / std::abs(speed) + 0.5;
887         warpProducer->set("ttl", std::max(new_ttl, 1));
888     }
889 
890     qDebug() << "warp LENGTH" << warpProducer->get_length();
891     warpProducer->set("set.test_audio", 1);
892     warpProducer->set("set.test_image", 1);
893     warpProducer->set("kdenlive:id", binId().toUtf8().constData());
894     if (state == PlaylistState::AudioOnly) {
895         warpProducer->set("set.test_audio", 0);
896     }
897     if (state == PlaylistState::VideoOnly) {
898         warpProducer->set("set.test_image", 0);
899     }
900     m_timewarpProducers[clipId] = warpProducer;
901     m_effectStack->addService(m_timewarpProducers[clipId]);
902     return std::shared_ptr<Mlt::Producer>(warpProducer->cut());
903 }
904 
giveMasterAndGetTimelineProducer(int clipId,std::shared_ptr<Mlt::Producer> master,PlaylistState::ClipState state,int tid,bool secondPlaylist)905 std::pair<std::shared_ptr<Mlt::Producer>, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr<Mlt::Producer> master, PlaylistState::ClipState state, int tid, bool secondPlaylist)
906 {
907     int in = master->get_in();
908     int out = master->get_out();
909     if (master->parent().is_valid()) {
910         // in that case, we have a cut
911         // check whether it's a timewarp
912         double speed = 1.0;
913         bool timeWarp = false;
914         if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) {
915             speed = master->parent().get_double("warp_speed");
916             timeWarp = true;
917         } else if (master->parent().type() == mlt_service_chain_type) {
918             timeWarp = true;
919         }
920         if (master->parent().get_int("_loaded") == 1) {
921             // we already have a clip that shares the same master
922             if (state != PlaylistState::Disabled || timeWarp) {
923                 // In that case, we must create copies
924                 std::shared_ptr<Mlt::Producer> prod(getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed)->cut(in, out));
925                 return {prod, false};
926             }
927             if (state == PlaylistState::Disabled) {
928                 if (!m_disabledProducer) {
929                     qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet";
930                     createDisabledMasterProducer();
931                 }
932                 return {std::shared_ptr<Mlt::Producer>(m_disabledProducer->cut(in, out)), false};
933             }
934             // We have a good id, this clip can be used
935             return {master, true};
936         } else {
937             master->parent().set("_loaded", 1);
938             if (timeWarp) {
939                 m_timewarpProducers[clipId] = std::make_shared<Mlt::Producer>(&master->parent());
940                 m_effectStack->loadService(m_timewarpProducers[clipId]);
941                 return {master, true};
942             }
943             if (state == PlaylistState::AudioOnly) {
944                 int audioStream = master->parent().get_int("audio_index");
945                 if (audioStream > -1) {
946                     tid += 100 * audioStream;
947                 }
948                 if (secondPlaylist) {
949                     tid = -tid;
950                 }
951                 m_audioProducers[tid] = std::make_shared<Mlt::Producer>(&master->parent());
952                 m_effectStack->loadService(m_audioProducers[tid]);
953                 return {master, true};
954             }
955             if (state == PlaylistState::VideoOnly) {
956                 // good, we found a master video producer, and we didn't have any
957                 if (m_clipType != ClipType::Color && m_clipType != ClipType::Image && m_clipType != ClipType::Text) {
958                     // Color, image and text clips always use master producer in timeline
959                     if (secondPlaylist) {
960                         tid = -tid;
961                     }
962                     m_videoProducers[tid] = std::make_shared<Mlt::Producer>(&master->parent());
963                     m_effectStack->loadService(m_videoProducers[tid]);
964                 } else {
965                     // Ensure clip out = length - 1 so that effects work correctly
966                     if (out != master->parent().get_length() - 1) {
967                         master->parent().set("out", master->parent().get_length() - 1);
968                     }
969                 }
970                 return {master, true};
971             }
972             if (state == PlaylistState::Disabled) {
973                 if (!m_disabledProducer) {
974                     createDisabledMasterProducer();
975                 }
976                 return {std::make_shared<Mlt::Producer>(m_disabledProducer->cut(master->get_in(), master->get_out())), true};
977             }
978             qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master";
979             Q_ASSERT(false);
980         }
981     } else if (master->is_valid()) {
982         // in that case, we have a master
983         qDebug() << "Warning: weird, we received a master clip in lieue of a cut";
984         double speed = 1.0;
985         if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) {
986             speed = master->get_double("warp_speed");
987         }
988         return {getTimelineProducer(-1, clipId, state, master->get_int("audio_index"), speed), false};
989     }
990     // we have a problem
991     return {std::shared_ptr<Mlt::Producer>(ClipController::mediaUnavailable->cut()), false};
992 }
993 
cloneProducer(bool removeEffects)994 std::shared_ptr<Mlt::Producer> ProjectClip::cloneProducer(bool removeEffects)
995 {
996     Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", "string");
997     Mlt::Service s(m_masterProducer->get_service());
998     int ignore = s.get_int("ignore_points");
999     if (ignore) {
1000         s.set("ignore_points", 0);
1001     }
1002     c.connect(s);
1003     c.set("time_format", "frames");
1004     c.set("no_meta", 1);
1005     c.set("no_root", 1);
1006     c.set("no_profile", 1);
1007     c.set("root", "/");
1008     c.set("store", "kdenlive");
1009     c.run();
1010     if (ignore) {
1011         s.set("ignore_points", ignore);
1012     }
1013     const QByteArray clipXml = c.get("string");
1014     qDebug()<<"============= CLONED CLIP: \n\n"<<clipXml<<"\n\n======================";
1015     std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData()));
1016     if (strcmp(prod->get("mlt_service"), "avformat") == 0) {
1017         prod->set("mlt_service", "avformat-novalidate");
1018         prod->set("mute_on_pause", 0);
1019     }
1020 
1021     // we pass some properties that wouldn't be passed because of the novalidate
1022     const char *prefix = "meta.";
1023     const size_t prefix_len = strlen(prefix);
1024     for (int i = 0; i < m_masterProducer->count(); ++i) {
1025         char *current = m_masterProducer->get_name(i);
1026         if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) {
1027             prod->set(current, m_masterProducer->get(i));
1028         }
1029     }
1030 
1031     if (removeEffects) {
1032         int ct = 0;
1033         Mlt::Filter *filter = prod->filter(ct);
1034         while (filter) {
1035             qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service");
1036             QString ix = QString::fromLatin1(filter->get("kdenlive_id"));
1037             if (!ix.isEmpty()) {
1038                 qDebug() << "/ + + DELETING";
1039                 if (prod->detach(*filter) == 0) {
1040                 } else {
1041                     ct++;
1042                 }
1043             } else {
1044                 ct++;
1045             }
1046             delete filter;
1047             filter = prod->filter(ct);
1048         }
1049     }
1050     prod->set("id", nullptr);
1051     return prod;
1052 }
1053 
cloneProducer(const std::shared_ptr<Mlt::Producer> & producer)1054 std::shared_ptr<Mlt::Producer> ProjectClip::cloneProducer(const std::shared_ptr<Mlt::Producer> &producer)
1055 {
1056     Mlt::Consumer c(*producer->profile(), "xml", "string");
1057     Mlt::Service s(producer->get_service());
1058     int ignore = s.get_int("ignore_points");
1059     if (ignore) {
1060         s.set("ignore_points", 0);
1061     }
1062     c.connect(s);
1063     c.set("time_format", "frames");
1064     c.set("no_meta", 1);
1065     c.set("no_root", 1);
1066     c.set("no_profile", 1);
1067     c.set("root", "/");
1068     c.set("store", "kdenlive");
1069     c.start();
1070     if (ignore) {
1071         s.set("ignore_points", ignore);
1072     }
1073     const QByteArray clipXml = c.get("string");
1074     std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData()));
1075     if (strcmp(prod->get("mlt_service"), "avformat") == 0) {
1076         prod->set("mlt_service", "avformat-novalidate");
1077         prod->set("mute_on_pause", 0);
1078     }
1079     return prod;
1080 }
1081 
softClone(const char * list)1082 std::shared_ptr<Mlt::Producer> ProjectClip::softClone(const char *list)
1083 {
1084     QString service = QString::fromLatin1(m_masterProducer->get("mlt_service"));
1085     QString resource = QString::fromUtf8(m_masterProducer->get("resource"));
1086     std::shared_ptr<Mlt::Producer> clone(new Mlt::Producer(*pCore->thumbProfile(), service.toUtf8().constData(), resource.toUtf8().constData()));
1087     Mlt::Filter scaler(*pCore->thumbProfile(), "swscale");
1088     Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space");
1089     clone->attach(scaler);
1090     clone->attach(converter);
1091     Mlt::Properties original(m_masterProducer->get_properties());
1092     Mlt::Properties cloneProps(clone->get_properties());
1093     cloneProps.pass_list(original, list);
1094     return clone;
1095 }
1096 
getClone()1097 std::unique_ptr<Mlt::Producer> ProjectClip::getClone()
1098 {
1099     const char *list = ClipController::getPassPropertiesList();
1100     QString service = QString::fromLatin1(m_masterProducer->get("mlt_service"));
1101     QString resource = QString::fromUtf8(m_masterProducer->get("resource"));
1102     std::unique_ptr<Mlt::Producer> clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData()));
1103     Mlt::Properties original(m_masterProducer->get_properties());
1104     Mlt::Properties cloneProps(clone->get_properties());
1105     cloneProps.pass_list(original, list);
1106     return clone;
1107 }
1108 
zone() const1109 QPoint ProjectClip::zone() const
1110 {
1111     return ClipController::zone();
1112 }
1113 
hash()1114 const QString ProjectClip::hash()
1115 {
1116     QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
1117     if (!clipHash.isEmpty()) {
1118         return clipHash;
1119     }
1120     return getFileHash();
1121 }
1122 
getFolderHash(QDir dir,QString fileName)1123 const QByteArray ProjectClip::getFolderHash(QDir dir, QString fileName)
1124 {
1125     QStringList files = dir.entryList(QDir::Files);
1126     fileName.append(files.join(QLatin1Char(',')));
1127     // Include file hash info in case we have several folders with same file names (can happen for image sequences)
1128     if (!files.isEmpty()) {
1129         QPair<QByteArray, qint64> hashData = calculateHash(dir.absoluteFilePath(files.first()));
1130         fileName.append(hashData.first);
1131         fileName.append(QString::number(hashData.second));
1132         if (files.size() > 1) {
1133             hashData = calculateHash(dir.absoluteFilePath(files.at(files.size() / 2)));
1134             fileName.append(hashData.first);
1135             fileName.append(QString::number(hashData.second));
1136         }
1137     }
1138     QByteArray fileData = fileName.toUtf8();
1139     return QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1140 }
1141 
getFileHash()1142 const QString ProjectClip::getFileHash()
1143 {
1144     QByteArray fileData;
1145     QByteArray fileHash;
1146     switch (m_clipType) {
1147     case ClipType::SlideShow:
1148         fileHash = getFolderHash(QFileInfo(clipUrl()).absoluteDir(), QFileInfo(clipUrl()).fileName());
1149         break;
1150     case ClipType::Text:
1151         fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8();
1152         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1153         break;
1154     case ClipType::TextTemplate:
1155         fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
1156         fileData.append(getProducerProperty(QStringLiteral("templatetext")).toUtf8());
1157         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1158         break;
1159     case ClipType::QText:
1160         fileData = getProducerProperty(QStringLiteral("text")).toUtf8();
1161         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1162         break;
1163     case ClipType::Color:
1164         fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
1165         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1166         break;
1167     default:
1168         QPair<QByteArray, qint64> hashData = calculateHash(clipUrl());
1169         fileHash = hashData.first;
1170         ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(hashData.second));
1171         break;
1172     }
1173     if (fileHash.isEmpty()) {
1174         qDebug() << "// WARNING EMPTY CLIP HASH: ";
1175         return QString();
1176     }
1177     QString result = fileHash.toHex();
1178     ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result);
1179     return result;
1180 }
1181 
1182 
calculateHash(const QString path)1183 const QPair<QByteArray, qint64> ProjectClip::calculateHash(const QString path)
1184 {
1185     QFile file(path);
1186     QByteArray fileHash;
1187     qint64 fSize = 0;
1188     if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file
1189         /*
1190         * 1 MB = 1 second per 450 files (or faster)
1191         * 10 MB = 9 seconds per 450 files (or faster)
1192         */
1193         QByteArray fileData;
1194         fSize = file.size();
1195         if (fSize > 2000000) {
1196             fileData = file.read(1000000);
1197             if (file.seek(file.size() - 1000000)) {
1198                 fileData.append(file.readAll());
1199             }
1200         } else {
1201             fileData = file.readAll();
1202         }
1203         file.close();
1204         fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1205     }
1206     return {fileHash, fSize};
1207 }
1208 
getOriginalFps() const1209 double ProjectClip::getOriginalFps() const
1210 {
1211     return originalFps();
1212 }
1213 
hasProxy() const1214 bool ProjectClip::hasProxy() const
1215 {
1216     QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy"));
1217     return proxy.size() > 2;
1218 }
1219 
setProperties(const QMap<QString,QString> & properties,bool refreshPanel)1220 void ProjectClip::setProperties(const QMap<QString, QString> &properties, bool refreshPanel)
1221 {
1222     qDebug() << "// SETTING CLIP PROPERTIES: " << properties;
1223     QMapIterator<QString, QString> i(properties);
1224     QMap<QString, QString> passProperties;
1225     bool refreshAnalysis = false;
1226     bool reload = false;
1227     bool refreshOnly = true;
1228     if (properties.contains(QStringLiteral("templatetext"))) {
1229         m_description = properties.value(QStringLiteral("templatetext"));
1230         if (auto ptr = m_model.lock())
1231             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1232                                                                            AbstractProjectItem::ClipStatus);
1233         refreshPanel = true;
1234     }
1235     // Some properties also need to be passed to track producers
1236     QStringList timelineProperties{
1237         QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"),         QStringLiteral("threads"),
1238         QStringLiteral("force_colorspace"), QStringLiteral("force_tff"),           QStringLiteral("force_progressive"), QStringLiteral("video_delay")
1239     };
1240     QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"),   QStringLiteral("resource"), QStringLiteral("force_fps"),   QStringLiteral("set.test_image"), QStringLiteral("video_index"), QStringLiteral("disable_exif")};
1241     QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"),     QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")};
1242     QVector<int> updateRoles;
1243     while (i.hasNext()) {
1244         i.next();
1245         setProducerProperty(i.key(), i.value());
1246         if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) {
1247             reload = true;
1248             refreshOnly = false;
1249         }
1250         if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) {
1251             refreshAnalysis = true;
1252         }
1253         if (timelineProperties.contains(i.key())) {
1254             passProperties.insert(i.key(), i.value());
1255         }
1256     }
1257     if (properties.contains(QStringLiteral("resource"))) {
1258         // Clip source was changed, update important stuff
1259         refreshPanel = true;
1260         reload = true;
1261         resetProducerProperty(QStringLiteral("kdenlive:file_hash"));
1262         if (m_clipType == ClipType::Color) {
1263             refreshOnly = true;
1264             updateRoles << TimelineModel::ResourceRole;
1265         } else if (properties.contains("_fullreload")) {
1266             // Clip resource changed, update thumbnail, name, clear hash
1267             refreshOnly = false;
1268             // Enforce reloading clip type in case of clip replacement
1269             m_service.clear();
1270             m_clipType = ClipType::Unknown;
1271             clearBackupProperties();
1272             updateRoles << TimelineModel::ResourceRole << TimelineModel::MaxDurationRole << TimelineModel::NameRole;
1273         }
1274     }
1275     if (properties.contains(QStringLiteral("kdenlive:proxy")) && !properties.contains("_fullreload")) {
1276         QString value = properties.value(QStringLiteral("kdenlive:proxy"));
1277         // If value is "-", that means user manually disabled proxy on this clip
1278         if (value.isEmpty() || value == QLatin1String("-")) {
1279             // reset proxy
1280             if (pCore->taskManager.hasPendingJob({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::PROXYJOB)) {
1281                 // The proxy clip is being created, abort
1282                 pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::PROXYJOB);
1283             } else {
1284                 reload = true;
1285                 refreshOnly = false;
1286                 // Restore original url
1287                 QString resource = getProducerProperty(QStringLiteral("kdenlive:originalurl"));
1288                 if (!resource.isEmpty()) {
1289                     setProducerProperty(QStringLiteral("resource"), resource);
1290                 }
1291             }
1292         } else {
1293             // A proxy was requested, make sure to keep original url
1294             setProducerProperty(QStringLiteral("kdenlive:originalurl"), url());
1295             backupOriginalProperties();
1296             ProxyTask::start({ObjectType::BinClip,m_binId.toInt()}, this);
1297         }
1298     } else if (!reload) {
1299         const QList<QString> propKeys = properties.keys();
1300         for (const QString &k : propKeys) {
1301             if (forceReloadProperties.contains(k)) {
1302                 refreshPanel = true;
1303                 refreshOnly = false;
1304                 reload = true;
1305                 break;
1306             }
1307         }
1308     }
1309     if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) {
1310         reload = true;
1311     }
1312     if (refreshAnalysis) {
1313         emit refreshAnalysisPanel();
1314     }
1315     if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) {
1316         // Make sure length is >= kdenlive:duration
1317         int producerLength = getProducerIntProperty(QStringLiteral("length"));
1318         int kdenliveLength = getFramePlaytime();
1319         if (producerLength < kdenliveLength) {
1320             setProducerProperty(QStringLiteral("length"), kdenliveLength);
1321         }
1322         m_duration = getStringDuration();
1323         if (auto ptr = m_model.lock())
1324             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1325                                                                            AbstractProjectItem::DataDuration);
1326         refreshOnly = false;
1327         reload = true;
1328     }
1329     QVector <int> refreshRoles;
1330     if (properties.contains(QStringLiteral("kdenlive:tags"))) {
1331         setTags(properties.value(QStringLiteral("kdenlive:tags")));
1332         if (auto ptr = m_model.lock()) {
1333             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1334                                                                            AbstractProjectItem::DataTag);
1335         }
1336         refreshRoles << TimelineModel::TagRole;
1337     }
1338     if (properties.contains(QStringLiteral("kdenlive:clipname"))) {
1339         m_name = properties.value(QStringLiteral("kdenlive:clipname"));
1340         refreshPanel = true;
1341         if (auto ptr = m_model.lock()) {
1342             std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1343                                                                            AbstractProjectItem::DataName);
1344         }
1345         refreshRoles << TimelineModel::NameRole;
1346     }
1347     // update timeline clips
1348     if (!reload) {
1349         updateTimelineClips(refreshRoles);
1350     }
1351     bool audioStreamChanged = properties.contains(QStringLiteral("audio_index"));
1352     if (reload) {
1353         // producer has changed, refresh monitor and thumbnail
1354         if (hasProxy()) {
1355             pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::PROXYJOB);
1356             setProducerProperty(QStringLiteral("_overwriteproxy"), 1);
1357             ProxyTask::start({ObjectType::BinClip,m_binId.toInt()}, this);
1358         } else {
1359             reloadProducer(refreshOnly, properties.contains(QStringLiteral("kdenlive:proxy")));
1360         }
1361         if (refreshOnly) {
1362             if (auto ptr = m_model.lock()) {
1363                 emit std::static_pointer_cast<ProjectItemModel>(ptr)->refreshClip(m_binId);
1364             }
1365         }
1366         if (!updateRoles.isEmpty()) {
1367             updateTimelineClips(updateRoles);
1368         }
1369     } else {
1370         if (properties.contains(QStringLiteral("kdenlive:active_streams")) && m_audioInfo) {
1371             // Clip is a multi audio stream and currently in clip monitor, update target tracks
1372             m_audioInfo->updateActiveStreams(properties.value(QStringLiteral("kdenlive:active_streams")));
1373             pCore->bin()->updateTargets(clipId());
1374             if (!audioStreamChanged) {
1375                 pCore->bin()->reloadMonitorStreamIfActive(clipId());
1376                 pCore->bin()->checkProjectAudioTracks(clipId(), m_audioInfo->activeStreams().count());
1377                 refreshPanel = true;
1378             }
1379         }
1380         if (audioStreamChanged) {
1381             refreshAudioInfo();
1382             emit audioThumbReady();
1383             pCore->bin()->reloadMonitorStreamIfActive(clipId());
1384             refreshPanel = true;
1385         }
1386     }
1387     if (refreshPanel && m_properties) {
1388         // Some of the clip properties have changed through a command, update properties panel
1389         emit refreshPropertiesPanel();
1390     }
1391     if (!passProperties.isEmpty() && (!reload || refreshOnly)) {
1392         for (auto &p : m_audioProducers) {
1393             QMapIterator<QString, QString> pr(passProperties);
1394             while (pr.hasNext()) {
1395                 pr.next();
1396                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1397             }
1398         }
1399         for (auto &p : m_videoProducers) {
1400             QMapIterator<QString, QString> pr(passProperties);
1401             while (pr.hasNext()) {
1402                 pr.next();
1403                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1404             }
1405         }
1406         for (auto &p : m_timewarpProducers) {
1407             QMapIterator<QString, QString> pr(passProperties);
1408             while (pr.hasNext()) {
1409                 pr.next();
1410                 p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData());
1411             }
1412         }
1413     }
1414 }
1415 
buildProperties(QWidget * parent)1416 ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent)
1417 {
1418     auto ptr = m_model.lock();
1419     Q_ASSERT(ptr);
1420     auto *panel = new ClipPropertiesController(static_cast<ClipController *>(this), parent);
1421     connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties);
1422     connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData);
1423     connect(this, &ProjectClip::updateStreamInfo, panel, &ClipPropertiesController::updateStreamInfo);
1424     connect(panel, &ClipPropertiesController::requestProxy, this, [this](bool doProxy) {
1425         QList<std::shared_ptr<ProjectClip>> clipList{std::static_pointer_cast<ProjectClip>(shared_from_this())};
1426         pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList);
1427     });
1428     connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy);
1429     return panel;
1430 }
1431 
deleteProxy()1432 void ProjectClip::deleteProxy()
1433 {
1434     // Disable proxy file
1435     QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy"));
1436     QList<std::shared_ptr<ProjectClip>> clipList{std::static_pointer_cast<ProjectClip>(shared_from_this())};
1437     pCore->currentDoc()->slotProxyCurrentItem(false, clipList);
1438     // Delete
1439     bool ok;
1440     QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok);
1441     if (ok && proxy.length() > 2) {
1442         proxy = QFileInfo(proxy).fileName();
1443         if (dir.exists(proxy)) {
1444             dir.remove(proxy);
1445         }
1446     }
1447 }
1448 
updateParent(std::shared_ptr<TreeItem> parent)1449 void ProjectClip::updateParent(std::shared_ptr<TreeItem> parent)
1450 {
1451     if (parent) {
1452         auto item = std::static_pointer_cast<AbstractProjectItem>(parent);
1453         ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId());
1454     }
1455     AbstractProjectItem::updateParent(parent);
1456 }
1457 
matches(const QString & condition)1458 bool ProjectClip::matches(const QString &condition)
1459 {
1460     // TODO
1461     Q_UNUSED(condition)
1462     return true;
1463 }
1464 
rename(const QString & name,int column)1465 bool ProjectClip::rename(const QString &name, int column)
1466 {
1467     QMap<QString, QString> newProperties;
1468     QMap<QString, QString> oldProperties;
1469     bool edited = false;
1470     switch (column) {
1471     case 0:
1472         if (m_name == name) {
1473             return false;
1474         }
1475         // Rename clip
1476         oldProperties.insert(QStringLiteral("kdenlive:clipname"), m_name);
1477         newProperties.insert(QStringLiteral("kdenlive:clipname"), name);
1478         m_name = name;
1479         edited = true;
1480         break;
1481     case 2:
1482         if (m_description == name) {
1483             return false;
1484         }
1485         // Rename clip
1486         if (m_clipType == ClipType::TextTemplate) {
1487             oldProperties.insert(QStringLiteral("templatetext"), m_description);
1488             newProperties.insert(QStringLiteral("templatetext"), name);
1489         } else {
1490             oldProperties.insert(QStringLiteral("kdenlive:description"), m_description);
1491             newProperties.insert(QStringLiteral("kdenlive:description"), name);
1492         }
1493         m_description = name;
1494         edited = true;
1495         break;
1496     }
1497     if (edited) {
1498         pCore->bin()->slotEditClipCommand(m_binId, oldProperties, newProperties);
1499     }
1500     return edited;
1501 }
1502 
getData(DataType type) const1503 QVariant ProjectClip::getData(DataType type) const
1504 {
1505     switch (type) {
1506         case AbstractProjectItem::IconOverlay:
1507             if (m_clipStatus == FileStatus::StatusMissing) {
1508                 return QVariant("window-close");
1509             }
1510             if (m_clipStatus == FileStatus::StatusWaiting) {
1511                 return QVariant("view-refresh");
1512             }
1513             return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant();
1514         default:
1515             return AbstractProjectItem::getData(type);
1516     }
1517 }
1518 
audioChannels() const1519 int ProjectClip::audioChannels() const
1520 {
1521     if (!audioInfo()) {
1522         return 0;
1523     }
1524     return audioInfo()->channels();
1525 }
1526 
discardAudioThumb()1527 void ProjectClip::discardAudioThumb()
1528 {
1529     if (!m_audioInfo) {
1530         return;
1531     }
1532     pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::AUDIOTHUMBJOB);
1533     QString audioThumbPath;
1534     QList <int> streams = m_audioInfo->streams().keys();
1535     // Delete audio thumbnail data
1536     for (int &st : streams) {
1537         audioThumbPath = getAudioThumbPath(st);
1538         if (!audioThumbPath.isEmpty()) {
1539             QFile::remove(audioThumbPath);
1540         }
1541         // Clear audio cache
1542         QString key = QString("%1:%2").arg(m_binId).arg(st);
1543         pCore->audioThumbCache.insert(key, QByteArray("-"));
1544     }
1545     // Delete thumbnail
1546     for (int &st : streams) {
1547         audioThumbPath = getAudioThumbPath(st);
1548         if (!audioThumbPath.isEmpty()) {
1549             QFile::remove(audioThumbPath);
1550         }
1551     }
1552 
1553     resetProducerProperty(QStringLiteral("kdenlive:audio_max"));
1554     m_audioThumbCreated = false;
1555     refreshAudioInfo();
1556 }
1557 
getAudioStreamFfmpegIndex(int mltStream)1558 int ProjectClip::getAudioStreamFfmpegIndex(int mltStream)
1559 {
1560     if (!m_masterProducer || !audioInfo()) {
1561         return -1;
1562     }
1563     QList<int> audioStreams = audioInfo()->streams().keys();
1564     if (audioStreams.contains(mltStream)) {
1565         return audioStreams.indexOf(mltStream);
1566     }
1567     return -1;
1568 }
1569 
getAudioThumbPath(int stream)1570 const QString ProjectClip::getAudioThumbPath(int stream)
1571 {
1572     if (audioInfo() == nullptr) {
1573         return QString();
1574     }
1575     bool ok = false;
1576     QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok);
1577     if (!ok) {
1578         return QString();
1579     }
1580     const QString clipHash = hash();
1581     if (clipHash.isEmpty()) {
1582         return QString();
1583     }
1584     QString audioPath = thumbFolder.absoluteFilePath(clipHash);
1585     audioPath.append(QLatin1Char('_') + QString::number(stream));
1586     int roundedFps = int(pCore->getCurrentFps());
1587     audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps));
1588     return audioPath;
1589 }
1590 
updatedAnalysisData(const QString & name,const QString & data,int offset)1591 QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset)
1592 {
1593     if (data.isEmpty()) {
1594         // Remove data
1595         return QStringList() << QString("kdenlive:clipanalysis." + name) << QString();
1596         // m_controller->resetProperty("kdenlive:clipanalysis." + name);
1597     }
1598     QString current = getProducerProperty("kdenlive:clipanalysis." + name);
1599     if (!current.isEmpty()) {
1600         if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")),
1601                                        KGuiItem(i18n("Add"))) == KMessageBox::Yes) {
1602             // Merge data
1603             //TODO MLT7: convert to Mlt::Animation
1604             /*auto &profile = pCore->getCurrentProfile();
1605             Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
1606             Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
1607             Mlt::GeometryItem item;
1608             int pos = 0;
1609             while (newGeometry.next_key(&item, pos) == 0) {
1610                 pos = item.frame();
1611                 item.frame(pos + offset);
1612                 pos++;
1613                 geometry.insert(item);
1614             }
1615             return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise();*/
1616             // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise());
1617         }
1618         // Add data with another name
1619         int i = 1;
1620         QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i));
1621         while (!previous.isEmpty()) {
1622             ++i;
1623             previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i));
1624         }
1625         return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset);
1626         // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset));
1627     }
1628     return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset);
1629     // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset));
1630 }
1631 
analysisData(bool withPrefix)1632 QMap<QString, QString> ProjectClip::analysisData(bool withPrefix)
1633 {
1634     return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix);
1635 }
1636 
geometryWithOffset(const QString & data,int offset)1637 const QString ProjectClip::geometryWithOffset(const QString &data, int offset)
1638 {
1639     if (offset == 0) {
1640         return data;
1641     }
1642     // TODO MLT7: port to Mlt::Animation
1643     /*auto &profile = pCore->getCurrentProfile();
1644     Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
1645     Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height());
1646     Mlt::GeometryItem item;
1647     int pos = 0;
1648     while (geometry.next_key(&item, pos) == 0) {
1649         pos = item.frame();
1650         item.frame(pos + offset);
1651         pos++;
1652         newgeometry.insert(item);
1653     }
1654     return newgeometry.serialise();
1655     */
1656     return QString();
1657 }
1658 
isSplittable() const1659 bool ProjectClip::isSplittable() const
1660 {
1661     return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist);
1662 }
1663 
setBinEffectsEnabled(bool enabled)1664 void ProjectClip::setBinEffectsEnabled(bool enabled)
1665 {
1666     ClipController::setBinEffectsEnabled(enabled);
1667 }
1668 
registerService(std::weak_ptr<TimelineModel> timeline,int clipId,const std::shared_ptr<Mlt::Producer> & service,bool forceRegister)1669 void ProjectClip::registerService(std::weak_ptr<TimelineModel> timeline, int clipId, const std::shared_ptr<Mlt::Producer> &service, bool forceRegister)
1670 {
1671     if (!service->is_cut() || forceRegister) {
1672         int hasAudio = service->get_int("set.test_audio") == 0;
1673         int hasVideo = service->get_int("set.test_image") == 0;
1674         if (hasVideo && m_videoProducers.count(clipId) == 0) {
1675             // This is an undo producer, register it!
1676             m_videoProducers[clipId] = service;
1677             m_effectStack->addService(m_videoProducers[clipId]);
1678         } else if (hasAudio && m_audioProducers.count(clipId) == 0) {
1679             // This is an undo producer, register it!
1680             m_audioProducers[clipId] = service;
1681             m_effectStack->addService(m_audioProducers[clipId]);
1682         }
1683     }
1684     registerTimelineClip(std::move(timeline), clipId);
1685 }
1686 
registerTimelineClip(std::weak_ptr<TimelineModel> timeline,int clipId)1687 void ProjectClip::registerTimelineClip(std::weak_ptr<TimelineModel> timeline, int clipId)
1688 {
1689     Q_ASSERT(m_registeredClips.count(clipId) == 0);
1690     Q_ASSERT(!timeline.expired());
1691     if (m_hasAudio) {
1692         if (auto ptr = timeline.lock()) {
1693             if (ptr->getClipState(clipId) == PlaylistState::AudioOnly) {
1694                 m_audioCount++;
1695             }
1696         }
1697     }
1698     m_registeredClips[clipId] = std::move(timeline);
1699     setRefCount(uint(m_registeredClips.size()), m_audioCount);
1700     emit registeredClipChanged();
1701 }
1702 
checkClipBounds()1703 void ProjectClip::checkClipBounds()
1704 {
1705     m_boundaryTimer.start();
1706 }
1707 
refreshBounds()1708 void ProjectClip::refreshBounds()
1709 {
1710     QVector <QPoint> boundaries;
1711     for (const auto &registeredClip : m_registeredClips) {
1712         if (auto ptr = registeredClip.second.lock()) {
1713             QPoint point = ptr->getClipInDuration(registeredClip.first);
1714             if (!boundaries.contains(point)) {
1715                 boundaries << point;
1716             }
1717         }
1718     }
1719     emit boundsChanged(boundaries);
1720 }
1721 
deregisterTimelineClip(int clipId,bool audioClip)1722 void ProjectClip::deregisterTimelineClip(int clipId, bool audioClip)
1723 {
1724     Q_ASSERT(m_registeredClips.count(clipId) > 0);
1725     if (m_hasAudio && audioClip) {
1726         m_audioCount--;
1727     }
1728     m_registeredClips.erase(clipId);
1729     if (m_videoProducers.count(clipId) > 0) {
1730         m_effectStack->removeService(m_videoProducers[clipId]);
1731         m_videoProducers.erase(clipId);
1732     }
1733     if (m_audioProducers.count(clipId) > 0) {
1734         m_effectStack->removeService(m_audioProducers[clipId]);
1735         m_audioProducers.erase(clipId);
1736     }
1737     setRefCount(uint(m_registeredClips.size()), m_audioCount);
1738     emit registeredClipChanged();
1739 }
1740 
timelineInstances() const1741 QList<int> ProjectClip::timelineInstances() const
1742 {
1743     QList<int> ids;
1744     for (const auto &registeredClip : m_registeredClips) {
1745         ids.push_back(registeredClip.first);
1746     }
1747     return ids;
1748 }
1749 
selfSoftDelete(Fun & undo,Fun & redo)1750 bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo)
1751 {
1752     Fun operation = [this]() {
1753         // Free audio thumb data and timeline producers
1754         pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()});
1755         m_audioLevels.clear();
1756         m_disabledProducer.reset();
1757         m_audioProducers.clear();
1758         m_videoProducers.clear();
1759         m_timewarpProducers.clear();
1760         return true;
1761     };
1762     operation();
1763 
1764     auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop
1765     for (const auto &clip : toDelete) {
1766         if (m_registeredClips.count(clip.first) == 0) {
1767             // clip already deleted, was probably grouped with another one
1768             continue;
1769         }
1770         if (auto timeline = clip.second.lock()) {
1771             timeline->requestClipUngroup(clip.first, undo, redo);
1772             timeline->requestItemDeletion(clip.first, undo, redo);
1773         } else {
1774             qDebug() << "Error while deleting clip: timeline unavailable";
1775             Q_ASSERT(false);
1776             return false;
1777         }
1778     }
1779     PUSH_LAMBDA(operation, redo);
1780     qDebug()<<"===== REMOVING MASTER PRODUCER; CURRENT COUNT: "<<m_masterProducer.use_count()<<"\n:::::::::::::::::::::::::::";
1781     return AbstractProjectItem::selfSoftDelete(undo, redo);
1782 }
1783 
getAudio_lambda()1784 Fun ProjectClip::getAudio_lambda()
1785 {
1786     return [this]() {
1787         if (KdenliveSettings::audiothumbnails() && (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Playlist) && m_audioLevels.isEmpty()) {
1788             // Generate audio levels
1789             AudioLevelsTask::start({ObjectType::BinClip, m_binId.toInt()}, this, false);
1790         }
1791         return true;
1792     };
1793 }
1794 
isIncludedInTimeline()1795 bool ProjectClip::isIncludedInTimeline()
1796 {
1797     return m_registeredClips.size() > 0;
1798 }
1799 
replaceInTimeline()1800 void ProjectClip::replaceInTimeline()
1801 {
1802     int updatedDuration = m_resetTimelineOccurences ? getFramePlaytime() : -1;
1803     m_resetTimelineOccurences = false;
1804     for (const auto &clip : m_registeredClips) {
1805         if (auto timeline = clip.second.lock()) {
1806             timeline->requestClipReload(clip.first, updatedDuration);
1807         } else {
1808             qDebug() << "Error while reloading clip: timeline unavailable";
1809             Q_ASSERT(false);
1810         }
1811     }
1812 }
1813 
updateTimelineClips(const QVector<int> & roles)1814 void ProjectClip::updateTimelineClips(const QVector<int> &roles)
1815 {
1816     for (const auto &clip : m_registeredClips) {
1817         if (auto timeline = clip.second.lock()) {
1818             timeline->requestClipUpdate(clip.first, roles);
1819         } else {
1820             qDebug() << "Error while reloading clip thumb: timeline unavailable";
1821             Q_ASSERT(false);
1822             return;
1823         }
1824     }
1825 }
1826 
updateZones()1827 void ProjectClip::updateZones()
1828 {
1829     int zonesCount = childCount();
1830     if (zonesCount == 0) {
1831         resetProducerProperty(QStringLiteral("kdenlive:clipzones"));
1832         return;
1833     }
1834     QJsonArray list;
1835     for (int i = 0; i < zonesCount; ++i) {
1836         std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
1837         if (clip) {
1838             QJsonObject currentZone;
1839             currentZone.insert(QLatin1String("name"), QJsonValue(clip->name()));
1840             QPoint zone = clip->zone();
1841             currentZone.insert(QLatin1String("in"), QJsonValue(zone.x()));
1842             currentZone.insert(QLatin1String("out"), QJsonValue(zone.y()));
1843             if (clip->rating() > 0) {
1844                 currentZone.insert(QLatin1String("rating"), QJsonValue(int(clip->rating())));
1845             }
1846             if (!clip->tags().isEmpty()) {
1847                 currentZone.insert(QLatin1String("tags"), QJsonValue(clip->tags()));
1848             }
1849             list.push_back(currentZone);
1850         }
1851     }
1852     QJsonDocument json(list);
1853     setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson()));
1854 }
1855 
1856 
getThumbFromPercent(int percent,bool storeFrame)1857 void ProjectClip::getThumbFromPercent(int percent, bool storeFrame)
1858 {
1859     // extract a maximum of 30 frames for bin preview
1860     if (percent < 0) {
1861         if (hasProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"))) {
1862             int framePos = qMax(0, getProducerIntProperty(QStringLiteral("kdenlive:thumbnailFrame")));
1863             setThumbnail(ThumbnailCache::get()->getThumbnail(m_binId, framePos), -1, -1);
1864         }
1865         return;
1866     }
1867     int duration = getFramePlaytime();
1868     int steps = qCeil(qMax(pCore->getCurrentFps(), double(duration) / 30));
1869     int framePos = duration * percent / 100;
1870     framePos -= framePos%steps;
1871     if (ThumbnailCache::get()->hasThumbnail(m_binId, framePos)) {
1872         setThumbnail(ThumbnailCache::get()->getThumbnail(m_binId, framePos), -1, -1);
1873     } else {
1874         // Generate percent thumbs
1875         CacheTask::start({ObjectType::BinClip,m_binId.toInt()}, 30, 0, 0, this);
1876     }
1877     if (storeFrame) {
1878         setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), framePos);
1879     }
1880 }
1881 
setRating(uint rating)1882 void ProjectClip::setRating(uint rating)
1883 {
1884     AbstractProjectItem::setRating(rating);
1885     setProducerProperty(QStringLiteral("kdenlive:rating"), int(rating));
1886     pCore->currentDoc()->setModified(true);
1887 }
1888 
getAudioMax(int stream)1889 int ProjectClip::getAudioMax(int stream)
1890 {
1891     const QString key = QString("kdenlive:audio_max%1").arg(stream);
1892     if (m_masterProducer->property_exists(key.toUtf8().constData())) {
1893         return m_masterProducer->get_int(key.toUtf8().constData());
1894     }
1895     // Process audio max for the stream
1896     const QString key2 = QString("_kdenlive:audio%1").arg(stream);
1897     if (!m_masterProducer->property_exists(key2.toUtf8().constData())) {
1898         return 0;
1899     }
1900     const QVector <uint8_t> audioData = *static_cast<QVector<uint8_t> *>(m_masterProducer->get_data(key2.toUtf8().constData()));
1901     if (audioData.isEmpty()) {
1902         return 0;
1903     }
1904     uint max = *std::max_element(audioData.constBegin(), audioData.constEnd());
1905     m_masterProducer->set(key.toUtf8().constData(), int(max));
1906     return int(max);
1907 }
1908 
audioFrameCache(int stream)1909 const QVector <uint8_t> ProjectClip::audioFrameCache(int stream)
1910 {
1911     QVector <uint8_t> audioLevels;
1912     if (stream == -1) {
1913         if (m_audioInfo) {
1914             stream = m_audioInfo->ffmpeg_audio_index();
1915         } else {
1916             return audioLevels;
1917         }
1918     }
1919     const QString key = QString("_kdenlive:audio%1").arg(stream);
1920     if (m_masterProducer->get_data(key.toUtf8().constData())) {
1921         const QVector <uint8_t> audioData = *static_cast<QVector<uint8_t> *>(m_masterProducer->get_data(key.toUtf8().constData()));
1922         return audioData;
1923     } else {
1924         qDebug()<<"=== AUDIO NOT FOUND ";
1925     }
1926     return QVector <uint8_t>();
1927 
1928     /*QString key = QString("%1:%2").arg(m_binId).arg(stream);
1929     QByteArray audioData;
1930     if (pCore->audioThumbCache.find(key, &audioData)) {
1931         if (audioData != QByteArray("-")) {
1932             QDataStream in(audioData);
1933             in >> audioLevels;
1934             return audioLevels;
1935         }
1936     }
1937     // convert cached image
1938     const QString cachePath = getAudioThumbPath(stream);
1939     // checking for cached thumbs
1940     QImage image(cachePath);
1941     if (!image.isNull()) {
1942         int channels = m_audioInfo->channelsForStream(stream);
1943         int n = image.width() * image.height();
1944         for (int i = 0; i < n; i++) {
1945             QRgb p = image.pixel(i / channels, i % channels);
1946             audioLevels << uint8_t(qRed(p));
1947             audioLevels << uint8_t(qGreen(p));
1948             audioLevels << uint8_t(qBlue(p));
1949             audioLevels << uint8_t(qAlpha(p));
1950         }
1951         // populate vector
1952         QDataStream st(&audioData, QIODevice::WriteOnly);
1953         st << audioLevels;
1954         pCore->audioThumbCache.insert(key, audioData);
1955     }
1956     return audioLevels;*/
1957 }
1958 
setClipStatus(FileStatus::ClipStatus status)1959 void ProjectClip::setClipStatus(FileStatus::ClipStatus status)
1960 {
1961     AbstractProjectItem::setClipStatus(status);
1962     updateTimelineClips({TimelineModel::StatusRole});
1963     if (auto ptr = m_model.lock()) {
1964         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
1965                                                                        AbstractProjectItem::IconOverlay);
1966     }
1967 }
1968 
renameAudioStream(int id,QString name)1969 void ProjectClip::renameAudioStream(int id, QString name)
1970 {
1971     if (m_audioInfo) {
1972         m_audioInfo->renameStream(id, name);
1973         QString prop = QString("kdenlive:streamname.%1").arg(id);
1974         m_masterProducer->set(prop.toUtf8().constData(), name.toUtf8().constData());
1975         if (m_audioInfo->activeStreams().keys().contains(id)) {
1976             pCore->bin()->updateTargets(clipId());
1977         }
1978         pCore->bin()->reloadMonitorStreamIfActive(clipId());
1979     }
1980 }
1981 
requestAddStreamEffect(int streamIndex,const QString effectName)1982 void ProjectClip::requestAddStreamEffect(int streamIndex, const QString effectName)
1983 {
1984     QStringList readEffects = m_streamEffects.value(streamIndex);
1985     QString oldEffect;
1986     // Remove effect if present (parameters might have changed
1987     for (const QString &effect : qAsConst(readEffects)) {
1988         if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
1989             oldEffect = effect;
1990             break;
1991         }
1992     }
1993     Fun redo = [this, streamIndex, effectName]() {
1994         addAudioStreamEffect(streamIndex, effectName);
1995         emit updateStreamInfo(streamIndex);
1996         return true; };
1997     Fun undo = [this, streamIndex, effectName, oldEffect]() {
1998         if (!oldEffect.isEmpty()) {
1999             // restore previous parameter value
2000             addAudioStreamEffect(streamIndex, oldEffect);
2001         } else {
2002             removeAudioStreamEffect(streamIndex, effectName);
2003         }
2004         emit updateStreamInfo(streamIndex);
2005         return true;
2006     };
2007     addAudioStreamEffect(streamIndex, effectName);
2008     pCore->pushUndo(undo, redo, i18n("Add stream effect"));
2009 }
2010 
requestRemoveStreamEffect(int streamIndex,const QString effectName)2011 void ProjectClip::requestRemoveStreamEffect(int streamIndex, const QString effectName)
2012 {
2013     QStringList readEffects = m_streamEffects.value(streamIndex);
2014     QString oldEffect = effectName;
2015     // Remove effect if present (parameters might have changed
2016     for (const QString &effect : qAsConst(readEffects)) {
2017         if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
2018             oldEffect = effect;
2019             break;
2020         }
2021     }
2022     Fun undo = [this, streamIndex, effectName, oldEffect]() {
2023         addAudioStreamEffect(streamIndex, oldEffect);
2024         emit updateStreamInfo(streamIndex);
2025         return true; };
2026     Fun redo = [this, streamIndex, effectName]() {
2027         removeAudioStreamEffect(streamIndex, effectName);
2028         emit updateStreamInfo(streamIndex);
2029         return true; };
2030     removeAudioStreamEffect(streamIndex, effectName);
2031     pCore->pushUndo(undo, redo, i18n("Remove stream effect"));
2032 }
2033 
addAudioStreamEffect(int streamIndex,const QString effectName)2034 void ProjectClip::addAudioStreamEffect(int streamIndex, const QString effectName)
2035 {
2036     QString addedEffectName;
2037     QMap <QString, QString> effectParams;
2038     if (effectName.contains(QLatin1Char(' '))) {
2039         // effect has parameters
2040         QStringList params = effectName.split(QLatin1Char(' '));
2041         addedEffectName = params.takeFirst();
2042         for (const QString &p : qAsConst(params)) {
2043             QStringList paramValue = p.split(QLatin1Char('='));
2044             if (paramValue.size() == 2) {
2045                 effectParams.insert(paramValue.at(0), paramValue.at(1));
2046             }
2047         }
2048     } else {
2049         addedEffectName = effectName;
2050     }
2051     QStringList effects;
2052     if (m_streamEffects.contains(streamIndex)) {
2053         QStringList readEffects = m_streamEffects.value(streamIndex);
2054         // Remove effect if present (parameters might have changed
2055         for (const QString &effect : qAsConst(readEffects)) {
2056             if (effect == addedEffectName || effect.startsWith(addedEffectName + QStringLiteral(" "))) {
2057                 continue;
2058             }
2059             effects << effect;
2060         }
2061         effects << effectName;
2062     } else {
2063         effects = QStringList({effectName});
2064     }
2065     m_streamEffects.insert(streamIndex, effects);
2066     setProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex), effects.join(QLatin1Char('#')));
2067     for (auto &p : m_audioProducers) {
2068         int stream = p.first / 100;
2069         if (stream == streamIndex) {
2070             // Remove existing effects with same name
2071             int max = p.second->filter_count();
2072             for (int i = 0; i < max; i++) {
2073                 QScopedPointer<Mlt::Filter> f(p.second->filter(i));
2074                 if (f->get("mlt_service") == addedEffectName) {
2075                     p.second->detach(*f.get());
2076                     break;
2077                 }
2078             }
2079             Mlt::Filter filt(*p.second->profile(), addedEffectName.toUtf8().constData());
2080             if (filt.is_valid()) {
2081                 // Add stream effect markup
2082                 filt.set("kdenlive:stream", 1);
2083                 // Set parameters
2084                 QMapIterator<QString, QString> i(effectParams);
2085                 while (i.hasNext()) {
2086                     i.next();
2087                     filt.set(i.key().toUtf8().constData(), i.value().toUtf8().constData());
2088                 }
2089                 p.second->attach(filt);
2090             }
2091         }
2092     }
2093 }
2094 
removeAudioStreamEffect(int streamIndex,QString effectName)2095 void ProjectClip::removeAudioStreamEffect(int streamIndex, QString effectName)
2096 {
2097     QStringList effects;
2098     if (effectName.contains(QLatin1Char(' '))) {
2099         effectName = effectName.section(QLatin1Char(' '), 0, 0);
2100     }
2101     if (m_streamEffects.contains(streamIndex)) {
2102         QStringList readEffects = m_streamEffects.value(streamIndex);
2103         // Remove effect if present (parameters might have changed
2104         for (const QString &effect : qAsConst(readEffects)) {
2105             if (effect == effectName || effect.startsWith(effectName + QStringLiteral(" "))) {
2106                 continue;
2107             }
2108             effects << effect;
2109         }
2110         if (effects.isEmpty()) {
2111             m_streamEffects.remove(streamIndex);
2112             resetProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex));
2113         } else {
2114             m_streamEffects.insert(streamIndex, effects);
2115             setProducerProperty(QString("kdenlive:stream:%1").arg(streamIndex), effects.join(QLatin1Char('#')));
2116         }
2117     } else {
2118         // No effects for this stream, this is not expected, abort
2119         return;
2120     }
2121     for (auto &p : m_audioProducers) {
2122         int stream = p.first / 100;
2123         if (stream == streamIndex) {
2124             int max = p.second->filter_count();
2125             for (int i = 0; i < max; i++) {
2126                 std::shared_ptr<Mlt::Filter> fl(p.second->filter(i));
2127                 if (!fl->is_valid()) {
2128                     continue;
2129                 }
2130                 if (fl->get_int("kdenlive:stream") != 1)  {
2131                     // This is not an audio stream effect
2132                     continue;
2133                 }
2134                 if (fl->get("mlt_service") == effectName) {
2135                     p.second->detach(*fl.get());
2136                     break;
2137                 }
2138             }
2139         }
2140     }
2141 }
2142 
getAudioStreamEffect(int streamIndex) const2143 QStringList ProjectClip::getAudioStreamEffect(int streamIndex) const
2144 {
2145     QStringList effects;
2146     if (m_streamEffects.contains(streamIndex)) {
2147         effects = m_streamEffects.value(streamIndex);
2148     }
2149     return effects;
2150 }
2151 
updateTimelineOnReload()2152 void ProjectClip::updateTimelineOnReload()
2153 {
2154     if (m_registeredClips.size() > 0 && m_registeredClips.size() < 3) {
2155         bool reloadProducer = true;
2156         for (const auto &clip : m_registeredClips) {
2157             if (auto timeline = clip.second.lock()) {
2158                 if (timeline->getClipPlaytime(clip.first) < static_cast<int>(frameDuration())) {
2159                     reloadProducer = false;
2160                     break;
2161                 }
2162             }
2163             if (reloadProducer) {
2164                 m_resetTimelineOccurences = true;
2165             }
2166         }
2167     }
2168 }
2169 
updateJobProgress()2170 void ProjectClip::updateJobProgress()
2171 {
2172     if (auto ptr = m_model.lock()) {
2173         std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(m_binId, AbstractProjectItem::JobProgress);
2174     }
2175 }
2176 
setInvalid()2177 void ProjectClip::setInvalid()
2178 {
2179     m_isInvalid = true;
2180     m_producerLock.unlock();
2181 }
2182 
updateProxyProducer(const QString & path)2183 void ProjectClip::updateProxyProducer(const QString &path)
2184 {
2185     resetProducerProperty(QStringLiteral("_overwriteproxy"));
2186     setProducerProperty(QStringLiteral("resource"), path);
2187     reloadProducer(false, true);
2188 }
2189 
importJsonMarkers(const QString & json)2190 void ProjectClip::importJsonMarkers(const QString &json)
2191 {
2192     getMarkerModel()->importFromJson(json, true);
2193 }
2194