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 ®isteredClip : 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 ®isteredClip : 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