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