diff --git a/src/audiomixer/mixermanager.cpp b/src/audiomixer/mixermanager.cpp index 9370c0985..65ddc457f 100644 --- a/src/audiomixer/mixermanager.cpp +++ b/src/audiomixer/mixermanager.cpp @@ -1,224 +1,215 @@ /*************************************************************************** * Copyright (C) 2019 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "mixermanager.hpp" #include "mixerwidget.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "kdenlivesettings.h" #include "mlt++/MltService.h" #include "mlt++/MltTractor.h" #include #include #include #include #include const double log_factor = 1.0 / log10(1.0 / 127); static inline double levelToDB(double level) { if (level <= 0) { return -100; } return 100 * (1.0 - log10(level) * log_factor); } MixerManager::MixerManager(QWidget *parent) : QWidget(parent) , m_masterMixer(nullptr) , m_connectedWidgets(false) , m_expandedWidth(-1) { m_masterBox = new QHBoxLayout; m_channelsBox = new QScrollArea(this); m_box = new QHBoxLayout; m_box->setSpacing(0); auto *channelsBoxContainer = new QWidget; channelsBoxContainer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_channelsBox->setWidget(channelsBoxContainer); m_channelsBox->setWidgetResizable(true); m_channelsBox->setFrameShape(QFrame::NoFrame); m_box->addWidget(m_channelsBox); m_channelsLayout = new QHBoxLayout; m_channelsLayout->setContentsMargins(0, 0, 0, 0); m_channelsLayout->setSpacing(0); channelsBoxContainer->setLayout(m_channelsLayout); m_channelsLayout->addStretch(10); m_line = new QFrame(this); m_line->setFrameShape(QFrame::VLine); m_line->setFrameShadow(QFrame::Sunken); m_box->addWidget(m_line); m_box->addLayout(m_masterBox); setLayout(m_box); } void MixerManager::registerTrack(int tid, std::shared_ptr service, const QString &trackTag) { if (m_mixers.count(tid) > 0) { // Track already registered return; } std::shared_ptr mixer(new MixerWidget(tid, service, trackTag, this)); connect(mixer.get(), &MixerWidget::muteTrack, [&](int id, bool mute) { m_model->setTrackProperty(id, "hide", mute ? QStringLiteral("1") : QStringLiteral("3")); }); if (m_connectedWidgets) { mixer->connectMixer(true); } connect(this, &MixerManager::updateLevels, mixer.get(), &MixerWidget::updateAudioLevel); + connect(this, &MixerManager::clearMixers, mixer.get(), &MixerWidget::clear); connect(mixer.get(), &MixerWidget::toggleSolo, [&](int trid, bool solo) { if (!solo) { // unmute for (int id : m_soloMuted) { if (m_mixers.count(id) > 0) { m_model->setTrackProperty(id, "hide", QStringLiteral("1")); } } m_soloMuted.clear(); } else { if (!m_soloMuted.isEmpty()) { // Another track was solo, discard first for (int id : m_soloMuted) { if (m_mixers.count(id) > 0) { m_model->setTrackProperty(id, "hide", QStringLiteral("1")); } } m_soloMuted.clear(); } for (auto item : m_mixers) { if (item.first != trid && !item.second->isMute()) { m_model->setTrackProperty(item.first, "hide", QStringLiteral("3")); m_soloMuted << item.first; item.second->unSolo(); } } } }); m_mixers[tid] = mixer; m_channelsLayout->insertWidget(0, mixer.get()); } void MixerManager::deregisterTrack(int tid) { Q_ASSERT(m_mixers.count(tid) > 0); m_mixers.erase(tid); } -void MixerManager::resetAudioValues() -{ - qDebug()<<"======\n\nRESTTING AUDIO VALUES\n\n------------------_"; - for (auto item : m_mixers) { - item.second.get()->clear(); - } - if (m_masterMixer) { - m_masterMixer->clear(); - } -} - void MixerManager::cleanup() { for (auto item : m_mixers) { m_channelsLayout->removeWidget(item.second.get()); } m_mixers.clear(); if (m_masterMixer) { m_masterMixer->clear(true); } } void MixerManager::setModel(std::shared_ptr model) { // Insert master mixer m_model = model; connect(m_model.get(), &TimelineItemModel::dataChanged, [&](const QModelIndex &topLeft, const QModelIndex &, const QVector &roles) { if (roles.contains(TimelineModel::IsDisabledRole)) { int id = (int) topLeft.internalId(); if (m_mixers.count(id) > 0) { m_mixers[id]->setMute(m_model->data(topLeft, TimelineModel::IsDisabledRole).toBool()); } else { qDebug()<<"=== MODEL DATA CHANGED: MUTE DONE TRACK NOT FOUND!!!"; } } }); Mlt::Tractor *service = model->tractor(); if (m_masterMixer != nullptr) { // delete previous master mixer m_masterBox->removeWidget(m_masterMixer.get()); } m_masterMixer.reset(new MixerWidget(-1, service, i18n("Master"), this)); connect(m_masterMixer.get(), &MixerWidget::muteTrack, [&](int /*id*/, bool mute) { m_model->tractor()->set("hide", mute ? 3 : 1); }); if (m_connectedWidgets) { m_masterMixer->connectMixer(true); } connect(this, &MixerManager::updateLevels, m_masterMixer.get(), &MixerWidget::updateAudioLevel); + connect(this, &MixerManager::clearMixers, m_masterMixer.get(), &MixerWidget::clear); m_masterBox->addWidget(m_masterMixer.get()); collapseMixers(); } void MixerManager::recordStateChanged(int tid, bool recording) { if (m_mixers.count(tid) > 0) { m_mixers[tid]->setRecordState(recording); } } void MixerManager::connectMixer(bool doConnect, bool channelsOnly) { m_connectedWidgets = doConnect; for (auto item : m_mixers) { item.second->connectMixer(m_connectedWidgets); } if (!channelsOnly && m_masterMixer != nullptr) { m_masterMixer->connectMixer(m_connectedWidgets); } } void MixerManager::collapseMixers() { if (KdenliveSettings::mixerCollapse()) { m_expandedWidth = width(); m_channelsBox->setFixedWidth(0); m_line->setMaximumWidth(0); connectMixer(false, true); setFixedWidth(m_masterMixer->width() + 2 * m_box->contentsMargins().left()); } else { m_line->setMaximumWidth(QWIDGETSIZE_MAX); m_channelsBox->setMaximumWidth(QWIDGETSIZE_MAX); m_channelsBox->setMinimumWidth(m_masterMixer->width() + 2 * m_box->contentsMargins().left()); setMaximumWidth(QWIDGETSIZE_MAX); if (m_expandedWidth > 0) { setFixedWidth(m_expandedWidth); } connectMixer(true, true); QTimer::singleShot(500, this, &MixerManager::resetSizePolicy); } } void MixerManager::resetSizePolicy() { setMaximumWidth(QWIDGETSIZE_MAX); setMinimumWidth(0); } diff --git a/src/audiomixer/mixermanager.hpp b/src/audiomixer/mixermanager.hpp index 5d5cf6012..c062dd49c 100644 --- a/src/audiomixer/mixermanager.hpp +++ b/src/audiomixer/mixermanager.hpp @@ -1,88 +1,88 @@ /*************************************************************************** * Copyright (C) 2019 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef MIXERMANGER_H #define MIXERMANGER_H #include "definitions.h" #include #include #include namespace Mlt { class Tractor; } class MixerWidget; class QHBoxLayout; class TimelineItemModel; class QScrollArea; class QFrame; class MixerManager : public QWidget { Q_OBJECT public: MixerManager(QWidget *parent); /** @brief Shows the parameters of the given transition model */ void registerTrack(int tid, std::shared_ptr service, const QString &trackTag); void deregisterTrack(int tid); void setModel(std::shared_ptr model); void cleanup(); /** @brief Connect the mixer widgets to the correspondant filters */ void connectMixer(bool doConnect, bool channelsOnly = false); void collapseMixers(); public slots: - void resetAudioValues(); void recordStateChanged(int tid, bool recording); private slots: void resetSizePolicy(); signals: void updateLevels(int); void recordAudio(int tid); void purgeCache(); + void clearMixers(); protected: std::unordered_map> m_mixers; std::shared_ptr m_masterMixer; private: std::shared_ptr m_masterService; std::shared_ptr m_model; QHBoxLayout *m_box; QHBoxLayout *m_masterBox; QHBoxLayout *m_channelsLayout; QScrollArea *m_channelsBox; QFrame *m_line; int m_lastFrame; bool m_connectedWidgets; int m_expandedWidth; QVector m_soloMuted; }; #endif diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index e736095a3..bd1e37f02 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1437 +1,1439 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "jobs/cachejob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include "logger.hpp" #include #include #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, std::move(producer)) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_outPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(const QString&, id), Q_ARG(const QString&, self->getProducerProperty(QStringLiteral("kdenlive:clipzones")))); return self; } void ProjectClip::importEffects(const std::shared_ptr &producer) { m_effectStack->importEffects(producer, PlaylistState::Disabled, true); } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); } QString ProjectClip::getToolTip() const { return url(); } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail(QList audioLevels) { audioFrameCache = audioLevels; m_audioThumbCreated = true; } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { GenTime d = duration(); return (size_t)d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly, bool audioStreamChanged) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } if (QFile::exists(m_path) && !hasProxy()) { clearBackupProperties(); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ClipType::ProducerType type = clipType(); if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) { xml.removeAttribute("out"); } ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); if (audioStreamChanged) { discardAudioThumb(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString()); } } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile) { getProducerXML(document, includeMeta, includeProfile); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(producer); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); replaceInTimeline(); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(converter); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); // Required to make get_playtime() return > 1 m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer if (trackId == -1 || (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text))) { // Temporary copy, return clone of master int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration : -1)); } if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (m_audioProducers.count(trackId) == 0) { m_audioProducers[trackId] = cloneProducer(true); m_audioProducers[trackId]->set("set.test_audio", 0); m_audioProducers[trackId]->set("set.test_image", 1); m_effectStack->addService(m_audioProducers[trackId]); } return std::shared_ptr(m_audioProducers[trackId]->cut()); } if (m_audioProducers.count(trackId) > 0) { m_effectStack->removeService(m_audioProducers[trackId]); m_audioProducers.erase(trackId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_videoProducers.count(trackId) == 0) { m_videoProducers[trackId] = cloneProducer(true); m_videoProducers[trackId]->set("set.test_audio", 1); m_videoProducers[trackId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[trackId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(trackId) > 0) { m_effectStack->removeService(m_videoProducers[trackId]); m_videoProducers.erase(trackId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration : -1)); } // For timewarp clips, we keep one separate producer for each clip. std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed))).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(warpProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList(false)); warpProducer->set("length", (int) (original_length / std::abs(speed) + 0.5)); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(-1, clipId, state, speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); } return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { m_audioProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any m_videoProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_videoProducers[clipId]); return {master, true}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { createDisabledMasterProducer(); } return {std::make_shared(m_disabledProducer->cut(master->get_in(), master->get_out())), true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(-1, clipId, state, speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } /* std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { std::shared_ptr prod(originalProducer()->cut()); int length = getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (length > 0) { prod->set_in_and_out(0, length); } return prod; } if (state == PlaylistState::VideoOnly) { if (m_timelineProducers.count(0) > 0) { return std::shared_ptr(m_timelineProducers.find(0)->second->cut()); } std::shared_ptr videoProd = cloneProducer(); videoProd->set("audio_index", -1); m_timelineProducers[0] = videoProd; return std::shared_ptr(videoProd->cut()); } if (state == PlaylistState::AudioOnly) { if (m_timelineProducers.count(-track) > 0) { return std::shared_ptr(m_timelineProducers.find(-track)->second->cut()); } std::shared_ptr audioProd = cloneProducer(); audioProd->set("video_index", -1); m_timelineProducers[-track] = audioProd; return std::shared_ptr(audioProd->cut()); } if (m_timelineProducers.count(track) > 0) { return std::shared_ptr(m_timelineProducers.find(track)->second->cut()); } std::shared_ptr normalProd = cloneProducer(); m_timelineProducers[track] = normalProd; return std::shared_ptr(normalProd->cut()); }*/ std::shared_ptr ProjectClip::cloneProducer(bool removeEffects) { Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.run(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod; prod.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); + prod->set("mute_on_pause", 0); } // we pass some properties that wouldn't be passed because of the novalidate const char *prefix = "meta."; const size_t prefix_len = strlen(prefix); for (int i = 0; i < m_masterProducer->count(); ++i) { char *current = m_masterProducer->get_name(i); if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) { prod->set(current, m_masterProducer->get(i)); } } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELTING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } std::shared_ptr ProjectClip::cloneProducer(const std::shared_ptr &producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); + prod->set("mute_on_pause", 0); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromLatin1(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } QPoint ProjectClip::zone() const { return ClipController::zone(); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{ QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_delay") }; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio"), QStringLiteral("audio_index"), QStringLiteral("video_index")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); backupOriginalProperties(); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { if (m_clipType != ClipType::Color) { reload = true; refreshOnly = false; } else { // Clip resource changed, update thumbnail reload = true; refreshPanel = true; updateRoles << TimelineModel::ResourceRole; } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly, properties.contains(QStringLiteral("audio_index"))); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty() && (!reload || refreshOnly)) { for (auto &p : m_audioProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_videoProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_timewarpProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); auto *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; refreshAudioInfo(); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath(bool miniThumb) { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (miniThumb) { audioPath.append(QStringLiteral(".png")); return audioPath; } if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (const auto &m_registeredClip : m_registeredClips) { ids.push_back(m_registeredClip.first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(const QVector &roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } void ProjectClip::updateZones() { int zonesCount = childCount(); if (zonesCount == 0) { resetProducerProperty(QStringLiteral("kdenlive:clipzones")); return; } QJsonArray list; for (int i = 0; i < zonesCount; ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { QJsonObject currentZone; currentZone.insert(QLatin1String("name"), QJsonValue(clip->name())); QPoint zone = clip->zone(); currentZone.insert(QLatin1String("in"), QJsonValue(zone.x())); currentZone.insert(QLatin1String("out"), QJsonValue(zone.y())); list.push_back(currentZone); } } QJsonDocument json(list); setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson())); } void ProjectClip::getThumbFromPercent(int percent) { // extract a maximum of 50 frames for bin preview percent += percent%2; int duration = getFramePlaytime(); int framePos = duration * percent / 100; if (ThumbnailCache::get()->hasThumbnail(m_binId, framePos)) { setThumbnail(ThumbnailCache::get()->getThumbnail(m_binId, framePos)); } else { // Generate percent thumbs int id; if (pCore->jobManager()->hasPendingJob(m_binId, AbstractClipJob::CACHEJOB, &id)) { } else { pCore->jobManager()->startJob({m_binId}, -1, QString(), 150, 50); } } } diff --git a/src/core.cpp b/src/core.cpp index 38867b71c..26c5f4941 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,792 +1,792 @@ /* Copyright (C) 2014 Till Theato This file is part of kdenlive. See www.kdenlive.org. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. */ #include "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "capture/mediacapture.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "audiomixer/mixermanager.hpp" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() : m_thumbProfile(nullptr) , m_capture(new MediaCapture(this)) { } void Core::prepareShutdown() { m_guiConstructed = false; QThreadPool::globalInstance()->clear(); } Core::~Core() { qDebug() << "deleting core"; if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(bool isAppImage, const QString &MltPath) { if (m_self) { return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); if (isAppImage) { QString appPath = qApp->applicationDirPath(); KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg"))); KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay"))); KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe"))); KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt"))); MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles"))); } else { // Open connection with Mlt MltConnection::construct(MltPath); } // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } void Core::initGUI(const QUrl &Url) { m_guiConstructed = true; m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); m_mixerWidget = new MixerManager(m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); connect(m_capture.get(), &MediaCapture::recordStateChanged, m_mixerWidget, &MixerManager::recordStateChanged); m_monitorManager = new MonitorManager(this); - connect(m_monitorManager, &MonitorManager::cleanMixer, m_mixerWidget, &MixerManager::resetAudioValues); + connect(m_monitorManager, &MonitorManager::cleanMixer, m_mixerWidget, &MixerManager::clearMixers); // Producer queue, creating MLT::Producers on request /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, QString()); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); m_mainWindow->show(); QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount())); } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { return m_jobManager; } LibraryWidget *Core::library() { return m_library; } MixerManager *Core::mixer() { return m_mixerWidget; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; m_thumbProfile.reset(); // inform render widget profileChanged(); m_mainWindow->updateRenderWidgetProfile(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } else { qDebug()<<"// ERROR QUERYING NON CLIP PROPERTIES\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; } break; case ObjectType::TimelineComposition: case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; case ObjectType::Master: return PlaylistState::Disabled; break; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(id.second); break; case ObjectType::TimelineTrack: case ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->duration(); default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->refreshClipMonitor(); break; case ObjectType::Master: requestMonitorRefresh(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } void Core::displayMessage(const QString &message, MessageType type, int timeout) { if (m_mainWindow) { m_mainWindow->displayMessage(message, type, timeout); } else { qDebug() << message; } } void Core::displayBinMessage(const QString &text, int type, const QList &actions) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId); break; case (int)ObjectType::BinClip: return m_binWidget->getClipEffectStack(itemId); case (int)ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getMasterEffectStackModel(); default: return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getTrackNames(bool videoOnly) { if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(videoOnly); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_mainWindow || !m_mainWindow->getCurrentTimeline() || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second); break; case ObjectType::TimelineTrack: m_mainWindow->getCurrentTimeline()->controller()->invalidateTrack(itemId.second); break; case ObjectType::BinClip: m_binWidget->invalidateClip(QString::number(itemId.second)); break; case ObjectType::Master: m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(0, -1); break; default: // compositions should not have effects break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip && m_mainWindow) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_mainWindow && id.first == ObjectType::TimelineClip && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade"))) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } Mlt::Profile *Core::thumbProfile() { QMutexLocker lck(&m_thumbProfileMutex); if (!m_thumbProfile) { m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_thumbProfile->set_height(144); int width = 144 * m_thumbProfile->dar() + 0.5; if (width % 8 > 0) { width += 8 - width % 8; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } int Core::getTimelinePosition() const { if (m_mainWindow && m_guiConstructed) { return m_mainWindow->getCurrentTimeline()->controller()->timelinePosition(); } return 0; } void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } void Core::startMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, true); } else if (checkAudio) { m_capture->recordAudio(tid, true); } m_mediaCaptureFile = m_capture->getCaptureOutputLocation(); } void Core::stopMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, false); } else if (checkAudio) { m_capture->recordAudio(tid, false); } } QStringList Core::getAudioCaptureDevices() { return m_capture->getAudioCaptureDevices(); } int Core::getMediaCaptureState() { return m_capture->getState(); } bool Core::isMediaCapturing() { return m_capture->isRecording(); } MediaCapture *Core::getAudioDevice() { return m_capture.get(); } QString Core::getProjectFolderName() { if (currentDoc()) { return currentDoc()->projectDataFolder() + QDir::separator(); } return QString(); } QString Core::getTimelineClipBinId(int cid) { if (!m_guiConstructed) { return QString(); } if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid); } return QString(); } int Core::getDurationFromString(const QString &time) { if (!m_guiConstructed) { return 0; } const QString duration = currentDoc()->timecode().reformatSeparators(time); return currentDoc()->timecode().getFrameCount(duration); } diff --git a/src/main.cpp b/src/main.cpp index 8dcf1af62..c0dd8f329 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,256 +1,267 @@ /*************************************************************************** * Copyright (C) 2007 by Marco Gittler (g.marco@freenet.de) * * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "core.h" #include "dialogs/splash.hpp" #include "logger.hpp" #include #include #include "kxmlgui_version.h" #include #include #ifdef USE_DRMINGW #include #elif defined(KF5_USE_CRASH) #include #endif #include #include #include "definitions.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include //new #include +#ifdef Q_OS_WIN +extern "C" +{ + // Inform the driver we could make use of the discrete gpu + __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; + __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +} +#endif + + int main(int argc, char *argv[]) { #ifdef USE_DRMINGW ExcHndlInit(); #endif // Force QDomDocument to use a deterministic XML attribute order qSetGlobalQHashSeed(0); Logger::init(); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + //TODO: is it a good option ? QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) QCoreApplication::setAttribute(Qt::AA_X11InitThreads); #elif defined(Q_OS_WIN) KSharedConfigPtr configWin = KSharedConfig::openConfig("kdenliverc"); KConfigGroup grp1(configWin, "misc"); if (grp1.exists()) { int glMode = grp1.readEntry("opengl_backend", 0); if (glMode > 0) { QCoreApplication::setAttribute((Qt::ApplicationAttribute)glMode, true); } } #endif QApplication app(argc, argv); app.setApplicationName(QStringLiteral("kdenlive")); app.setOrganizationDomain(QStringLiteral("kde.org")); app.setWindowIcon(QIcon(QStringLiteral(":/pics/kdenlive.png"))); KLocalizedString::setApplicationDomain("kdenlive"); #ifdef Q_OS_WIN qputenv("KDE_FORK_SLAVES", "1"); QString path = qApp->applicationDirPath() + QLatin1Char(';') + qgetenv("PATH"); qputenv("PATH", path.toUtf8().constData()); const QStringList themes {"/icons/breeze/breeze-icons.rcc", "/icons/breeze-dark/breeze-icons-dark.rcc"}; for(const QString theme : themes ) { const QString themePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, theme); if (!themePath.isEmpty()) { const QString iconSubdir = theme.left(theme.lastIndexOf('/')); if (QResource::registerResource(themePath, iconSubdir)) { if (QFileInfo::exists(QLatin1Char(':') + iconSubdir + QStringLiteral("/index.theme"))) { qDebug() << "Loaded icon theme:" << theme; } else { qWarning() << "No index.theme found in" << theme; QResource::unregisterResource(themePath, iconSubdir); } } else { qWarning() << "Invalid rcc file" << theme; } } } #endif KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp(config, "unmanaged"); if (!grp.exists()) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (env.contains(QStringLiteral("XDG_CURRENT_DESKTOP")) && env.value(QStringLiteral("XDG_CURRENT_DESKTOP")).toLower() == QLatin1String("kde")) { qCDebug(KDENLIVE_LOG) << "KDE Desktop detected, using system icons"; } else { // We are not on a KDE desktop, force breeze icon theme // Check if breeze theme is available QStringList iconThemes = KIconTheme::list(); if (iconThemes.contains(QStringLiteral("breeze"))) { grp.writeEntry("force_breeze", true); grp.writeEntry("use_dark_breeze", true); qCDebug(KDENLIVE_LOG) << "Non KDE Desktop detected, forcing Breeze icon theme"; } } // Set breeze dark as default on first opening KConfigGroup cg(config, "UiSettings"); cg.writeEntry("ColorScheme", "Breeze Dark"); } // Init DBus services KDBusService programDBusService; bool forceBreeze = grp.readEntry("force_breeze", QVariant(false)).toBool(); if (forceBreeze) { bool darkBreeze = grp.readEntry("use_dark_breeze", QVariant(false)).toBool(); QIcon::setThemeName(darkBreeze ? QStringLiteral("breeze-dark") : QStringLiteral("breeze")); } // Create KAboutData KAboutData aboutData(QByteArray("kdenlive"), i18n("Kdenlive"), KDENLIVE_VERSION, i18n("An open source video editor."), KAboutLicense::GPL, i18n("Copyright © 2007–2019 Kdenlive authors"), i18n("Please report bugs to http://bugs.kde.org"), QStringLiteral("https://kdenlive.org")); aboutData.addAuthor(i18n("Jean-Baptiste Mardelle"), i18n("MLT and KDE SC 4 / KF5 port, main developer and maintainer"), QStringLiteral("jb@kdenlive.org")); aboutData.addAuthor(i18n("Nicolas Carion"), i18n("Code re-architecture & timeline rewrite"), QStringLiteral("french.ebook.lover@gmail.com")); aboutData.addAuthor(i18n("Vincent Pinon"), i18n("KF5 port, Windows cross-build, bugs fixing"), QStringLiteral("vpinon@kde.org")); aboutData.addAuthor(i18n("Laurent Montel"), i18n("Bugs fixing, clean up code, optimization etc."), QStringLiteral("montel@kde.org")); aboutData.addAuthor(i18n("Till Theato"), i18n("Bug fixing, etc."), QStringLiteral("root@ttill.de")); aboutData.addAuthor(i18n("Simon A. Eugster"), i18n("Color scopes, bug fixing, etc."), QStringLiteral("simon.eu@gmail.com")); aboutData.addAuthor(i18n("Marco Gittler"), i18n("MLT transitions and effects, timeline, audio thumbs"), QStringLiteral("g.marco@freenet.de")); aboutData.addAuthor(i18n("Dan Dennedy"), i18n("Bug fixing, etc."), QStringLiteral("dan@dennedy.org")); aboutData.addAuthor(i18n("Alberto Villa"), i18n("Bug fixing, logo, etc."), QStringLiteral("avilla@FreeBSD.org")); aboutData.addAuthor(i18n("Jean-Michel Poure"), i18n("Rendering profiles customization"), QStringLiteral("jm@poure.com")); aboutData.addAuthor(i18n("Ray Lehtiniemi"), i18n("Bug fixing, etc."), QStringLiteral("rayl@mail.com")); aboutData.addAuthor(i18n("Steve Guilford"), i18n("Bug fixing, etc."), QStringLiteral("s.guilford@dbplugins.com")); aboutData.addAuthor(i18n("Jason Wood"), i18n("Original KDE 3 version author (not active anymore)"), QStringLiteral("jasonwood@blueyonder.co.uk")); aboutData.addCredit(i18n("Nara Oliveira and Farid Abdelnour | Estúdio Gunga"), i18n("Kdenlive 16.08 icon")); aboutData.setTranslator(i18n("NAME OF TRANSLATORS"), i18n("EMAIL OF TRANSLATORS")); aboutData.setOrganizationDomain(QByteArray("kde.org")); aboutData.setOtherText( i18n("Using:\nMLT version %1\nFFmpeg libraries", mlt_version_get_string())); aboutData.setDesktopFileName(QStringLiteral("org.kde.kdenlive")); // Register about data KAboutData::setApplicationData(aboutData); // Add rcc stored icons to the search path so that we always find our icons KIconLoader *loader = KIconLoader::global(); loader->reconfigure("kdenlive", QStringList() << QStringLiteral(":/pics")); // Set app stuff from about data app.setApplicationDisplayName(aboutData.displayName()); app.setOrganizationDomain(aboutData.organizationDomain()); app.setApplicationVersion(aboutData.version()); app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true); // Create command line parser with options QCommandLineParser parser; aboutData.setupCommandLine(&parser); parser.setApplicationDescription(aboutData.shortDescription()); parser.addVersionOption(); parser.addHelpOption(); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("config"), i18n("Set a custom config file name"), QStringLiteral("config"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mlt-path"), i18n("Set the path for MLT environment"), QStringLiteral("mlt-path"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mlt-log"), i18n("MLT log level"), QStringLiteral("verbose/debug"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("i"), i18n("Comma separated list of clips to add"), QStringLiteral("clips"))); parser.addPositionalArgument(QStringLiteral("file"), i18n("Document to open")); // Parse command line parser.process(app); aboutData.processCommandLine(&parser); #ifdef USE_DRMINGW ExcHndlInit(); #elif defined(KF5_USE_CRASH) KCrash::initialize(); #endif //auto splash = new Splash(&app); //splash->show(); //qApp->processEvents(); qmlRegisterUncreatableMetaObject(PlaylistState::staticMetaObject, // static meta object "com.enums", // import statement 1, 0, // major and minor version of the import "ClipState", // name in QML "Error: only enums"); qmlRegisterUncreatableMetaObject(ClipType::staticMetaObject, // static meta object "com.enums", // import statement 1, 0, // major and minor version of the import "ProducerType", // name in QML "Error: only enums"); if (parser.value(QStringLiteral("mlt-log")) == QStringLiteral("verbose")) { mlt_log_set_level(MLT_LOG_VERBOSE); } else if (parser.value(QStringLiteral("mlt-log")) == QStringLiteral("debug")) { mlt_log_set_level(MLT_LOG_DEBUG); } QUrl url; if (parser.positionalArguments().count() != 0) { url = QUrl::fromLocalFile(parser.positionalArguments().at(0)); // Make sure we get an absolute URL so that we can autosave correctly QString currentPath = QDir::currentPath(); QUrl startup = QUrl::fromLocalFile(currentPath.endsWith(QDir::separator()) ? currentPath : currentPath + QDir::separator()); url = startup.resolved(url); } Core::build(!parser.value(QStringLiteral("config")).isEmpty(), parser.value(QStringLiteral("mlt-path"))); pCore->initGUI(url); //delete splash; //splash->endSplash(); //qApp->processEvents(); int result = app.exec(); Core::clean(); if (result == EXIT_RESTART || result == EXIT_CLEAN_RESTART) { qCDebug(KDENLIVE_LOG) << "restarting app"; if (result == EXIT_CLEAN_RESTART) { // Delete config file KSharedConfigPtr config = KSharedConfig::openConfig(); if (config->name().contains(QLatin1String("kdenlive"))) { // Make sure we delete our config file QFile f(QStandardPaths::locate(QStandardPaths::ConfigLocation, config->name(), QStandardPaths::LocateFile)); if (f.exists()) { qDebug()<<" = = = =\nGOT Deleted file: "<start(app.applicationFilePath(), progArgs); restart->waitForReadyRead(); restart->waitForFinished(1000); result = EXIT_SUCCESS; } return result; } diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index 81060bb7f..0042b2c93 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,1055 +1,1056 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "clipcontroller.h" #include "bin/model/markerlistmodel.hpp" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "profiles/profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include #include #include std::shared_ptr ClipController::mediaUnavailable; ClipController::ClipController(const QString &clipId, const std::shared_ptr &producer) : selectedEffectIndex(1) , m_audioThumbCreated(false) , m_masterProducer(producer) , m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr) , m_usesProxy(false) , m_audioInfo(nullptr) , m_videoIndex(0) , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_hasAudio(false) , m_hasVideo(false) , m_producerLock(QReadWriteLock::Recursive) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_properties) { setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); m_service = m_properties->get("mlt_service"); QString proxy = m_properties->get("kdenlive:proxy"); QString path = m_properties->get("resource"); if (proxy.length() > 2) { if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); m_properties->set("resource", path.toUtf8().constData()); } // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative() && path != QLatin1String("")) { path.prepend(pCore->currentDoc()->documentRoot()); m_properties->set("resource", path.toUtf8().constData()); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); checkAudioVideo(); } else { m_producerLock.lockForWrite(); } } ClipController::~ClipController() { delete m_properties; m_masterProducer.reset(); } const QString ClipController::binId() const { return m_controllerBinId; } const std::unique_ptr &ClipController::audioInfo() const { return m_audioInfo; } void ClipController::addMasterProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::addmasterproducer"; QString documentRoot = pCore->currentDoc()->documentRoot(); m_masterProducer = producer; m_properties = new Mlt::Properties(m_masterProducer->get_properties()); m_producerLock.unlock(); // Pass temporary properties QMapIterator i(m_tempProps); while (i.hasNext()) { i.next(); switch(i.value().type()) { case QVariant::Int: setProducerProperty(i.key(), i.value().toInt()); break; case QVariant::Double: setProducerProperty(i.key(), i.value().toDouble()); break; default: setProducerProperty(i.key(), i.value().toString()); break; } } m_tempProps.clear(); int id = m_controllerBinId.toInt(); m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack()); if (!m_masterProducer->is_valid()) { m_masterProducer = ClipController::mediaUnavailable; qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { checkAudioVideo(); QString proxy = m_properties->get("kdenlive:proxy"); m_service = m_properties->get("mlt_service"); QString path = m_properties->get("resource"); m_usesProxy = false; if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); emitProducerChanged(m_controllerBinId, producer); setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); } connectEffectStack(); } namespace { QString producerXml(const std::shared_ptr &producer, bool includeMeta, bool includeProfile) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); if (!s.is_valid()) { return QString(); } int ignore = s.get_int("ignore_points"); if (ignore != 0) { s.set("ignore_points", 0); } c.set("time_format", "frames"); if (!includeMeta) { c.set("no_meta", 1); } if (!includeProfile) { c.set("no_profile", 1); } c.set("store", "kdenlive"); c.set("no_root", 1); c.set("root", "/"); c.connect(s); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return QString::fromUtf8(c.get("string")); } } // namespace void ClipController::getProducerXML(QDomDocument &document, bool includeMeta, bool includeProfile) { // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = producerXml(m_masterProducer, includeMeta, includeProfile); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { QReadLocker lock(&m_producerLock); date = QFileInfo(m_path).lastModified(); m_videoIndex = -1; int audioIndex = -1; // special case: playlist with a proxy clip have to be detected separately if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) { audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_videoIndex == -1) { m_clipType = ClipType::Audio; } else { if (audioIndex == -1) { m_clipType = ClipType::Video; } else { m_clipType = ClipType::AV; } if (m_service == QLatin1String("avformat")) { m_properties->set("mlt_service", "avformat-novalidate"); + m_properties->set("mute_on_pause", 0); } } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { m_clipType = ClipType::SlideShow; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Image; m_hasLimitedDuration = false; } } else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) { m_clipType = ClipType::Color; m_hasLimitedDuration = false; } else if (m_service == QLatin1String("kdenlivetitle")) { if (!m_path.isEmpty()) { m_clipType = ClipType::TextTemplate; } else { m_clipType = ClipType::Text; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("webvfx")) { m_clipType = ClipType::WebVfx; } else if (m_service == QLatin1String("qtext")) { m_clipType = ClipType::QText; } else if (m_service == QLatin1String("blipflash")) { // Mostly used for testing m_clipType = ClipType::AV; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Unknown; } if (audioIndex > -1 || m_clipType == ClipType::Playlist) { m_audioInfo = std::make_unique(m_masterProducer, audioIndex); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->frames_to_time(m_masterProducer->get_playtime(), mlt_time_clock)); m_masterProducer->set("out", m_masterProducer->frames_to_time(m_masterProducer->get_length() - 1, mlt_time_clock)); } } } bool ClipController::hasLimitedDuration() const { return m_hasLimitedDuration; } void ClipController::forceLimitedDuration() { m_hasLimitedDuration = true; } std::shared_ptr ClipController::originalProducer() { QReadLocker lock(&m_producerLock); return m_masterProducer; } Mlt::Producer *ClipController::masterProducer() { return new Mlt::Producer(*m_masterProducer); } bool ClipController::isValid() { if (m_masterProducer == nullptr) { return false; } return m_masterProducer->is_valid(); } // static const char *ClipController::getPassPropertiesList(bool passLength) { if (!passLength) { return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,file_hash,autorotate,xmldata,video_index,audio_index,set.test_image,set.test_audio"; } return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length,video_index,audio_index,set.test_image,set.test_audio"; } QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix) { QReadLocker lock(&m_producerLock); Mlt::Properties subProperties; subProperties.pass_values(*m_properties, prefix.toUtf8().constData()); QMap subclipsData; for (int i = 0; i < subProperties.count(); i++) { subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i)); } return subclipsData; } void ClipController::updateProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::updateProducer"; // TODO replace all track producers if (!m_properties) { // producer has not been initialized return addMasterProducer(producer); } m_producerLock.lockForWrite(); Mlt::Properties passProperties; // Keep track of necessary properties QString proxy = producer->get("kdenlive:proxy"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property m_usesProxy = true; } else { m_usesProxy = false; } // When resetting profile, duration can change so we invalidate it to 0 in that case int length = m_properties->get_int("length"); const char *passList = getPassPropertiesList(m_usesProxy && length > 0); // This is necessary as some properties like set.test_audio are reset on producer creation passProperties.pass_list(*m_properties, passList); delete m_properties; *m_masterProducer = producer.get(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); m_producerLock.unlock(); checkAudioVideo(); // Pass properties from previous producer m_properties->pass_list(passProperties, passList); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_effectStack->resetService(m_masterProducer); emitProducerChanged(m_controllerBinId, producer); // URL and name should not be updated otherwise when proxying a clip we cannot find back the original url /*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource")); if (m_url.isValid()) { m_name = m_url.fileName(); } */ } qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource"); } const QString ClipController::getStringDuration() { QReadLocker lock(&m_producerLock); if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime > 0) { return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df)); } return m_properties->frames_to_time(m_masterProducer->get_length(), mlt_time_smpte_df); } return i18n("Unknown"); } int ClipController::getProducerDuration() const { QReadLocker lock(&m_producerLock); if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { return playtime = m_masterProducer->get_length(); } return playtime; } return -1; } char *ClipController::framesToTime(int frames) const { QReadLocker lock(&m_producerLock); if (m_masterProducer) { return m_masterProducer->frames_to_time(frames, mlt_time_clock); } return nullptr; } GenTime ClipController::getPlaytime() const { QReadLocker lock(&m_producerLock); if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return {m_masterProducer->get_playtime(), fps}; } int ClipController::getFramePlaytime() const { QReadLocker lock(&m_producerLock); if (!m_masterProducer || !m_masterProducer->is_valid()) { return 0; } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return playtime == 0 ? m_masterProducer->get_playtime() : playtime; } return m_masterProducer->get_playtime(); } QString ClipController::getProducerProperty(const QString &name) const { QReadLocker lock(&m_producerLock); if (m_properties == nullptr) { return QString(); } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get(correctedName.toUtf8().constData()); } return QString(m_properties->get(name.toUtf8().constData())); } int ClipController::getProducerIntProperty(const QString &name) const { QReadLocker lock(&m_producerLock); if (!m_properties) { return 0; } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get_int(correctedName.toUtf8().constData()); } return m_properties->get_int(name.toUtf8().constData()); } qint64 ClipController::getProducerInt64Property(const QString &name) const { QReadLocker lock(&m_producerLock); if (!m_properties) { return 0; } return m_properties->get_int64(name.toUtf8().constData()); } double ClipController::getProducerDoubleProperty(const QString &name) const { QReadLocker lock(&m_producerLock); if (!m_properties) { return 0; } return m_properties->get_double(name.toUtf8().constData()); } QColor ClipController::getProducerColorProperty(const QString &name) const { QReadLocker lock(&m_producerLock); if (!m_properties) { return {}; } mlt_color color = m_properties->get_color(name.toUtf8().constData()); return QColor::fromRgb(color.r, color.g, color.b); } QMap ClipController::currentProperties(const QMap &props) { QMap currentProps; QMap::const_iterator i = props.constBegin(); while (i != props.constEnd()) { currentProps.insert(i.key(), getProducerProperty(i.key())); ++i; } return currentProps; } double ClipController::originalFps() const { QReadLocker lock(&m_producerLock); if (!m_properties) { return 0; } QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex); return m_properties->get_double(propertyName.toUtf8().constData()); } QString ClipController::videoCodecProperty(const QString &property) const { QReadLocker lock(&m_producerLock); if (!m_properties) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::codec(bool audioCodec) const { QReadLocker lock(&m_producerLock); if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_properties->get_int("audio_index") : m_videoIndex); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::clipUrl() const { return m_path; } bool ClipController::sourceExists() const { if (m_clipType == ClipType::Color || m_clipType == ClipType::Text) { return true; } if (m_clipType == ClipType::SlideShow) { //TODO return true; } return QFile::exists(m_path); } QString ClipController::clipName() const { QString name = getProducerProperty(QStringLiteral("kdenlive:clipname")); if (!name.isEmpty()) { return name; } return QFileInfo(m_path).fileName(); } QString ClipController::description() const { if (m_clipType == ClipType::TextTemplate) { QString name = getProducerProperty(QStringLiteral("templatetext")); return name; } QString name = getProducerProperty(QStringLiteral("kdenlive:description")); if (!name.isEmpty()) { return name; } return getProducerProperty(QStringLiteral("meta.attr.comment.markup")); } QString ClipController::serviceName() const { return m_service; } void ClipController::setProducerProperty(const QString &name, int value) { if (!m_masterProducer) { m_tempProps.insert(name, value); return; } QWriteLocker lock(&m_producerLock); m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, double value) { if (!m_masterProducer) { m_tempProps.insert(name, value); return; } QWriteLocker lock(&m_producerLock); m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, const QString &value) { if (!m_masterProducer) { m_tempProps.insert(name, value); return; } QWriteLocker lock(&m_producerLock); if (value.isEmpty()) { m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } else { m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData()); } } void ClipController::resetProducerProperty(const QString &name) { if (!m_masterProducer) { m_tempProps.insert(name, QString()); return; } QWriteLocker lock(&m_producerLock); m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType::ProducerType ClipController::clipType() const { return m_clipType; } const QSize ClipController::getFrameSize() const { QReadLocker lock(&m_producerLock); if (m_masterProducer == nullptr) { return QSize(); } int width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } int height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } return QSize(width, height); } bool ClipController::hasAudio() const { return m_hasAudio; } void ClipController::checkAudioVideo() { QReadLocker lock(&m_producerLock); m_masterProducer->seek(0); if (m_masterProducer->get_int("_placeholder") == 1 || m_masterProducer->get("text") == QLatin1String("INVALID")) { // This is a placeholder file, try to guess from its properties QString orig_service = m_masterProducer->get("kdenlive:orig_service"); if (orig_service.startsWith(QStringLiteral("avformat")) || (m_masterProducer->get_int("audio_index") + m_masterProducer->get_int("video_index") > 0)) { m_hasAudio = m_masterProducer->get_int("audio_index") >= 0; m_hasVideo = m_masterProducer->get_int("video_index") >= 0; } else { // Assume image or text producer m_hasAudio = false; m_hasVideo = true; } return; } QScopedPointer frame(m_masterProducer->get_frame()); if (frame->is_valid()) { // test_audio returns 1 if there is NO audio (strange but true at the time this code is written) m_hasAudio = frame->get_int("test_audio") == 0; m_hasVideo = frame->get_int("test_image") == 0; } else { qDebug()<<"* * * *ERROR INVALID FRAME On test"; } } bool ClipController::hasVideo() const { return m_hasVideo; } PlaylistState::ClipState ClipController::defaultState() const { if (hasVideo()) { return PlaylistState::VideoOnly; } if (hasAudio()) { return PlaylistState::AudioOnly; } return PlaylistState::Disabled; } QPixmap ClipController::pixmap(int framePosition, int width, int height) { // TODO refac this should use the new thumb infrastructure QReadLocker lock(&m_producerLock); m_masterProducer->seek(framePosition); Mlt::Frame *frame = m_masterProducer->get_frame(); if (frame == nullptr || !frame->is_valid()) { QPixmap p(width, height); p.fill(QColor(Qt::red).rgb()); return p; } frame->set("rescale.interp", "bilinear"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (width == 0) { width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } } if (height == 0) { height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } } // int ow = frameWidth; // int oh = height; mlt_image_format format = mlt_image_rgb24a; width += width % 2; height += height % 2; const uchar *imagedata = frame->get_image(format, width, height); QImage image(imagedata, width, height, QImage::Format_RGBA8888); QPixmap pixmap; pixmap.convertFromImage(image); delete frame; return pixmap; } void ClipController::setZone(const QPoint &zone) { setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x()); setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y()); } QPoint ClipController::zone() const { int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int max = getFramePlaytime() - 1; int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max); if (out <= in) { out = max; } QPoint zone(in, out); return zone; } const QString ClipController::getClipHash() const { return getProducerProperty(QStringLiteral("kdenlive:file_hash")); } Mlt::Properties &ClipController::properties() { QReadLocker lock(&m_producerLock); return *m_properties; } void ClipController::backupOriginalProperties() { QReadLocker lock(&m_producerLock); if (m_properties->get_int("kdenlive:original.backup") == 1) { return; } int propsCount = m_properties->count(); // store original props QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")}; for (int j = 0; j < propsCount; j++) { QString propName = m_properties->get_name(j); if (doNotPass.contains(propName)) { continue; } if (!propName.startsWith(QLatin1Char('_'))) { propName.prepend(QStringLiteral("kdenlive:original.")); m_properties->set(propName.toUtf8().constData(), m_properties->get(j)); } } m_properties->set("kdenlive:original.backup", 1); } void ClipController::clearBackupProperties() { QReadLocker lock(&m_producerLock); if (m_properties->get_int("kdenlive:original.backup") == 0) { return; } int propsCount = m_properties->count(); // clear original props QStringList passProps; for (int j = 0; j < propsCount; j++) { QString propName = m_properties->get_name(j); if (propName.startsWith(QLatin1String("kdenlive:original."))) { passProps << propName; } } for (const QString &p : passProps) { m_properties->set(p.toUtf8().constData(), (char *)nullptr); } m_properties->set("kdenlive:original.backup", (char *)nullptr); } void ClipController::mirrorOriginalProperties(Mlt::Properties &props) { QReadLocker lock(&m_producerLock); if (m_usesProxy && QFileInfo(m_properties->get("resource")).fileName() == QFileInfo(m_properties->get("kdenlive:proxy")).fileName()) { // This is a proxy, we need to use the real source properties if (m_properties->get_int("kdenlive:original.backup") == 0) { // We have a proxy clip, load original source producer std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData()); // Get frame to make sure we retrieve all original props std::shared_ptr fr(prod->get_frame()); if (!prod->is_valid()) { return; } int width = 0; int height = 0; mlt_image_format format = mlt_image_none; fr->get_image(format, width, height); Mlt::Properties sourceProps(prod->get_properties()); props.inherit(sourceProps); int propsCount = sourceProps.count(); // store original props QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")}; for (int i = 0; i < propsCount; i++) { QString propName = sourceProps.get_name(i); if (doNotPass.contains(propName)) { continue; } if (!propName.startsWith(QLatin1Char('_'))) { propName.prepend(QStringLiteral("kdenlive:original.")); m_properties->set(propName.toUtf8().constData(), sourceProps.get(i)); } } m_properties->set("kdenlive:original.backup", 1); } // Properties were fetched in the past, reuse Mlt::Properties sourceProps; sourceProps.pass_values(*m_properties, "kdenlive:original."); props.inherit(sourceProps); } else { if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio) { // Make sure that a frame / image was fetched to initialize all meta properties QString progressive = m_properties->get("meta.media.progressive"); if (progressive.isEmpty()) { // Fetch a frame to initialize required properties QScopedPointer tmpProd(nullptr); if (KdenliveSettings::gpu_accel()) { QString service = m_masterProducer->get("mlt_service"); tmpProd.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), m_masterProducer->get("resource"))); } std::shared_ptr fr(tmpProd ? tmpProd->get_frame() : m_masterProducer->get_frame()); mlt_image_format format = mlt_image_none; int width = 0; int height = 0; fr->get_image(format, width, height); } } props.inherit(*m_properties); } } void ClipController::addEffect(QDomElement &xml) { Q_UNUSED(xml) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); // Add effect to list and setup a kdenlive_ix value int kdenlive_ix = 0; for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); int ix = effect->get_int("kdenlive_ix"); if (ix > kdenlive_ix) { kdenlive_ix = ix; } } kdenlive_ix++; xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix); EffectsParameterList params = EffectsController::getEffectArgs(xml); EffectManager effect(service); effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps())); if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::removeEffect(int effectIndex, bool delayRefresh) { Q_UNUSED(effectIndex) Q_UNUSED(delayRefresh) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.removeEffect(effectIndex, true); if (!delayRefresh) { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } */ } void ClipController::moveEffect(int oldPos, int newPos) { Q_UNUSED(oldPos) Q_UNUSED(newPos) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.moveEffect(oldPos, newPos); */ } int ClipController::effectsCount() { int count = 0; QReadLocker lock(&m_producerLock); Mlt::Service service(m_masterProducer->parent()); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QString id = effect->get("kdenlive_id"); if (!id.isEmpty()) { count++; } } return count; } void ClipController::changeEffectState(const QList &indexes, bool disable) { Q_UNUSED(indexes) Q_UNUSED(disable) // TODO refac : this must be rewritten /* Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) { effect->set("disable", (int)disable); } } if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::updateEffect(const QDomElement &e, int ix) { Q_UNUSED(e) Q_UNUSED(ix) // TODO refac : this must be rewritten /* QString tag = e.attribute(QStringLiteral("id")); if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) { // this filters cannot be edited, remove and re-add it removeEffect(ix, true); QDomElement clone = e.cloneNode().toElement(); addEffect(clone); return; } EffectsParameterList params = EffectsController::getEffectArgs(e); Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) { continue; } service.lock(); QString prefix; QString ser = effect->get("mlt_service"); if (ser == QLatin1String("region")) { prefix = QStringLiteral("filter0."); } for (int j = 0; j < params.count(); ++j) { effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData()); // qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId); // slotRefreshTracks(); */ } bool ClipController::hasEffects() const { return m_effectStack->rowCount() > 0; } void ClipController::setBinEffectsEnabled(bool enabled) { m_effectStack->setEffectStackEnabled(enabled); } void ClipController::saveZone(QPoint zone, const QDir &dir) { QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt")); if (dir.exists(path)) { // TODO ask for overwrite } Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); QReadLocker lock(&m_producerLock); Mlt::Producer prod(m_masterProducer->get_producer()); Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y()); Mlt::Playlist list(pCore->getCurrentProfile()->profile()); list.insert_at(0, *prod2, 0); // list.set("title", desc.toUtf8().constData()); xmlConsumer.connect(list); xmlConsumer.run(); delete prod2; } std::shared_ptr ClipController::getEffectStack() const { return m_effectStack; } bool ClipController::addEffect(const QString &effectId) { return m_effectStack->appendEffect(effectId, true); } bool ClipController::copyEffect(const std::shared_ptr &stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), !m_hasAudio ? PlaylistState::VideoOnly : !m_hasVideo ? PlaylistState::AudioOnly : PlaylistState::Disabled); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } void ClipController::refreshAudioInfo() { if (m_audioInfo && m_masterProducer) { QReadLocker lock(&m_producerLock); m_audioInfo->setAudioIndex(m_masterProducer, m_properties->get_int("audio_index")); } } QList ClipController::audioStreams() const { if (m_audioInfo) { return m_audioInfo->streams(); } return {}; } int ClipController::audioStreamsCount() const { if (m_audioInfo) { return m_audioInfo->streams().count(); } return 0; } diff --git a/src/monitor/glwidget.cpp b/src/monitor/glwidget.cpp index f4e2f3d31..446276ddf 100644 --- a/src/monitor/glwidget.cpp +++ b/src/monitor/glwidget.cpp @@ -1,1888 +1,1884 @@ /* * Copyright (c) 2011-2016 Meltytech, LLC * Original author: Dan Dennedy * Modified for Kdenlive: Jean-Baptiste Mardelle * * GL shader based on BSD licensed code from Peter Bengtsson: * http://www.fourcc.org/source/YUV420P-OpenGL-GLSLang.c * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include "core.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "timeline2/view/qml/timelineitems.h" #include #ifndef GL_UNPACK_ROW_LENGTH #ifdef GL_UNPACK_ROW_LENGTH_EXT #define GL_UNPACK_ROW_LENGTH GL_UNPACK_ROW_LENGTH_EXT #else #error GL_UNPACK_ROW_LENGTH undefined #endif #endif #ifdef QT_NO_DEBUG #define check_error(fn) \ { \ } #else #define check_error(fn) \ { \ uint err = fn->glGetError(); \ if (err != GL_NO_ERROR) { \ qCCritical(KDENLIVE_LOG) << "GL error" << hex << err << dec << "at" << __FILE__ << ":" << __LINE__; \ } \ } #endif #ifndef GL_TIMEOUT_IGNORED #define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull #endif using namespace Mlt; GLWidget::GLWidget(int id, QObject *parent) : QQuickView((QWindow *)parent) , sendFrameForAnalysis(false) , m_glslManager(nullptr) , m_consumer(nullptr) , m_producer(nullptr) , m_id(id) , m_rulerHeight(QFontMetrics(QApplication::font()).lineSpacing() * 0.7) , m_shader(nullptr) , m_initSem(0) , m_analyseSem(1) , m_isInitialized(false) , m_threadStartEvent(nullptr) , m_threadStopEvent(nullptr) , m_threadCreateEvent(nullptr) , m_threadJoinEvent(nullptr) , m_displayEvent(nullptr) , m_frameRenderer(nullptr) , m_projectionLocation(0) , m_modelViewLocation(0) , m_vertexLocation(0) , m_texCoordLocation(0) , m_colorspaceLocation(0) , m_zoom(1.0f) + , m_profileSize(1920, 1080) + , m_colorSpace(601) + , m_dar(1.78) , m_sendFrame(false) , m_isZoneMode(false) , m_isLoopMode(false) , m_offset(QPoint(0, 0)) , m_fbo(nullptr) , m_shareContext(nullptr) , m_openGLSync(false) , m_ClientWaitSync(nullptr) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); #endif m_texture[0] = m_texture[1] = m_texture[2] = 0; qRegisterMetaType("Mlt::Frame"); qRegisterMetaType("SharedFrame"); setPersistentOpenGLContext(true); setPersistentSceneGraph(true); setClearBeforeRendering(false); setResizeMode(QQuickView::SizeRootObjectToView); m_offscreenSurface.setFormat(QWindow::format()); m_offscreenSurface.create(); m_refreshTimer.setSingleShot(true); m_refreshTimer.setInterval(50); - m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:black")); + m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:0")); m_blackClip->set("kdenlive:id", "black"); m_blackClip->set("out", 3); connect(&m_refreshTimer, &QTimer::timeout, this, &GLWidget::refresh); m_producer = m_blackClip; rootContext()->setContextProperty("markersModel", 0); if (!initGPUAccel()) { disableGPUAccel(); } connect(this, &QQuickWindow::sceneGraphInitialized, this, &GLWidget::initializeGL, Qt::DirectConnection); connect(this, &QQuickWindow::beforeRendering, this, &GLWidget::paintGL, Qt::DirectConnection); registerTimelineItems(); m_proxy = new MonitorProxy(this); connect(m_proxy, &MonitorProxy::seekRequestChanged, this, &GLWidget::requestSeek); rootContext()->setContextProperty("controller", m_proxy); } GLWidget::~GLWidget() { // C & D delete m_glslManager; delete m_threadStartEvent; delete m_threadStopEvent; delete m_threadCreateEvent; delete m_threadJoinEvent; delete m_displayEvent; if (m_frameRenderer) { if (m_frameRenderer->isRunning()) { QMetaObject::invokeMethod(m_frameRenderer, "cleanup"); m_frameRenderer->quit(); m_frameRenderer->wait(); m_frameRenderer->deleteLater(); } else { delete m_frameRenderer; } } m_blackClip.reset(); delete m_shareContext; delete m_shader; // delete pCore->getCurrentProfile(); } void GLWidget::updateAudioForAnalysis() { if (m_frameRenderer) { m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); } } void GLWidget::initializeGL() { if (m_isInitialized || !isVisible() || (openglContext() == nullptr)) return; openglContext()->makeCurrent(&m_offscreenSurface); initializeOpenGLFunctions(); qCDebug(KDENLIVE_LOG) << "OpenGL vendor: " << QString::fromUtf8((const char *)glGetString(GL_VENDOR)); qCDebug(KDENLIVE_LOG) << "OpenGL renderer: " << QString::fromUtf8((const char *)glGetString(GL_RENDERER)); qCDebug(KDENLIVE_LOG) << "OpenGL Threaded: " << openglContext()->supportsThreadedOpenGL(); qCDebug(KDENLIVE_LOG) << "OpenGL ARG_SYNC: " << openglContext()->hasExtension("GL_ARB_sync"); qCDebug(KDENLIVE_LOG) << "OpenGL OpenGLES: " << openglContext()->isOpenGLES(); // C & D if (onlyGLESGPUAccel()) { disableGPUAccel(); } createShader(); m_openGLSync = initGPUAccelSync(); // C & D if (m_glslManager) { // Create a context sharing with this context for the RenderThread context. // This is needed because openglContext() is active in another thread // at the time that RenderThread is created. // See this Qt bug for more info: https://bugreports.qt.io/browse/QTBUG-44677 // TODO: QTBUG-44677 is closed. still applicable? m_shareContext = new QOpenGLContext; m_shareContext->setFormat(openglContext()->format()); m_shareContext->setShareContext(openglContext()); m_shareContext->create(); } m_frameRenderer = new FrameRenderer(openglContext(), &m_offscreenSurface, m_ClientWaitSync); m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); openglContext()->makeCurrent(this); - // openglContext()->blockSignals(false); - connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::frameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::textureReady, this, &GLWidget::updateTexture, Qt::DirectConnection); connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::onFrameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::audioSamplesSignal, this, &GLWidget::audioSamplesSignal, Qt::QueuedConnection); m_initSem.release(); m_isInitialized = true; reconfigure(); } void GLWidget::resizeGL(int width, int height) { int x, y, w, h; height -= m_rulerHeight; double this_aspect = (double)width / height; - double video_aspect = pCore->getCurrentProfile()->dar(); // Special case optimization to negate odd effect of sample aspect ratio // not corresponding exactly with image resolution. - if ((int)(this_aspect * 1000) == (int)(video_aspect * 1000)) { + if ((int)(this_aspect * 1000) == (int)(m_dar * 1000)) { w = width; h = height; } // Use OpenGL to normalise sample aspect ratio - else if (height * video_aspect > width) { + else if (height * m_dar > width) { w = width; - h = width / video_aspect; + h = width / m_dar; } else { - w = height * video_aspect; + w = height * m_dar; h = height; } x = (width - w) / 2; y = (height - h) / 2; m_rect.setRect(x, y, w, h); - double scalex = (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom; + double scalex = (double)m_rect.width() / m_profileSize.width() * m_zoom; double scaley = (double)m_rect.width() / - ((double)pCore->getCurrentProfile()->height() * pCore->getCurrentProfile()->dar() / pCore->getCurrentProfile()->width()) / - pCore->getCurrentProfile()->width() * m_zoom; + ((double)m_profileSize.height() * m_dar / m_profileSize.width()) / + m_profileSize.width() * m_zoom; QPoint center = m_rect.center(); QQuickItem *rootQml = rootObject(); if (rootQml) { rootQml->setProperty("center", center); rootQml->setProperty("scalex", scalex); rootQml->setProperty("scaley", scaley); if (rootQml->objectName() == QLatin1String("rootsplit")) { // Adjust splitter pos rootQml->setProperty("splitterPos", x + (rootQml->property("realpercent").toDouble() * w)); } } emit rectChanged(); } void GLWidget::resizeEvent(QResizeEvent *event) { resizeGL(event->size().width(), event->size().height()); QQuickView::resizeEvent(event); } void GLWidget::createGPUAccelFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D tex;" "varying highp vec2 coordinates;" "void main(void) {" " gl_FragColor = texture2D(tex, coordinates);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("tex"); } void GLWidget::createShader() { m_shader = new QOpenGLShaderProgram; m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, "uniform highp mat4 projection;" "uniform highp mat4 modelView;" "attribute highp vec4 vertex;" "attribute highp vec2 texCoord;" "varying highp vec2 coordinates;" "void main(void) {" " gl_Position = projection * modelView * vertex;" " coordinates = texCoord;" "}"); // C & D if (m_glslManager) { createGPUAccelFragmentProg(); } else { // A & B createYUVTextureProjectFragmentProg(); } m_projectionLocation = m_shader->uniformLocation("projection"); m_modelViewLocation = m_shader->uniformLocation("modelView"); m_vertexLocation = m_shader->attributeLocation("vertex"); m_texCoordLocation = m_shader->attributeLocation("texCoord"); } void GLWidget::createYUVTextureProjectFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D Ytex, Utex, Vtex;" "uniform lowp int colorspace;" "varying highp vec2 coordinates;" "void main(void) {" " mediump vec3 texel;" " texel.r = texture2D(Ytex, coordinates).r - 0.0625;" // Y " texel.g = texture2D(Utex, coordinates).r - 0.5;" // U " texel.b = texture2D(Vtex, coordinates).r - 0.5;" // V " mediump mat3 coefficients;" " if (colorspace == 601) {" " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.39173, 2.017," // column 2 " 1.5958, -0.8129, 0.0);" // column 3 " } else {" // ITU-R 709 " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.213, 2.112," // column 2 " 1.793, -0.533, 0.0);" // column 3 " }" " gl_FragColor = vec4(coefficients * texel, 1.0);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("Ytex"); m_textureLocation[1] = m_shader->uniformLocation("Utex"); m_textureLocation[2] = m_shader->uniformLocation("Vtex"); m_colorspaceLocation = m_shader->uniformLocation("colorspace"); } static void uploadTextures(QOpenGLContext *context, const SharedFrame &frame, GLuint texture[]) { int width = frame.get_image_width(); int height = frame.get_image_height(); const uint8_t *image = frame.get_image(); QOpenGLFunctions *f = context->functions(); // The planes of pixel data may not be a multiple of the default 4 bytes. f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Upload each plane of YUV to a texture. if (texture[0] != 0u) { f->glDeleteTextures(3, texture); } check_error(f); f->glGenTextures(3, texture); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[0]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[1]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[2]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height + width / 2 * height / 2); check_error(f); } void GLWidget::clear() { stopGlsl(); update(); } void GLWidget::releaseAnalyse() { m_analyseSem.release(); } bool GLWidget::acquireSharedFrameTextures() { // A if ((m_glslManager == nullptr) && !openglContext()->supportsThreadedOpenGL()) { QMutexLocker locker(&m_contextSharedAccess); if (!m_sharedFrame.is_valid()) { return false; } uploadTextures(openglContext(), m_sharedFrame, m_texture); } else if (m_glslManager) { // C & D m_contextSharedAccess.lock(); if (m_sharedFrame.is_valid()) { m_texture[0] = *((const GLuint *)m_sharedFrame.get_image()); } } if (!m_texture[0]) { // C & D if (m_glslManager) m_contextSharedAccess.unlock(); return false; } return true; } void GLWidget::bindShaderProgram() { m_shader->bind(); // C & D if (m_glslManager) { m_shader->setUniformValue(m_textureLocation[0], 0); } else { // A & B m_shader->setUniformValue(m_textureLocation[0], 0); m_shader->setUniformValue(m_textureLocation[1], 1); m_shader->setUniformValue(m_textureLocation[2], 2); - m_shader->setUniformValue(m_colorspaceLocation, pCore->getCurrentProfile()->colorspace()); + m_shader->setUniformValue(m_colorspaceLocation, m_colorSpace); } } void GLWidget::releaseSharedFrameTextures() { // C & D if (m_glslManager) { glFinish(); m_contextSharedAccess.unlock(); } } bool GLWidget::initGPUAccel() { if (!KdenliveSettings::gpu_accel()) return false; m_glslManager = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "glsl.manager"); return m_glslManager->is_valid(); } // C & D // TODO: insure safe, idempotent on all pipelines. void GLWidget::disableGPUAccel() { delete m_glslManager; m_glslManager = nullptr; KdenliveSettings::setGpu_accel(false); // Need to destroy MLT global reference to prevent filters from trying to use GPU. mlt_properties_set_data(mlt_global_properties(), "glslManager", nullptr, 0, nullptr, nullptr); emit gpuNotSupported(); } bool GLWidget::onlyGLESGPUAccel() const { return (m_glslManager != nullptr) && openglContext()->isOpenGLES(); } #if defined(Q_OS_WIN) bool GLWidget::initGPUAccelSync() { // no-op // TODO: getProcAddress is not working on Windows? return false; } #else bool GLWidget::initGPUAccelSync() { if (!KdenliveSettings::gpu_accel()) return false; if (m_glslManager == nullptr) return false; if (!openglContext()->hasExtension("GL_ARB_sync")) return false; m_ClientWaitSync = (ClientWaitSync_fp)openglContext()->getProcAddress("glClientWaitSync"); if (m_ClientWaitSync) { return true; } else { qCDebug(KDENLIVE_LOG) << " / / // NO GL SYNC, ERROR"; // fallback on A || B // TODO: fallback on A || B || C? disableGPUAccel(); return false; } } #endif void GLWidget::paintGL() { QOpenGLFunctions *f = openglContext()->functions(); int width = this->width() * devicePixelRatio(); int height = this->height() * devicePixelRatio(); f->glDisable(GL_BLEND); f->glDisable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glViewport(0, (m_rulerHeight * devicePixelRatio() * 0.5 + 0.5), width, height); check_error(f); QColor color(KdenliveSettings::window_background()); f->glClearColor(color.redF(), color.greenF(), color.blueF(), color.alphaF()); f->glClear(GL_COLOR_BUFFER_BIT); check_error(f); if (!acquireSharedFrameTextures()) return; // Bind textures. for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, m_texture[i]); check_error(f); } } bindShaderProgram(); check_error(f); // Setup an orthographic projection. QMatrix4x4 projection; projection.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection); check_error(f); // Set model view. QMatrix4x4 modelView; if (!qFuzzyCompare(m_zoom, 1.0f)) { if ((offset().x() != 0) || (offset().y() != 0)) modelView.translate(-offset().x() * devicePixelRatio(), offset().y() * devicePixelRatio()); modelView.scale(zoom(), zoom()); } m_shader->setUniformValue(m_modelViewLocation, modelView); check_error(f); // Provide vertices of triangle strip. QVector vertices; width = m_rect.width() * devicePixelRatio(); height = m_rect.height() * devicePixelRatio(); vertices << QVector2D(float(-width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(-width) / 2.0f, float(height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(height) / 2.0f); m_shader->enableAttributeArray(m_vertexLocation); check_error(f); m_shader->setAttributeArray(m_vertexLocation, vertices.constData()); check_error(f); // Provide texture coordinates. QVector texCoord; texCoord << QVector2D(0.0f, 1.0f); texCoord << QVector2D(0.0f, 0.0f); texCoord << QVector2D(1.0f, 1.0f); texCoord << QVector2D(1.0f, 0.0f); m_shader->enableAttributeArray(m_texCoordLocation); check_error(f); m_shader->setAttributeArray(m_texCoordLocation, texCoord.constData()); check_error(f); // Render glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); if (m_sendFrame && m_analyseSem.tryAcquire(1)) { // Render RGB frame for analysis - int fullWidth = pCore->getCurrentProfile()->width(); - int fullHeight = pCore->getCurrentProfile()->height(); - if ((m_fbo == nullptr) || m_fbo->size() != QSize(fullWidth, fullHeight)) { + if ((m_fbo == nullptr) || m_fbo->size() != m_profileSize) { delete m_fbo; QOpenGLFramebufferObjectFormat fmt; fmt.setSamples(1); fmt.setInternalTextureFormat(GL_RGB); // GL_RGBA32F); // which one is the fastest ? - m_fbo = new QOpenGLFramebufferObject(fullWidth, fullHeight, fmt); // GL_TEXTURE_2D); + m_fbo = new QOpenGLFramebufferObject(m_profileSize.height(), m_profileSize.height(), fmt); // GL_TEXTURE_2D); } m_fbo->bind(); - glViewport(0, 0, fullWidth, fullHeight); + glViewport(0, 0, m_profileSize.width(), m_profileSize.height()); QMatrix4x4 projection2; projection2.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection2); glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); m_fbo->release(); emit analyseFrame(m_fbo->toImage()); m_sendFrame = false; } // Cleanup m_shader->disableAttributeArray(m_vertexLocation); m_shader->disableAttributeArray(m_texCoordLocation); m_shader->release(); for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); } } glActiveTexture(GL_TEXTURE0); check_error(f); releaseSharedFrameTextures(); check_error(f); } void GLWidget::slotZoom(bool zoomIn) { if (zoomIn) { if (qFuzzyCompare(m_zoom, 1.0f)) { setZoom(2.0f); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(3.0f); } else if (m_zoom < 1.0f) { setZoom(m_zoom * 2); } } else { if (qFuzzyCompare(m_zoom, 3.0f)) { setZoom(2.0); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(1.0); } else if (m_zoom > 0.2) { setZoom(m_zoom / 2); } } } void GLWidget::wheelEvent(QWheelEvent *event) { if (((event->modifiers() & Qt::ControlModifier) != 0u) && ((event->modifiers() & Qt::ShiftModifier) != 0u)) { slotZoom(event->delta() > 0); return; } emit mouseSeek(event->delta(), (uint)event->modifiers()); event->accept(); } void GLWidget::requestSeek() { if (!m_producer) { return; } if (m_proxy->seeking()) { m_producer->seek(m_proxy->seekPosition()); if (!qFuzzyIsNull(m_producer->get_speed())) { m_consumer->purge(); } if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); } } void GLWidget::seek(int pos) { if (!m_proxy->seeking()) { m_proxy->setSeekPosition(pos); m_producer->seek(pos); if (m_consumer->is_stopped()) { m_consumer->start(); } else { m_consumer->purge(); m_consumer->set("refresh", 1); } } else { m_proxy->setSeekPosition(pos); } } void GLWidget::requestRefresh() { if (m_proxy->seeking()) { return; } if (m_producer && qFuzzyIsNull(m_producer->get_speed())) { m_refreshTimer.start(); } } QString GLWidget::frameToTime(int frames) const { return m_consumer ? m_consumer->frames_to_time(frames, mlt_time_smpte_df) : QStringLiteral("-"); } void GLWidget::refresh() { m_refreshTimer.stop(); if (m_proxy->seeking()) { return; } QMutexLocker locker(&m_mltMutex); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); } bool GLWidget::checkFrameNumber(int pos, int offset) { emit consumerPosition(pos); if (!m_proxy->setPosition(pos)) { emit seekPosition(m_proxy->seekOrCurrentPosition()); } const double speed = m_producer->get_speed(); if (m_proxy->seeking()) { m_producer->set_speed(0); m_producer->seek(m_proxy->seekPosition()); if (qFuzzyIsNull(speed)) { m_consumer->set("refresh", 1); } else { m_producer->set_speed(speed); } return true; } int maxPos = m_producer->get_int("out"); if (m_isLoopMode || m_isZoneMode) { if (qFuzzyIsNull(speed) && pos >= maxPos) { m_consumer->purge(); if (!m_isLoopMode) { return false; } m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(1.0); m_consumer->set("refresh", 1); return true; } return true; } else if (!qFuzzyIsNull(speed)) { maxPos -= offset; if (pos >= (maxPos - 1) && speed > 0.) { // Playing past last clip, pause m_producer->set_speed(0); m_consumer->set("refresh", 0); m_consumer->purge(); m_producer->seek(qMax(0, maxPos)); return false; } else if (pos <= 0 && speed < 0.) { // rewinding reached 0, pause m_producer->set_speed(0); m_consumer->set("refresh", 0); m_consumer->purge(); m_producer->seek(0); return false; } } return true; } void GLWidget::mousePressEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mousePressEvent(event); return; } if ((event->button() & Qt::LeftButton) != 0u) { if ((event->modifiers() & Qt::ControlModifier) != 0u) { // Pan view m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } else { m_dragStart = event->pos(); } } else if ((event->button() & Qt::RightButton) != 0u) { emit showContextMenu(event->globalPos()); } else if ((event->button() & Qt::MiddleButton) != 0u) { m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } event->accept(); QQuickView::mousePressEvent(event); } void GLWidget::mouseMoveEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mouseMoveEvent(event); return; } /* if (event->modifiers() == Qt::ShiftModifier && m_producer) { emit seekTo(m_producer->get_length() * event->x() / width()); return; }*/ QQuickView::mouseMoveEvent(event); if (!m_panStart.isNull()) { emit panView(m_panStart - event->pos()); m_panStart = event->pos(); event->accept(); QQuickView::mouseMoveEvent(event); return; } if (!(event->buttons() & Qt::LeftButton)) { QQuickView::mouseMoveEvent(event); return; } if (!event->isAccepted() && !m_dragStart.isNull() && (event->pos() - m_dragStart).manhattanLength() >= QApplication::startDragDistance()) { m_dragStart = QPoint(); emit startDrag(); } } void GLWidget::keyPressEvent(QKeyEvent *event) { QQuickView::keyPressEvent(event); if (!event->isAccepted()) { emit passKeyEvent(event); } } void GLWidget::createThread(RenderThread **thread, thread_function_t function, void *data) { #ifdef Q_OS_WIN // On Windows, MLT event consumer-thread-create is fired from the Qt main thread. while (!m_isInitialized) { qApp->processEvents(); } #else if (!m_isInitialized) { m_initSem.acquire(); } #endif (*thread) = new RenderThread(function, data, m_shareContext, &m_offscreenSurface); (*thread)->start(); } static void onThreadCreate(mlt_properties owner, GLWidget *self, RenderThread **thread, int *priority, thread_function_t function, void *data) { Q_UNUSED(owner) Q_UNUSED(priority) // self->clearFrameRenderer(); self->createThread(thread, function, data); self->lockMonitor(); } static void onThreadJoin(mlt_properties owner, GLWidget *self, RenderThread *thread) { Q_UNUSED(owner) if (thread) { thread->quit(); thread->wait(); delete thread; // self->clearFrameRenderer(); self->releaseMonitor(); } } void GLWidget::startGlsl() { // C & D if (m_glslManager) { // clearFrameRenderer(); m_glslManager->fire_event("init glsl"); if (m_glslManager->get_int("glsl_supported") == 0) { disableGPUAccel(); } else { emit started(); } } } static void onThreadStarted(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->startGlsl(); } void GLWidget::releaseMonitor() { emit lockMonitor(false); } void GLWidget::lockMonitor() { emit lockMonitor(true); } void GLWidget::stopGlsl() { if (m_consumer) { m_consumer->purge(); } // C & D // TODO This is commented out for now because it is causing crashes. // Technically, this should be the correct thing to do, but it appears // some changes have created regression (see shotcut) // with respect to restarting the consumer in GPU mode. // m_glslManager->fire_event("close glsl"); m_texture[0] = 0; } static void onThreadStopped(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->stopGlsl(); } int GLWidget::setProducer(const std::shared_ptr &producer, bool isActive, int position) { int error = 0; QString currentId; int consumerPosition = 0; currentId = m_producer->parent().get("kdenlive:id"); if (producer) { m_producer = producer; } else { if (currentId == QLatin1String("black")) { return 0; } m_producer = m_blackClip; // Reset markersModel rootContext()->setContextProperty("markersModel", 0); } // redundant check. postcondition of above is m_producer != null if (m_producer) { m_producer->set_speed(0); if (m_consumer) { consumerPosition = m_consumer->position(); m_consumer->stop(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } error = reconfigure(); if (error == 0) { // The profile display aspect ratio may have changed. resizeGL(width(), height()); } } else { return error; } if (!m_consumer) { return error; } consumerPosition = m_consumer->position(); if (position == -1 && m_producer->parent().get("kdenlive:id") == currentId) { position = consumerPosition; } if (isActive) { startConsumer(); } m_proxy->requestSeekPosition(position > 0 ? position : m_producer->position()); return error; } int GLWidget::droppedFrames() const { return (m_consumer ? m_consumer->get_int("drop_count") : 0); } void GLWidget::resetDrops() { if (m_consumer) { m_consumer->set("drop_count", 0); } } void GLWidget::stopCapture() { if (strcmp(m_consumer->get("mlt_service"), "multi") == 0) { m_consumer->set("refresh", 0); m_consumer->purge(); m_consumer->stop(); } } int GLWidget::reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile) { Q_UNUSED(params); Q_UNUSED(path); Q_UNUSED(profile); // TODO Fix or delete /* QString serviceName = property("mlt_service").toString(); if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") != 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } m_consumer.reset(new Mlt::FilteredConsumer(*profile, "multi")); delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // build sub consumers // m_consumer->set("mlt_image_format", "yuv422"); reloadProfile(); int volume = KdenliveSettings::volume(); m_consumer->set("0", serviceName.toUtf8().constData()); m_consumer->set("0.mlt_image_format", "yuv422"); m_consumer->set("0.terminate_on_pause", 0); // m_consumer->set("0.preview_off", 1); m_consumer->set("0.real_time", 0); m_consumer->set("0.volume", (double)volume / 100); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { #ifdef Q_OS_WIN m_consumer->set("0.audio_buffer", 2048); #else m_consumer->set("0.audio_buffer", 512); #endif QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } m_consumer->set("1", "avformat"); m_consumer->set("1.target", path.toUtf8().constData()); // m_consumer->set("1.real_time", -KdenliveSettings::mltthreads()); m_consumer->set("terminate_on_pause", 0); m_consumer->set("1.terminate_on_pause", 0); // m_consumer->set("1.terminate_on_pause", 0);// was commented out. restoring it fixes mantis#3415 - FFmpeg recording freezes QStringList paramList = params.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramList.count(); ++i) { QString key = "1." + paramList.at(i).section(QLatin1Char('='), 0, 0); QString value = paramList.at(i).section(QLatin1Char('='), 1, 1); if (value == QLatin1String("%threads")) { value = QString::number(QThread::idealThreadCount()); } m_consumer->set(key.toUtf8().constData(), value.toUtf8().constData()); } // Connect the producer to the consumer - tell it to "run" later delete m_displayEvent; // C & D if (m_glslManager) { // D if (m_openGLSync) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // C m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_nosync_frame_show); } } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } m_consumer->connect(*m_producer.get()); m_consumer->start(); return 0; } */ return -1; } int GLWidget::reconfigure(bool reload) { int error = 0; // use SDL for audio, OpenGL for video QString serviceName = property("mlt_service").toString(); if (reload) { - m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:black")); + m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:0")); m_blackClip->set("kdenlive:id", "black"); reloadProfile(); return error; } if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") == 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } QString audioBackend = (KdenliveSettings::external_display()) ? QString("decklink:%1").arg(KdenliveSettings::blackmagic_output_device()) : KdenliveSettings::audiobackend(); if (m_consumer == nullptr || serviceName.isEmpty() || serviceName != audioBackend) { m_consumer.reset(new Mlt::FilteredConsumer(pCore->getCurrentProfile()->profile(), audioBackend.toLatin1().constData())); if (m_consumer->is_valid()) { serviceName = audioBackend; setProperty("mlt_service", serviceName); if (KdenliveSettings::external_display()) { m_consumer->set("terminate_on_pause", 0); } + m_profileSize = pCore->getCurrentFrameSize(); + m_colorSpace = pCore->getCurrentProfile()->colorspace(); + m_dar = pCore->getCurrentDar(); } else { // Warning, audio backend unavailable on system m_consumer.reset(); QStringList backends = {"sdl2_audio", "sdl_audio", "rtaudio"}; for (const QString &bk : backends) { if (bk == audioBackend) { // Already tested continue; } m_consumer.reset(new Mlt::FilteredConsumer(pCore->getCurrentProfile()->profile(), bk.toLatin1().constData())); if (m_consumer->is_valid()) { if (audioBackend == KdenliveSettings::sdlAudioBackend()) { // switch sdl audio backend KdenliveSettings::setSdlAudioBackend(bk); } qDebug() << "++++++++\nSwitching audio backend to: " << bk << "\n++++++++++"; KdenliveSettings::setAudiobackend(bk); serviceName = bk; setProperty("mlt_service", serviceName); break; } else { m_consumer.reset(); } } if (!m_consumer) { qWarning() << "WARNING, NO AUDIO BACKEND FOUND"; return -1; } } } delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // Connect the producer to the consumer - tell it to "run" later if (m_producer) { m_consumer->connect(*m_producer.get()); // m_producer->set_speed(0.0); } int dropFrames = realTime(); if (!KdenliveSettings::monitor_dropframes()) { dropFrames = -dropFrames; } m_consumer->set("real_time", dropFrames); // C & D if (m_glslManager) { if (!m_threadStartEvent) { m_threadStartEvent = m_consumer->listen("consumer-thread-started", this, (mlt_listener)onThreadStarted); } if (!m_threadStopEvent) { m_threadStopEvent = m_consumer->listen("consumer-thread-stopped", this, (mlt_listener)onThreadStopped); } if (!serviceName.startsWith(QLatin1String("decklink"))) { m_consumer->set("mlt_image_format", "glsl"); } } else { // A & B m_consumer->set("mlt_image_format", "yuv422"); } delete m_displayEvent; // C & D if (m_proxy->property("clipType").toInt() == ClipType::Audio) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_audio_frame_show); m_sharedFrame = SharedFrame(); m_texture[0] = 0; - } else if (m_glslManager) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } int volume = KdenliveSettings::volume(); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } /*if (!pCore->getCurrentProfile()->progressive()) m_consumer->set("progressive", property("progressive").toBool());*/ m_consumer->set("volume", volume / 100.0); // m_consumer->set("progressive", 1); m_consumer->set("rescale", KdenliveSettings::mltinterpolation().toUtf8().constData()); m_consumer->set("deinterlace_method", KdenliveSettings::mltdeinterlacer().toUtf8().constData()); /* #ifdef Q_OS_WIN m_consumer->set("audio_buffer", 2048); #else m_consumer->set("audio_buffer", 512); #endif */ int fps = qRound(pCore->getCurrentProfile()->profile().fps()); m_consumer->set("buffer", qMax(25, fps)); m_consumer->set("prefill", qMax(1, fps / 25)); m_consumer->set("drop_max", fps / 4); m_consumer->set("scrub_audio", 1); if (KdenliveSettings::monitor_gamma() == 0) { m_consumer->set("color_trc", "iec61966_2_1"); } else { m_consumer->set("color_trc", "bt709"); } } else { // Cleanup on error error = 2; } return error; } float GLWidget::zoom() const { return m_zoom; } -float GLWidget::scale() const -{ - return (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom; -} - void GLWidget::reloadProfile() { // The profile display aspect ratio may have changed. if (m_consumer) { // Make sure to delete and rebuild consumer to match profile m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); reconfigure(); } resizeGL(width(), height()); refreshSceneLayout(); } QSize GLWidget::profileSize() const { - return {pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->height()}; + return m_profileSize; } QRect GLWidget::displayRect() const { return m_rect; } QPoint GLWidget::offset() const { - return {m_offset.x() - ((int)((float)pCore->getCurrentProfile()->width() * m_zoom) - width()) / 2, - m_offset.y() - ((int)((float)pCore->getCurrentProfile()->height() * m_zoom) - height()) / 2}; + return {m_offset.x() - ((int)((float)m_profileSize.width() * m_zoom) - width()) / 2, + m_offset.y() - ((int)((float)m_profileSize.height() * m_zoom) - height()) / 2}; } void GLWidget::setZoom(float zoom) { double zoomRatio = zoom / m_zoom; m_zoom = zoom; emit zoomChanged(); if (rootObject()) { rootObject()->setProperty("zoom", m_zoom); double scalex = rootObject()->property("scalex").toDouble() * zoomRatio; rootObject()->setProperty("scalex", scalex); double scaley = rootObject()->property("scaley").toDouble() * zoomRatio; rootObject()->setProperty("scaley", scaley); } update(); } void GLWidget::onFrameDisplayed(const SharedFrame &frame) { m_contextSharedAccess.lock(); m_sharedFrame = frame; m_sendFrame = sendFrameForAnalysis; m_contextSharedAccess.unlock(); update(); + emit frameDisplayed(frame); } void GLWidget::mouseReleaseEvent(QMouseEvent *event) { QQuickView::mouseReleaseEvent(event); if (m_dragStart.isNull() && m_panStart.isNull() && (rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier)) { event->ignore(); return; } if (!m_dragStart.isNull() && m_panStart.isNull() && ((event->button() & Qt::LeftButton) != 0u) && !event->isAccepted()) { emit monitorPlay(); } m_dragStart = QPoint(); m_panStart = QPoint(); setCursor(Qt::ArrowCursor); } void GLWidget::purgeCache() { if (m_consumer) { m_consumer->purge(); m_producer->seek(m_proxy->position() + 1); } } void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) { QQuickView::mouseDoubleClickEvent(event); if (event->isAccepted()) { return; } if ((rootObject() == nullptr) || rootObject()->objectName() != QLatin1String("rooteffectscene")) { emit switchFullScreen(); } event->accept(); } void GLWidget::setOffsetX(int x, int max) { m_offset.setX(x); emit offsetChanged(); if (rootObject()) { rootObject()->setProperty("offsetx", m_zoom > 1.0f ? x - max / 2.0 - 10 : 0); } update(); } void GLWidget::setOffsetY(int y, int max) { m_offset.setY(y); if (rootObject()) { rootObject()->setProperty("offsety", m_zoom > 1.0f ? y - max / 2.0 - 10 : 0); } update(); } int GLWidget::realTime() const { // C & D if (m_glslManager) { return 1; } return KdenliveSettings::mltthreads(); } std::shared_ptr GLWidget::consumer() { return m_consumer; } void GLWidget::updateGamma() { reconfigure(); } void GLWidget::resetConsumer(bool fullReset) { if (fullReset && m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } reconfigure(); } const QString GLWidget::sceneList(const QString &root, const QString &fullPath) { QString playlist; qCDebug(KDENLIVE_LOG) << " * * *Setting document xml root: " << root; Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData()); if (!root.isEmpty()) { xmlConsumer.set("root", root.toUtf8().constData()); } if (!xmlConsumer.is_valid()) { return QString(); } m_producer->optimise(); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); xmlConsumer.set("time_format", "clock"); // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc) // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening // xmlConsumer.set("no_meta", 1); Mlt::Producer prod(m_producer->get_producer()); if (!prod.is_valid()) { return QString(); } xmlConsumer.connect(prod); xmlConsumer.run(); playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath; return playlist; } void GLWidget::updateTexture(GLuint yName, GLuint uName, GLuint vName) { m_texture[0] = yName; m_texture[1] = uName; m_texture[2] = vName; m_sendFrame = sendFrameForAnalysis; // update(); } // MLT consumer-frame-show event handler void GLWidget::on_audio_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); //qDebug()<<"== SHOWING FRAME: "<(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showAudioFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); //qDebug()<<"== SHOWING FRAME: "<(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { auto *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLNoSyncFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); qDebug()<<"== SHOWING GL FRAME: "<(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } RenderThread::RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface) : QThread(nullptr) , m_function(function) , m_data(data) , m_context(nullptr) , m_surface(surface) { if (context) { m_context = new QOpenGLContext; m_context->setFormat(context->format()); m_context->setShareContext(context); m_context->create(); m_context->moveToThread(this); } } RenderThread::~RenderThread() { // would otherwise leak if RenderThread is allocated with a context but not run. // safe post-run delete m_context; } // TODO: missing some exception handling? void RenderThread::run() { if (m_context) { m_context->makeCurrent(m_surface); } m_function(m_data); if (m_context) { m_context->doneCurrent(); delete m_context; m_context = nullptr; } } FrameRenderer::FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync) : QThread(nullptr) , m_semaphore(3) , m_context(nullptr) , m_surface(surface) , m_ClientWaitSync(clientWaitSync) , m_gl32(nullptr) , sendAudioForAnalysis(false) { Q_ASSERT(shareContext); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; // B & C & D if (KdenliveSettings::gpu_accel() || shareContext->supportsThreadedOpenGL()) { m_context = new QOpenGLContext; m_context->setFormat(shareContext->format()); m_context->setShareContext(shareContext); m_context->create(); m_context->moveToThread(this); } setObjectName(QStringLiteral("FrameRenderer")); moveToThread(this); start(); } FrameRenderer::~FrameRenderer() { delete m_context; delete m_gl32; } void FrameRenderer::showAudioFrame(Mlt::Frame frame) { // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showFrame(Mlt::Frame frame) { int width = 0; int height = 0; mlt_image_format format = mlt_image_yuv420p; frame.get_image(format, width, height); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); if ((m_context != nullptr) && m_context->isValid()) { m_context->makeCurrent(m_surface); // Upload each plane of YUV to a texture. QOpenGLFunctions *f = m_context->functions(); uploadTextures(m_context, m_displayFrame, m_renderTexture); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); f->glFinish(); for (int i = 0; i < 3; ++i) { std::swap(m_renderTexture[i], m_displayTexture[i]); } emit textureReady(m_displayTexture[0], m_displayTexture[1], m_displayTexture[2]); m_context->doneCurrent(); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); pipelineSyncToFrame(frame); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLNoSyncFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::cleanup() { if ((m_renderTexture[0] != 0u) && (m_renderTexture[1] != 0u) && (m_renderTexture[2] != 0u)) { m_context->makeCurrent(m_surface); m_context->functions()->glDeleteTextures(3, m_renderTexture); if ((m_displayTexture[0] != 0u) && (m_displayTexture[1] != 0u) && (m_displayTexture[2] != 0u)) { m_context->functions()->glDeleteTextures(3, m_displayTexture); } m_context->doneCurrent(); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; } } // D void FrameRenderer::pipelineSyncToFrame(Mlt::Frame &frame) { auto sync = (GLsync)frame.get_data("movit.convert.fence"); if (!sync) return; #ifdef Q_OS_WIN // On Windows, use QOpenGLFunctions_3_2_Core instead of getProcAddress. // TODO: move to initialization of m_ClientWaitSync if (!m_gl32) { m_gl32 = m_context->versionFunctions(); if (m_gl32) { m_gl32->initializeOpenGLFunctions(); } } if (m_gl32) { m_gl32->glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #else if (m_ClientWaitSync) { m_ClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #endif // Q_OS_WIN } void GLWidget::refreshSceneLayout() { if (!rootObject()) { return; } - rootObject()->setProperty("profile", QPoint(pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->height())); + rootObject()->setProperty("profile", QPoint(m_profileSize.width(), m_profileSize.height())); rootObject()->setProperty("scalex", (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom); rootObject()->setProperty("scaley", (double)m_rect.width() / - (((double)pCore->getCurrentProfile()->height() * pCore->getCurrentProfile()->dar() / pCore->getCurrentProfile()->width())) / - pCore->getCurrentProfile()->width() * m_zoom); + (((double)m_profileSize.height() * m_dar / m_profileSize.width())) / + m_profileSize.width() * m_zoom); } void GLWidget::switchPlay(bool play, double speed) { m_proxy->setSeekPosition(-1); if (!m_producer || !m_consumer) { return; } if (m_isZoneMode) { resetZoneMode(); } if (play) { if (m_id == Kdenlive::ClipMonitor && m_consumer->position() == m_producer->get_out()) { m_producer->seek(0); } m_producer->set_speed(speed); m_consumer->start(); m_consumer->set("refresh", 1); } else { emit paused(); m_producer->set_speed(0); m_producer->seek(m_consumer->position() + 1); m_consumer->purge(); m_consumer->start(); } } bool GLWidget::playZone(bool loop) { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_proxy->zoneOut()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = loop; return true; } bool GLWidget::loopClip() { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(0); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_producer->get_playtime()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = true; return true; } void GLWidget::resetZoneMode() { if (!m_isZoneMode && !m_isLoopMode) { return; } m_producer->set("out", m_producer->get_length()); m_isZoneMode = false; m_isLoopMode = false; } MonitorProxy *GLWidget::getControllerProxy() { return m_proxy; } int GLWidget::getCurrentPos() const { return m_proxy->seeking() ? m_proxy->seekPosition() : m_consumer->position(); } void GLWidget::setRulerInfo(int duration, const std::shared_ptr &model) { rootObject()->setProperty("duration", duration); if (model != nullptr) { // we are resetting marker/snap model, reset zone rootContext()->setContextProperty("markersModel", model.get()); } } void GLWidget::startConsumer() { if (m_consumer == nullptr) { return; } if (m_consumer->is_stopped() && m_consumer->start() == -1) { // ARGH CONSUMER BROKEN!!!! KMessageBox::error( qApp->activeWindow(), i18n("Could not create the video preview window.\nThere is something wrong with your Kdenlive install or your driver settings, please fix it.")); if (m_displayEvent) { delete m_displayEvent; } m_displayEvent = nullptr; m_consumer.reset(); return; } m_consumer->set("refresh", 1); } void GLWidget::stop() { m_refreshTimer.stop(); m_proxy->setSeekPosition(-1); // why this lock? QMutexLocker locker(&m_mltMutex); if (m_producer) { if (m_isZoneMode) { resetZoneMode(); } m_producer->set_speed(0.0); } if (m_consumer) { m_consumer->purge(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } } double GLWidget::playSpeed() const { if (m_producer) { return m_producer->get_speed(); } return 0.0; } void GLWidget::setDropFrames(bool drop) { // why this lock? QMutexLocker locker(&m_mltMutex); if (m_consumer) { int dropFrames = realTime(); if (!drop) { dropFrames = -dropFrames; } m_consumer->stop(); m_consumer->set("real_time", dropFrames); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } int GLWidget::volume() const { if ((!m_consumer) || (!m_producer)) { return -1; } if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { return ((int)100 * m_consumer->get_double("0.volume")); } return ((int)100 * m_consumer->get_double("volume")); } void GLWidget::setVolume(double volume) { if (m_consumer) { if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { m_consumer->set("0.volume", volume); } else { m_consumer->set("volume", volume); } } } int GLWidget::duration() const { if (!m_producer) { return 0; } return m_producer->get_playtime(); } void GLWidget::setConsumerProperty(const QString &name, const QString &value) { QMutexLocker locker(&m_mltMutex); if (m_consumer) { m_consumer->set(name.toUtf8().constData(), value.toUtf8().constData()); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } diff --git a/src/monitor/glwidget.h b/src/monitor/glwidget.h index 2e7b7b47b..4496e4428 100644 --- a/src/monitor/glwidget.h +++ b/src/monitor/glwidget.h @@ -1,339 +1,341 @@ /* * Copyright (c) 2011-2014 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef GLWIDGET_H #define GLWIDGET_H #include #include #include #include #include #include #include #include #include #include #include #include #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "kdenlivesettings.h" #include "scopes/sharedframe.h" class QOpenGLFunctions_3_2_Core; namespace Mlt { class Filter; class Producer; class Consumer; class Profile; } // namespace Mlt class RenderThread; class FrameRenderer; class MonitorProxy; using thread_function_t = void *(*)(void *); /* QQuickView that renders an . * * Creates an MLT consumer and renders a GL view from the consumer. This pipeline is one of: * * A. YUV gl texture w/o GPU filter acceleration * B. YUV gl texture multithreaded w/o GPU filter acceleration * C. RGB gl texture multithreaded w/ GPU filter acceleration and no sync * D. RGB gl texture multithreaded w/ GPU filter acceleration and sync */ class GLWidget : public QQuickView, protected QOpenGLFunctions { Q_OBJECT Q_PROPERTY(QRect rect READ rect NOTIFY rectChanged) Q_PROPERTY(float zoom READ zoom NOTIFY zoomChanged) Q_PROPERTY(QPoint offset READ offset NOTIFY offsetChanged) public: friend class MonitorController; friend class Monitor; friend class MonitorProxy; using ClientWaitSync_fp = GLenum (*)(GLsync, GLbitfield, GLuint64); GLWidget(int id, QObject *parent = nullptr); ~GLWidget() override; int requestedSeekPosition; void createThread(RenderThread **thread, thread_function_t function, void *data); void startGlsl(); void stopGlsl(); void clear(); // TODO: currently unused int reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile); void stopCapture(); int reconfigure(bool reload = false); /** @brief Get the current MLT producer playlist. * @return A string describing the playlist */ const QString sceneList(const QString &root, const QString &fullPath = QString()); int displayWidth() const { return m_rect.width(); } void updateAudioForAnalysis(); int displayHeight() const { return m_rect.height(); } QObject *videoWidget() { return this; } Mlt::Filter *glslManager() const { return m_glslManager; } QRect rect() const { return m_rect; } QRect effectRect() const { return m_effectRect; } float zoom() const; - float scale() const; QPoint offset() const; std::shared_ptr consumer(); Mlt::Producer *producer(); QSize profileSize() const; QRect displayRect() const; /** @brief set to true if we want to emit a QImage of the frame for analysis */ bool sendFrameForAnalysis; void updateGamma(); /** @brief delete and rebuild consumer, for example when external display is switched */ void resetConsumer(bool fullReset); void reloadProfile(); void lockMonitor(); void releaseMonitor(); int realTime() const; int droppedFrames() const; void resetDrops(); bool checkFrameNumber(int pos, int offset); /** @brief Return current timeline position */ int getCurrentPos() const; /** @brief Requests a monitor refresh */ void requestRefresh(); void setRulerInfo(int duration, const std::shared_ptr &model = nullptr); MonitorProxy *getControllerProxy(); bool playZone(bool loop = false); bool loopClip(); void startConsumer(); void stop(); int rulerHeight() const; /** @brief return current play producer's playing speed */ double playSpeed() const; /** @brief Turn drop frame feature on/off */ void setDropFrames(bool drop); /** @brief Returns current audio volume */ int volume() const; /** @brief Set audio volume on consumer */ void setVolume(double volume); /** @brief Returns current producer's duration in frames */ int duration() const; /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); /** @brief Clear consumer cache */ void purgeCache(); protected: void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; /** @brief Update producer, should ONLY be called from monitor */ int setProducer(const std::shared_ptr &producer, bool isActive, int position = -1); QString frameToTime(int frames) const; public slots: void seek(int pos); void requestSeek(); void setZoom(float zoom); void setOffsetX(int x, int max); void setOffsetY(int y, int max); void slotZoom(bool zoomIn); void initializeGL(); void releaseAnalyse(); void switchPlay(bool play, double speed = 1.0); signals: void frameDisplayed(const SharedFrame &frame); void frameRendered(int pos); void dragStarted(); void seekTo(int x); void gpuNotSupported(); void started(); void paused(); void playing(); void rectChanged(); void zoomChanged(); void offsetChanged(); void monitorPlay(); void switchFullScreen(bool minimizeOnly = false); void mouseSeek(int eventDelta, uint modifiers); void startDrag(); void analyseFrame(const QImage &); void audioSamplesSignal(const audioShortVector &, int, int, int); void showContextMenu(const QPoint &); void lockMonitor(bool); void passKeyEvent(QKeyEvent *); void panView(const QPoint &diff); void seekPosition(int); void consumerPosition(int); void activateMonitor(); protected: Mlt::Filter *m_glslManager; // TODO: MTL has lock/unlock of individual nodes. Use those. // keeping this for refactoring ease. QMutex m_mltMutex; std::shared_ptr m_consumer; std::shared_ptr m_producer; int m_id; int m_rulerHeight; private: QRect m_rect; QRect m_effectRect; GLuint m_texture[3]; QOpenGLShaderProgram *m_shader; QPoint m_panStart; QPoint m_dragStart; QSemaphore m_initSem; QSemaphore m_analyseSem; bool m_isInitialized; Mlt::Event *m_threadStartEvent; Mlt::Event *m_threadStopEvent; Mlt::Event *m_threadCreateEvent; Mlt::Event *m_threadJoinEvent; Mlt::Event *m_displayEvent; Mlt::Event *m_renderEvent; FrameRenderer *m_frameRenderer; int m_projectionLocation; int m_modelViewLocation; int m_vertexLocation; int m_texCoordLocation; int m_colorspaceLocation; int m_textureLocation[3]; QTimer m_refreshTimer; float m_zoom; + QSize m_profileSize; + int m_colorSpace; + double m_dar; bool m_sendFrame; bool m_isZoneMode; bool m_isLoopMode; QPoint m_offset; MonitorProxy *m_proxy; std::shared_ptr m_blackClip; static void on_frame_show(mlt_consumer, void *self, mlt_frame frame); static void on_audio_frame_show(mlt_consumer, void *self, mlt_frame frame); static void on_frame_render(mlt_consumer, GLWidget *widget, mlt_frame frame); static void on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); static void on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); QOpenGLFramebufferObject *m_fbo; void refreshSceneLayout(); void resetZoneMode(); /* OpenGL context management. Interfaces to MLT according to the configured render pipeline. */ private slots: void resizeGL(int width, int height); void updateTexture(GLuint yName, GLuint uName, GLuint vName); void paintGL(); void onFrameDisplayed(const SharedFrame &frame); void refresh(); protected: QMutex m_contextSharedAccess; QOffscreenSurface m_offscreenSurface; SharedFrame m_sharedFrame; QOpenGLContext *m_shareContext; bool acquireSharedFrameTextures(); void bindShaderProgram(); void createGPUAccelFragmentProg(); void createShader(); void createYUVTextureProjectFragmentProg(); void disableGPUAccel(); void releaseSharedFrameTextures(); // pipeline A - YUV gl texture w/o GPU filter acceleration // pipeline B - YUV gl texture multithreaded w/o GPU filter acceleration // pipeline C - RGB gl texture multithreaded w/ GPU filter acceleration and no sync // pipeline D - RGB gl texture multithreaded w/ GPU filter acceleration and sync bool m_openGLSync; bool initGPUAccelSync(); // pipeline C & D bool initGPUAccel(); bool onlyGLESGPUAccel() const; // pipeline A & B & C & D // not null iff D ClientWaitSync_fp m_ClientWaitSync; protected: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; void keyPressEvent(QKeyEvent *event) override; }; class RenderThread : public QThread { Q_OBJECT public: RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface); ~RenderThread() override; protected: void run() override; private: thread_function_t m_function; void *m_data; QOpenGLContext *m_context; QSurface *m_surface; }; class FrameRenderer : public QThread { Q_OBJECT public: explicit FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync); ~FrameRenderer() override; QSemaphore *semaphore() { return &m_semaphore; } QOpenGLContext *context() const { return m_context; } Q_INVOKABLE void showFrame(Mlt::Frame frame); Q_INVOKABLE void showAudioFrame(Mlt::Frame frame); Q_INVOKABLE void showGLFrame(Mlt::Frame frame); Q_INVOKABLE void showGLNoSyncFrame(Mlt::Frame frame); public slots: void cleanup(); signals: void textureReady(GLuint yName, GLuint uName = 0, GLuint vName = 0); void frameDisplayed(const SharedFrame &frame); void audioSamplesSignal(const audioShortVector &, int, int, int); private: QSemaphore m_semaphore; SharedFrame m_displayFrame; QOpenGLContext *m_context; QSurface *m_surface; GLWidget::ClientWaitSync_fp m_ClientWaitSync; void pipelineSyncToFrame(Mlt::Frame &); public: GLuint m_renderTexture[3]; GLuint m_displayTexture[3]; QOpenGLFunctions_3_2_Core *m_gl32; bool sendAudioForAnalysis; }; #endif diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index f72e3279e..b0358291b 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2113 +1,2113 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "monitor.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "jobs/jobmanager.h" #include "jobs/cutclipjob.h" #include "scopes/monitoraudiolevel.h" #include "timeline2/model/snapmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/thumbnailcache.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { auto *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_snaps(new SnapModel()) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_contextMenu(nullptr) , m_markerMenu(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_lastMonitorSceneType(MonitorSceneDefault) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor, &GLWidget::seekPosition, this, &Monitor::seekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::consumerPosition, this, &Monitor::slotSeekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); auto *monitorEventEater = new QuickMonitorEventEater(this); m_videoWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); - connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed); + connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek); connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); QWidget *sp1 = new QWidget(this); sp1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(sp1); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree"))); m_toolbar->setToolTip(i18n("Insert Zone to Project Bin")); m_toolbar->addSeparator(); } else if (id == Kdenlive::ProjectMonitor) { connect(m_glMonitor, &GLWidget::paused, m_monitorManager, &MonitorManager::cleanMixer); } if (id != Kdenlive::DvdMonitor) { QAction *markIn = new QAction(QIcon::fromTheme(QStringLiteral("zone-in")), i18n("Set Zone In"), this); QAction *markOut = new QAction(QIcon::fromTheme(QStringLiteral("zone-out")), i18n("Set Zone Out"), this); m_toolbar->addAction(markIn); m_toolbar->addAction(markOut); connect(markIn, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_in"))->trigger(); }); connect(markOut, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_out"))->trigger(); }); } m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_backward"))); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_forward"))); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(KdenliveSettings::volume()); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(i18n("Volume"), this); menu->setIcon(QIcon::fromTheme(QStringLiteral("audio-volume-medium"))); menu->addAction(widgetslider); m_configMenu->addMenu(menu); /*QIcon icon; if (KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon);*/ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); connect(m_glMonitor, &GLWidget::audioSamplesSignal, this, &Monitor::audioSamplesSignal); if (id != Kdenlive::ClipMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateTimelineClipZone); } else { connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone); } connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame); m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(m_monitorManager->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); /*QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(spacer);*/ m_toolbar->addSeparator(); int tm = 0; int bm = 0; m_toolbar->getContentsMargins(nullptr, &tm, nullptr, &bm); m_audioMeterWidget = new MonitorAudioLevel(m_toolbar->height() - tm - bm, this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; m_monitorManager->activateMonitor(m_id); refreshMonitorIfActive(); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = QIcon::fromTheme(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->recAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->data().toString() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } // TODO: add save zone to timeline monitor when fixed m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("monitor_multitrack"))); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::primaryScreen()->geometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = pCore->getCurrentProfile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } QString Monitor::getTimecodeFromFrames(int pos) { return m_monitorManager->timecode().getTimecodeFromFrames(pos); } double Monitor::fps() const { return m_monitorManager->timecode().fps(); } Timecode Monitor::timecode() const { return m_monitorManager->timecode(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(true).frames(m_monitorManager->timecode().fps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(false).frames(m_monitorManager->timecode().fps())); } } int Monitor::position() { return m_glMonitor->getCurrentPos(); } GenTime Monitor::getSnapForPos(bool previous) { int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return {frame, pCore->getCurrentFps()}; } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y()); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } else { // timeline emit timelineZoneChanged(); } checkOverlay(); } void Monitor::slotSetZoneEnd(bool discardLastFrame) { Q_UNUSED(discardLastFrame); int pos = m_glMonitor->getCurrentPos(); if (m_controller) { if (pos < (int)m_controller->frameDuration() - 1) { pos++; } } else pos++; m_glMonitor->getControllerProxy()->setZoneOut(pos); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { if (m_markerMenu) { // Fill guide menu m_markerMenu->clear(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { QList markersList = model->getAllMarkers(); for (CommentedTime mkr : markersList) { QAction *a = m_markerMenu->addAction(mkr.comment()); a->setData(mkr.time().frames(pCore->getCurrentFps())); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum()); float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum()); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum()))); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep()); m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical)); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? pause(); if (!m_videoWidget->isFullScreen() && !minimizeOnly) { // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget) if (qApp->screens().count() > 1) { for (auto screen : qApp->screens()) { if (screen != qApp->screenAt(this->parentWidget()->mapToGlobal(QPoint()))) { QRect rect = screen->availableGeometry(); m_videoWidget->setParent(nullptr); m_videoWidget->move(this->parentWidget()->mapFromGlobal(rect.topLeft())); break; } } } else { m_videoWidget->setParent(qApp->desktop()->screen(0)); } m_videoWidget->showFullScreen(); } else { m_videoWidget->setParent(m_glWidget); //m_videoWidget->move(this->pos()); m_videoWidget->showNormal(); auto *lay = (QGridLayout *)m_glWidget->layout(); lay->addWidget(m_videoWidget, 0, 0); } } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; // Get drag state QQuickItem *root = m_glMonitor->rootObject(); int dragType = 0; if (root) { dragType = root->property("dragType").toInt(); root->setProperty("dragType", 0); } QByteArray prodData; QPoint p = m_glMonitor->getControllerProxy()->zone(); if (p.x() == -1 || p.y() == -1) { prodData = m_controller->AbstractProjectItem::clipId().toUtf8(); } else { QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); list.append(QString::number(p.x())); list.append(QString::number(p.y() - 1)); prodData.append(list.join(QLatin1Char('/')).toUtf8()); } switch (dragType) { case 1: // Audio only drag prodData.prepend('A'); break; case 2: // Audio only drag prodData.prepend('V'); break; default: break; } mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->exec(Qt::MoveAction); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray clipData; clipData.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), clipData); drag->setMimeData(mimeData); drag->exec(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_videoWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, uint modifiers) { if ((modifiers & Qt::ControlModifier) != 0u) { int delta = m_monitorManager->timecode().fps(); if (eventDelta > 0) { delta = 0 - delta; } m_glMonitor->seek(m_glMonitor->getCurrentPos() - delta); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToPreviousSnap(); } else { emit seekToNextSnap(); } } else { if (eventDelta >= 0) { slotRewindOneFrame(); } else { slotForwardOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } GenTime inPoint(getZoneStart(), pCore->getCurrentFps()); GenTime outPoint(getZoneEnd(), pCore->getCurrentFps()); pCore->jobManager()->startJob({m_controller->clipId()}, -1, QString(), inPoint, outPoint); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled")) .completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) fileWidget->setSelectedUrl(relativeUrl); #else fileWidget->setSelection(relativeUrl.toString()); #endif KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QString folderInfo = pCore->bin()->getCurrentFolder(); pCore->bin()->droppedUrls(QList {QUrl::fromLocalFile(selectedFile)}, folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->seek(pos); m_monitorManager->cleanMixer(); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } std::shared_ptr model(nullptr); if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { model = m_controller->getMarkerModel(); } } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, m_monitorManager->timecode().fps()), &found); if (found) { overlayText = marker.comment(); } } m_glMonitor->getControllerProxy()->setMarkerComment(overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneOut() - 1); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed > -1) { speed = -1; } else { speed = currentspeed * 1.5; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotForward(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed < 1) { speed = 1; } else { speed = currentspeed * 1.2; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() - diff); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() + diff); } void Monitor::adjustRulerSize(int length, const std::shared_ptr &markerModel) { if (m_controller != nullptr) { m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } m_timePos->setRange(0, length); if (markerModel) { connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { if (slotActivateMonitor()) { start(); } } } void Monitor::refreshMonitorIfActive(bool directUpdate) { if (isActive()) { if (directUpdate) { m_glMonitor->refresh(); } else { m_glMonitor->requestRefresh(); } } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::resetPlayOrLoopZone(const QString &binId) { if (activeClipId() == binId) { m_glMonitor->resetZoneMode(); } } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } void Monitor::updateClipProducer(const std::shared_ptr &prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { Q_UNUSED(playlist) // TODO // Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); // m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } void Monitor::slotOpenClip(const std::shared_ptr &controller, int in, int out) { if (m_controller) { m_glMonitor->resetZoneMode(); disconnect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; m_snaps.reset(new SnapModel()); m_glMonitor->getControllerProxy()->resetZone(); if (controller) { connect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), controller->getMarkerModel()); loadQmlScene(MonitorSceneDefault); m_timePos->setRange(0, (int)m_controller->frameDuration()); updateMarkers(); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection); if (out == -1) { m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false); qDebug() << m_controller->zone(); } else { m_glMonitor->getControllerProxy()->setZone(in, out, false); } m_snaps->addPoint((int)m_controller->frameDuration()); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; if (!m_controller->hasVideo() || KdenliveSettings::displayClipMonitorInfo() & 0x10) { m_glMonitor->getControllerProxy()->setAudioThumb(ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } m_controller->getMarkerModel()->registerSnapModel(m_snaps); m_glMonitor->getControllerProxy()->setClipProperties(controller->clipType(), controller->hasAudioAndVideo(), controller->clipName()); m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); // hasEffects = controller->hasEffects(); } else { loadQmlScene(MonitorSceneDefault); m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->getControllerProxy()->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; m_glMonitor->getControllerProxy()->setClipProperties(ClipType::Unknown, false, QString()); } if (slotActivateMonitor()) { start(); } checkOverlay(); } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { // TODO Q_UNUSED(file) m_glMonitor->initializeGL(); // render->loadUrl(file); } void Monitor::slotSaveZone() { // TODO? or deprecate // render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone()); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { // TODO or deprecate Q_UNUSED(profile) m_timePos->updateTimeCode(tc); if (/* DISABLES CODE */ (true)) { return; } slotActivateMonitor(); // render->prepareProfileReset(tc.fps()); // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(m_monitorManager->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); double fps = m_monitorManager->timecode().fps(); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } void Monitor::resetConsumer(bool fullReset) { m_glMonitor->resetConsumer(fullReset); } const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone() { if (m_controller == nullptr) { return; } m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } void Monitor::updateTimelineClipZone() { emit zoneUpdated(m_glMonitor->getControllerProxy()->zone()); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { if (m_controller == nullptr) { return {}; } return m_controller->zone(); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit updateScene(); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return {}; } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } /*if (volume == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); }*/ } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { - m_monitorManager->frameDisplayed(frame); if (!m_glMonitor->checkFrameNumber(frame.get_position(), m_id == Kdenlive::ClipMonitor ? 0 : TimelineModel::seekDuration)) { m_playAction->setActive(false); } + m_monitorManager->frameDisplayed(frame); checkDrops(m_glMonitor->droppedFrames()); } void Monitor::checkDrops(int dropped) { if (m_droppedTimer.isValid()) { if (m_droppedTimer.hasExpired(1000)) { m_droppedTimer.invalidate(); double fps = m_monitorManager->timecode().fps(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } else { m_glMonitor->resetDrops(); fps -= dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); m_droppedTimer.start(); } } } else if (dropped > 0) { // Start m_dropTimer m_glMonitor->resetDrops(); m_droppedTimer.start(); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { bool ok = false; QDir dir = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); if (ok) { QString url = dir.absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(m_monitorManager->timecode().fps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } } return QString(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QQuickItem *root = m_glMonitor->rootObject(); if (root) { QMetaObject::invokeMethod(root, "updatePalette"); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); m_splitEffect.reset(); loadQmlScene(MonitorSceneDefault); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active qDebug() << " . . . .. ALREADY ACTIVE"; return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { // TODO m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), position()); m_splitEffect.reset(); m_splitProducer.reset(); loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(pCore->getCurrentProfile()->profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { m_splitEffect.reset(); pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(pCore->getCurrentProfile()->profile()); std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original)); // Delete all effects int ct = 0; Mlt::Filter *filter = clone->filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (clone->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = clone->filter(ct); } trac.set_track(*original, 0); trac.set_track(*clone.get(), 1); clone.get()->attach(*m_splitEffect.get()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete original; m_splitProducer = std::make_shared(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type) { if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500); type = MonitorSceneDefault; } double ratio = (double)m_glMonitor->profileSize().width() / (int)(m_glMonitor->profileSize().height() * pCore->getCurrentProfile()->dar() + 0.5); m_qmlManager->setScene(m_id, type, m_glMonitor->profileSize(), ratio, m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); QQuickItem *root = m_glMonitor->rootObject(); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(m_monitorManager->timecode().fps(), 'g', 2)); } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { QRect r = m_glMonitor->rect(); double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = 0.5 - ((root->property("splitterPos").toInt() - r.left() - r.width() / 2.0) / (double)r.width() / 2.0) / 0.75; // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", percent); } m_glMonitor->refresh(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } void Monitor::prepareAudioThumb() { if (m_controller) { m_glMonitor->getControllerProxy()->setAudioThumb(ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); m_glMonitor->rootObject()->setProperty("showFps", currentOverlay & 0x20); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::requestSeek(int pos) { m_glMonitor->seek(pos); m_monitorManager->cleanMixer(); } void Monitor::setProducer(std::shared_ptr producer, int pos) { m_glMonitor->setProducer(std::move(producer), isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } void Monitor::slotSeekPosition(int pos) { m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->seek(0); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->seek(m_glMonitor->duration()); } else { m_glMonitor->seek(pCore->projectDuration() - 1); } } void Monitor::addSnapPoint(int pos) { m_snaps->addPoint(pos); } void Monitor::removeSnapPoint(int pos) { m_snaps->removePoint(pos); } void Monitor::slotSetScreen(int screenIndex) { emit screenChanged(screenIndex); } void Monitor::slotZoomIn() { m_glMonitor->slotZoom(true); } void Monitor::slotZoomOut() { m_glMonitor->slotZoom(false); } void Monitor::setConsumerProperty(const QString &name, const QString &value) { m_glMonitor->setConsumerProperty(name, value); } void Monitor::purgeCache() { m_glMonitor->purgeCache(); } diff --git a/src/profiles/profilerepository.cpp b/src/profiles/profilerepository.cpp index ce8b024a1..e4150fefa 100644 --- a/src/profiles/profilerepository.cpp +++ b/src/profiles/profilerepository.cpp @@ -1,220 +1,219 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "profilerepository.hpp" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "profilemodel.hpp" #include #include #include #include #include #include std::unique_ptr ProfileRepository::instance; std::once_flag ProfileRepository::m_onceFlag; std::vector> ProfileRepository::colorProfiles{ {601, QStringLiteral("ITU-R 601")}, {709, QStringLiteral("ITU-R 709")}, {240, QStringLiteral("SMPTE240M")}}; ProfileRepository::ProfileRepository() { refresh(); } std::unique_ptr &ProfileRepository::get() { std::call_once(m_onceFlag, [] { instance.reset(new ProfileRepository()); }); return instance; } void ProfileRepository::refresh(bool fullRefresh) { QWriteLocker locker(&m_mutex); if (fullRefresh) { // Reset all profiles m_profiles.clear(); } // Helper function to check a profile and print debug info auto check_profile = [&](std::unique_ptr &profile, const QString &file) { if (m_profiles.count(file) > 0) { return false; } if (!profile->is_valid()) { qCWarning(KDENLIVE_LOG) << "//// WARNING: invalid profile found: " << file << ". Ignoring."; return false; } return true; }; // list MLT profiles. QDir mltDir(KdenliveSettings::mltpath()); QStringList profilesFiles = mltDir.entryList(QDir::Files); // list Custom Profiles QStringList customProfilesDir = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("profiles/"), QStandardPaths::LocateDirectory); for (const auto &dir : customProfilesDir) { QStringList files = QDir(dir).entryList(QDir::Files); for (const auto &file : files) { profilesFiles << QDir(dir).absoluteFilePath(file); } } // Iterate through files for (const auto &file : profilesFiles) { std::unique_ptr profile(new ProfileModel(file)); if (check_profile(profile, file)) { m_profiles.insert(std::make_pair(file, std::move(profile))); } } } QVector> ProfileRepository::getAllProfiles() const { QReadLocker locker(&m_mutex); QVector> list; for (const auto &profile : m_profiles) { list.push_back({profile.second->description(), profile.first}); } std::sort(list.begin(), list.end()); return list; } std::unique_ptr &ProfileRepository::getProfile(const QString &path) { QReadLocker locker(&m_mutex); if (m_profiles.count(path) == 0) { // qCWarning(KDENLIVE_LOG) << "//// WARNING: profile not found: " << path << ". Returning default profile instead."; QString default_profile = KdenliveSettings::default_profile(); if (m_profiles.count(default_profile) == 0) { qCWarning(KDENLIVE_LOG) << "//// WARNING: default profile not found: " << default_profile << ". Returning random profile instead."; return (*(m_profiles.begin())).second; } return m_profiles.at(default_profile); } - return m_profiles.at(path); } bool ProfileRepository::profileExists(const QString &path) const { QReadLocker locker(&m_mutex); return m_profiles.count(path) != 0; } // static QString ProfileRepository::getColorspaceDescription(int colorspace) { // TODO: should the descriptions be translated? for (const auto &cs : colorProfiles) { if (cs.first == colorspace) return cs.second; } return i18n("Unknown"); } // static int ProfileRepository::getColorspaceFromDescription(const QString &description) { for (const auto &cs : colorProfiles) { if (cs.second == description) return cs.first; } return 0; } QVector ProfileRepository::getAllFps() const { QReadLocker locker(&m_mutex); QVector res; for (const auto &ptr : m_profiles) { res.push_back(ptr.second->fps()); } std::sort(res.begin(), res.end()); res.erase(std::unique(res.begin(), res.end()), res.end()); return res; } QString ProfileRepository::findMatchingProfile(ProfileInfo *profile) const { QReadLocker locker(&m_mutex); for (const auto &ptr : m_profiles) { if (*ptr.second.get() == *profile) { return ptr.first; } } return QString(); } const QString ProfileRepository::saveProfile(ProfileInfo *profile, QString profilePath) { if (profilePath.isEmpty()) { int i = 0; QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } QString customName = QStringLiteral("customprofile"); profilePath = dir.absoluteFilePath(customName + QString::number(i)); while (QFile::exists(profilePath)) { ++i; profilePath = dir.absoluteFilePath(customName + QString::number(i)); } } QFile file(profilePath); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::sorry(nullptr, i18n("Cannot open file %1", profilePath)); return QString(); } QTextStream out(&file); out << "description=" << profile->description() << '\n' << "frame_rate_num=" << profile->frame_rate_num() << '\n' << "frame_rate_den=" << profile->frame_rate_den() << '\n' << "width=" << profile->width() << '\n' << "height=" << profile->height() << '\n' << "progressive=" << static_cast(profile->progressive()) << '\n' << "sample_aspect_num=" << profile->sample_aspect_num() << '\n' << "sample_aspect_den=" << profile->sample_aspect_den() << '\n' << "display_aspect_num=" << profile->display_aspect_num() << '\n' << "display_aspect_den=" << profile->display_aspect_den() << '\n' << "colorspace=" << profile->colorspace() << '\n'; if (file.error() != QFile::NoError) { KMessageBox::error(nullptr, i18n("Cannot write to file %1", profilePath)); profilePath.clear(); } file.close(); refresh(false); return profilePath; } bool ProfileRepository::deleteProfile(const QString &path) { bool success = false; if (path.contains(QLatin1Char('/'))) { success = QFile::remove(path); } if (!success) { qCDebug(KDENLIVE_LOG) << "//// Cannot delete profile " << path << ", does not seem to be custom one"; } refresh(true); return success; } diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index cd447eb3e..d4bc5e932 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,3620 +1,3621 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #include #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock")) .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline")) .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))( parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))( parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))( parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo")) .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) .method("requestItemResize", select_overload(&TimelineModel::requestItemResize))( parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( parameter_names("itemIds", "logUndo", "type")) .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo")) .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))( parameter_names("pos", "id", "trackName", "audioTrack")) .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")) .method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion")) .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear")) .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId")) .method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds")) .method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks")) .method("suggestCompositionMove", &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance")) // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("requestCompositionInsertion", select_overload, int &, bool)>( // &TimelineModel::requestCompositionInsertion))( // parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo")) .method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","changeDuration")); } int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_blockRefresh(false) , m_tractor(new Mlt::Tractor(*profile)) , m_masterStack(nullptr) , m_snaps(new SnapModel()) , m_undoStack(std::move(undo_stack)) , m_profile(profile) - , m_blackClip(new Mlt::Producer(*profile, "color:black")) + , m_blackClip(new Mlt::Producer(*profile, "color:0")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_closing(false) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); + m_blackClip->set("mlt_image_format", "rgb24a"); m_blackClip->set("set.test_audio", 0); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } void TimelineModel::prepareClose() { requestClearSelection(true); QWriteLocker locker(&m_lock); // Unlock all tracks to allow delting clip from tracks m_closing = true; auto it = m_allTracks.begin(); while (it != m_allTracks.end()) { (*it)->unlock(); ++it; } } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.cbegin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isItem(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.cbegin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, int separated) const { if (separated == 1) { // This will be A2, A1, V1, V2 return getTrackPosition(trackId) + 1; } if (separated == 2) { // This will be A1, A2, V1, V2 // Count audio/video tracks auto it = m_allTracks.cbegin(); int aCount = 0; int vCount = 0; int refPos = 0; bool isVideo = true; while (it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { if ((*it)->getId() == trackId) { refPos = aCount; isVideo = false; } aCount++; } else { // video track if ((*it)->getId() == trackId) { refPos = vCount; } vCount++; } ++it; } return isVideo ? aCount + refPos + 1 : aCount - refPos; } // This will be A1, V1, A2, V2 auto it = m_allTracks.cend(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = qMax(0, aCount - vCount); if (trackDiff > 0) { // more audio tracks, keep them below if (isAudio && trackPos > vCount) { return -trackPos; } } return isAudio ? 2 * trackPos : 2 * (vCount + 1 - trackPos) + 1; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return (*it)->getId(); } } return 0; } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return getTrackMltIndex((*it)->getId()); } } return 0; } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; while (it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { count++; } else { count--; if (count == 0) { return (*it)->getId(); } } ++it; } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO"; return -1; } int count = 0; while (it != m_allTracks.cbegin()) { if (!(*it)->isAudioTrack()) { count++; } else { count--; if (count == 0) { return (*it)->getId(); } } --it; } if ((*it)->isAudioTrack() && count == 1) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; // qDebug()<<"MOVING CLIP FROM: "< 0); if (m_allClips[clipId]->getPosition() == position && m_allClips[clipId]->getFakeTrackId() == trackId) { TRACE_RES(true); qDebug()<<"........\nABORTING MOVE; SAME POS/TRACK\n.........."; return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); TRACE_RES(res); return res; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = sourceTrackId; } if (currentPos == position && m_editMode == TimelineMode::NormalEdit && sourceTrackId == trackId) { TRACE_RES(position); return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { TRACE_RES(position); return position; } if (sourceTrackId == -1) { // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away... TRACE_RES(currentPos); return currentPos; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId && sourceTrackId != -1) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { TRACE_RES(position); return position; } } int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { TRACE_RES(currentPos); return currentPos; } possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); TRACE_RES(possible ? position : currentPos); return possible ? position : currentPos; } if (trackId != sourceTrackId) { // Try same track move possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = 0; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1); if (blank_length == 0 || blank_length > track_space) { blank_length = track_space; } } else { // Check space after the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1; if (blank_length == 0 || blank_length > track_space) { blank_length = track_space; } } } if (snapDistance > 0) { if (blank_length > 10 * snapDistance) { blank_length = 0; } } else if (blank_length / m_profile->fps() > 5) { blank_length = 0; } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false); if (possible) { TRACE_RES(updatedPos); return updatedPos; } } TRACE_RES(currentPos); return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(compoId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (getTrackById_const(trackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = currentTrack; } if (currentPos == position && currentTrack == trackId) { TRACE_RES(position); return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { TRACE_RES(position); return position; } /*bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position;*/ TRACE_RES(currentPos); return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug() << " / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state; return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { QWriteLocker locker(&m_lock); TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo, QVector allowedTracks) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; if (trackId > -1 && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) { trackId = -1; } } else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) { // Video target set but locked trackId = m_audioTarget; if (trackId > -1 && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) { trackId = -1; } } if (trackId == -1) { if (!allowedTracks.isEmpty()) { // No active tracks, aborting return true; } pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); return false; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo); int target_track; if (audioDrop) { target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget; } else { target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget; } if (useTargets && !allowedTracks.contains(target_track)) { target_track = -1; } qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); int mirror = getMirrorTrackId(trackId); if (mirror > -1 && getTrackById_const(mirror)->isLocked()) { mirror = -1; } bool canMirrorDrop = !useTargets && mirror > -1; if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = mirror; } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, true, true, true, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, undo, redo); } if (isClip(itemId)) { return requestClipDeletion(itemId, undo, redo); } if (isComposition(itemId)) { return requestCompositionDeletion(itemId, undo, redo); } Q_ASSERT(false); return false; } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isItem(itemId)); QString actionLabel; if (m_groups->isInGroup(itemId)) { actionLabel = i18n("Remove group"); } else { if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); } else { actionLabel = i18n("Delete Composition"); } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestItemDeletion(itemId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } TRACE_RES(res); requestClearSelection(true); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo, false, true); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true); if (!res) { undo(); return false; } else { Fun unplant_op = [this, compositionId]() { unplantComposition(compositionId); return true; }; unplant_op(); PUSH_LAMBDA(unplant_op, redo); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; int new_in = composition->getPosition(); int new_out = new_in + composition->getPlaytime(); Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); composition->setCurrentTrackId(trackId, true); replantCompositions(compositionId, false); checkRefresh(new_in, new_out); return true; }; if (operation()) { Fun update_monitor = [this, new_in, new_out]() { checkRefresh(new_in, new_out); return true; }; update_monitor(); PUSH_LAMBDA(update_monitor, operation); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { if (track->isLocked()) { continue; } std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, moveMirrorTracks); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool moveMirrorTracks, bool allowViewRefresh, QVector allowedTracks) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); Q_ASSERT(isItem(itemId)); if (getGroupElements(groupId).count(itemId) == 0) { // this group doesn't contain the clip, abort return false; } bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; std::unordered_set all_clips; std::unordered_set all_compositions; // Separate clips from compositions to sort for (int affectedItemId : all_items) { if (isClip(affectedItemId)) { all_clips.insert(affectedItemId); } else { all_compositions.insert(affectedItemId); } } // Sort clips first std::vector sorted_clips(all_clips.begin(), all_clips.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) { int p1 = m_allClips[clipId1]->getPosition(); int p2 = m_allClips[clipId2]->getPosition(); return delta_pos > 0 ? p2 <= p1 : p1 <= p2; }); // Sort compositions. We need to delete in the move direction from top to bottom std::vector sorted_compositions(all_compositions.begin(), all_compositions.end()); std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) { int p1 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2; }); sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end()); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = [this, finalMove]() { if (finalMove) { updateDuration(); } return true; }; // Check if there is a track move bool updatePositionOnly = false; // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, finalMove, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } if (finalMove) { updateDuration(); } return true; }; } std::unordered_map old_track_ids, old_position, old_forced_track; // First, remove clips if (delta_track != 0) { // We delete our clips only if changing track for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, true, false); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } if (!moveMirrorTracks) { if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = 0; } else { audio_delta = 0; } } else { if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } } } // We need to insert depending on the move direction to avoid confusing the view // std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); bool updateThisView = allowViewRefresh; if (delta_track == 0) { // Special case, we are moving on same track, avoid too many calculations for (int item : sorted_clips) { int current_track_id = getItemTrackId(item); if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) { continue; } int target_position = getItemPosition(item) + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } else { ok = ok && requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } else { // Track changed for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, logUndo); if (!m_groups->isInGroup(clipId)) { TRACE_RES(false); return false; } bool res = requestItemDeletion(clipId, logUndo); TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); bool isSelection = m_currentSelection == current_group; if (isSelection) { m_currentSelection = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { if (m_groups->getType(current_group) == GroupType::Selection) { Q_ASSERT(isSelection); // in the case of a selection group, we delete the group but don't log it in the undo object Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; m_groups->ungroupItem(one_child, tmp_undo, tmp_redo); } else { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { // Undo is processed in requestClipDeletion return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } const QVariantList TimelineModel::getGroupData(int itemId) { QWriteLocker locker(&m_lock); if (!m_groups->isInGroup(itemId)) { return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)}; } int groupId = m_groups->getRootId(itemId); QVariantList result; std::unordered_set items = m_groups->getLeaves(groupId); for (int id : items) { result << id << getItemPosition(id) << getItemPlaytime(id); } return result; } void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right) { Q_ASSERT(startPos.size() == endPos.size()); QMap> startData; QMap> endData; while (!startPos.isEmpty()) { int id = startPos.takeFirst().toInt(); int in = startPos.takeFirst().toInt(); int duration = startPos.takeFirst().toInt(); startData.insert(id, {in, duration}); id = endPos.takeFirst().toInt(); in = endPos.takeFirst().toInt(); duration = endPos.takeFirst().toInt(); endData.insert(id, {in, duration}); } QMapIterator> i(startData); QList changedItems; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; while (i.hasNext()) { i.next(); QPair startItemPos = i.value(); QPair endItemPos = endData.value(i.key()); if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) { // Revert individual items to original position requestItemResize(i.key(), startItemPos.second, right, false, 0, true); changedItems << i.key(); } } for (int id : changedItems) { QPair endItemPos = endData.value(id); result = result & requestItemResize(id, endItemPos.second, right, true, undo, redo, false); if (!result) { break; } } if (result) { PUSH_UNDO(undo, redo, i18n("Resize group")); } else { undo(); } } const std::vector TimelineModel::getBoundaries(int itemId) { std::vector boundaries; std::unordered_set items; if (m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); items = m_groups->getLeaves(groupId); } else { items.insert(itemId); } for (int id : items) { if (isClip(id) || isComposition(id)) { int in = getItemPosition(id); int out = in + getItemPlaytime(id); boundaries.push_back(in); boundaries.push_back(out); } } return boundaries; } int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed) { QWriteLocker locker(&m_lock); TRACE(itemId, size, right, true, snapDistance, allowSingleResize); Q_ASSERT(isClip(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); //size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } else { all_items.insert(itemId); } for (int id : items) { if (id == itemId) { all_items.insert(id); continue; } int start = getItemPosition(id); int end = in + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; for (int id : all_items) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } // First delete clip, then timewarp, resize and reinsert int pos = getItemPosition(id); if (!right) { pos += getItemPlaytime(id) - size; } result = getTrackById(tid)->requestClipDeletion(id, true, true, undo, redo, false, true); result = result && requestClipTimeWarp(id, speed, false, undo, redo); result = result && requestItemResize(id, size, true, true, undo, redo); result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, true, undo, redo); if (!result) { break; } } if (!result) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result) { PUSH_UNDO(undo, redo, i18n("Resize clip speed")); } int res = result ? size : -1; TRACE_RES(res); return res; } int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance) { if (snapDistance > 0 && getItemTrackId(itemId) != -1) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; if (right && size > out - in && isClip(itemId)) { int targetPos = in + size - 1; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; } } else if (!right && size > (out - in) && isClip(itemId)) { int targetPos = out - size; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = out - getTrackById_const(trackId)->getBlankStart(in - 1); } } int timelinePos = pCore->getTimelinePosition(); m_snaps->addPoint(timelinePos); int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); m_snaps->removePoint(timelinePos); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } return size; } int TimelineModel::requestItemSpeedChange(int itemId, int size, bool right, int snapDistance) { Q_ASSERT(isClip(itemId)); QWriteLocker locker(&m_lock); TRACE(itemId, size, right, snapDistance); Q_ASSERT(isItem(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (right && size > out - in) { int targetPos = in + size - 1; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, out + 1, targetPos, false).empty()) { size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; } } else if (!right && size > (out - in)) { int targetPos = out - size; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, targetPos, in - 1, false).empty()) { size = out - getTrackById_const(trackId)->getBlankStart(in - 1); } } int timelinePos = pCore->getTimelinePosition(); m_snaps->addPoint(timelinePos); int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); m_snaps->removePoint(timelinePos); qDebug()<<"==== RESIZE REQUEST: "< 0 ? proposed_size : size; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isItem(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } all_items.insert(itemId); for (int id : items) { if (id == itemId) { continue; } int start = getItemPosition(id); int end = start + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; int finalPos = right ? in + size : out - size; int finalSize; int resizedCount = 0; for (int id : all_items) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } if (right) { finalSize = finalPos - getItemPosition(id); } else { finalSize = getItemPosition(id) + getItemPlaytime(id) - finalPos; } result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo); resizedCount++; } if (!result || resizedCount == 0) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } int res = result ? size : -1; TRACE_RES(res); return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); TRACE(ids, logUndo, type); if (type == GroupType::Selection || type == GroupType::Leaf) { // Selections shouldn't be done here. Call requestSetSelection instead TRACE_RES(-1); return -1; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; int result = requestClipsGroup(ids, undo, redo, type); if (result > -1 && logUndo) { PUSH_UNDO(undo, redo, i18n("Group clips")); } TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { QWriteLocker locker(&m_lock); if (type != GroupType::Selection) { requestClearSelection(); } int clipsCount = 0; QList tracks; for (int id : ids) { if (isClip(id)) { int trackId = getClipTrackId(id); if (trackId == -1) { return -1; } tracks << trackId; clipsCount++; } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } if (type == GroupType::Selection && ids.size() == 1) { // only one element selected, no group created return -1; } if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) { // Check if we are grouping an AVSplit std::unordered_set::const_iterator it = ids.begin(); int firstId = *it; std::advance(it, 1); int secondId = *it; bool isAVGroup = false; if (getClipBinId(firstId) == getClipBinId(secondId)) { if (getClipState(firstId) == PlaylistState::AudioOnly) { if (getClipState(secondId) == PlaylistState::VideoOnly) { isAVGroup = true; } } else if (getClipState(secondId) == PlaylistState::AudioOnly) { isAVGroup = true; } } if (isAVGroup) { type = GroupType::AVSplit; } } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type != GroupType::Selection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return groupId; } bool TimelineModel::requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemIds, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; requestClearSelection(); std::unordered_set roots; std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); for (int root : roots) { if (isGroup(root)) { result = result && requestClipUngroup(root, undo, redo); } } if (!result) { bool undone = undo(); Q_ASSERT(undone); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); requestClearSelection(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; result = requestClipUngroup(itemId, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection; if (!isSelection) { requestClearSelection(); } bool res = m_groups->ungroupItem(itemId, undo, redo); if (res && !isSelection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return res; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo, true); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int previousId = -1; if (position < (int)m_allTracks.size()) { previousId = getTrackIndexFromPosition(position); } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); // Adjust compositions that were affecting track at previous pos Fun local_update = [previousId, position, this]() { if (previousId > -1) { for (auto &compo : m_allCompositions) { if (compo.second->getATrack() == position && compo.second->getForcedTrack() == -1) { compo.second->setATrack(position + 1, previousId); } } } return true; }; local_update(); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, local_update, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, true); local_update(); if (updateView) { _resetView(); } return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); if (updateView) { _resetView(); } return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track QWriteLocker locker(&m_lock); TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); if (m_allTracks.size() < 2) { pCore->displayMessage(i18n("Cannot delete last track in timeline"), InformationMessage, 500); return false; } std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); _resetView(); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert) { // qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; beginInsertRows(QModelIndex(), pos, pos); endInsertRows(); int cache = (int)QThread::idealThreadCount() + ((int)m_allTracks.size() + 1) * 2; mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache)); } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { // qDebug() << "DEREGISTER TRACK" << id; emit checkTrackDeletion(id); auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table beginRemoveRows(QModelIndex(), index, index); endInsertRows(); if (updateView) { _resetView(); } int cache = (int)QThread::idealThreadCount() + ((int)m_allTracks.size() + 1) * 2; mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache)); return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); requestClearSelection(true); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isItem(int id) const { return isClip(id) || isComposition(id); } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { if (m_closing) { return; } int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set("out", duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::getBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::getNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::getPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { TRACE(pos); return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { TRACE(pos); return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo) { QWriteLocker locker(&m_lock); // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } // TRACE_RES(result); return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, std::move(transProps)); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point requestClearSelection(true); clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); notifyChange(modelIndex, modelIndex, StartRole); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo, false); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of a_track, and increasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.emplace_back(trackPos, compo.first); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [&](const std::pair &a, const std::pair &b) { if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) { return a.first < b.first; } return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack(); }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count()); Mlt::Transition &transition = *m_allCompositions[compo.second].get(); int ret = field->plant_transition(transition, aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); notifyChange(modelIndex, modelIndex, ItemATrack); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } if (!clip->checkConsistency()) { qDebug() << "Consistency check failed for clip" << cp.first; return false; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { auto tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } // Check that the selection is in a valid state: if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isGroup(m_currentSelection)) { qDebug() << "Selection is in inconsistent state"; return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } std::shared_ptr TimelineModel::producer() { return std::make_shared(tractor()); } void TimelineModel::checkRefresh(int start, int end) { if (m_blockRefresh) { return; } int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } std::shared_ptr TimelineModel::getMasterEffectStackModel() { READ_LOCK(); if (m_masterStack == nullptr) { m_masterService.reset(new Mlt::Service(*m_tractor.get())); m_masterStack = EffectStackModel::construct(m_masterService, {ObjectType::Master, 0}, m_undoStack); } return m_masterStack; } void TimelineModel::importMasterEffects(std::weak_ptr service) { READ_LOCK(); if (m_masterStack == nullptr) { getMasterEffectStackModel(); } m_masterStack->importEffects(std::move(service), PlaylistState::Disabled); } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo, false, false); } if (old_trackId != -1) { m_allClips[clipId]->refreshProducerFromBin(); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool changeDuration, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, changeDuration, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool changeDuration) { QWriteLocker locker(&m_lock); TRACE(clipId, speed); Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, changeDuration, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, changeDuration, undo, redo); } if (!result) { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); TRACE_RES(false); return false; } } else { // If clip is not inserted on a track, we just change the producer result = m_allClips[clipId]->useTimewarpProducer(speed, changeDuration, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); } TRACE_RES(result); return result; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.cbegin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.cbegin()) { --it; if ((*it)->isAudioTrack() == audioWanted) { return (*it)->getId(); } } return trackId; } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.cend()) { ++it; if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.cend() ? trackId : (*it)->getId(); } bool TimelineModel::requestClearSelection(bool onDeletion) { QWriteLocker locker(&m_lock); TRACE(); if (m_currentSelection == -1) { TRACE_RES(true); return true; } if (isGroup(m_currentSelection)) { // Reset offset display on clips std::unordered_set items = m_groups->getLeaves(m_currentSelection); for (auto &id : items) { if (isGroup(id)) { std::unordered_set children = m_groups->getLeaves(id); items.insert(children.begin(), children.end()); } else if (isClip(id)) { m_allClips[id]->clearOffset(); m_allClips[id]->setGrab(false); m_allClips[id]->setSelected(false); } else if (isComposition(id)) { m_allCompositions[id]->setGrab(false); m_allCompositions[id]->setSelected(false); } if (m_groups->getType(m_currentSelection) == GroupType::Selection) { m_groups->destructGroupItem(m_currentSelection); } } } else { if (isClip(m_currentSelection)) { m_allClips[m_currentSelection]->setGrab(false); m_allClips[m_currentSelection]->setSelected(false); } else if (isComposition(m_currentSelection)) { m_allCompositions[m_currentSelection]->setGrab(false); m_allCompositions[m_currentSelection]->setSelected(false); } Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection)); } m_currentSelection = -1; emit selectionChanged(); TRACE_RES(true); return true; } void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo) { Fun operation = [this, onDeletion]() { requestClearSelection(onDeletion); return true; }; Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } std::unordered_set TimelineModel::getCurrentSelection() const { READ_LOCK(); if (m_currentSelection == -1) { return {}; } if (isGroup(m_currentSelection)) { return m_groups->getLeaves(m_currentSelection); } else { Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection)); return {m_currentSelection}; } } void TimelineModel::requestAddToSelection(int itemId, bool clear) { QWriteLocker locker(&m_lock); TRACE(itemId, clear); if (clear) { requestClearSelection(); } std::unordered_set selection = getCurrentSelection(); if (selection.count(itemId) == 0) { selection.insert(itemId); requestSetSelection(selection); } } void TimelineModel::requestRemoveFromSelection(int itemId) { QWriteLocker locker(&m_lock); TRACE(itemId); std::unordered_set all_items = {itemId}; int parentGroup = m_groups->getDirectAncestor(itemId); if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) { all_items = m_groups->getLeaves(parentGroup); } std::unordered_set selection = getCurrentSelection(); for (int current_itemId : all_items) { if (selection.count(current_itemId) > 0) { selection.erase(current_itemId); } } requestSetSelection(selection); } bool TimelineModel::requestSetSelection(const std::unordered_set &ids) { QWriteLocker locker(&m_lock); TRACE(ids); requestClearSelection(); // if the items are in groups, we must retrieve their topmost containing groups std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); bool result = true; if (roots.size() == 0) { m_currentSelection = -1; } else if (roots.size() == 1) { m_currentSelection = *(roots.begin()); setSelected(m_currentSelection, true); } else { Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (ids.size() == 2) { // Check if we selected 2 clips from the same master QList pairIds; for (auto &id : roots) { if (isClip(id)) { pairIds << id; } } if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) { // Check if they have same bin id // Both clips have same bin ID, display offset int pos1 = getClipPosition(pairIds.at(0)); int pos2 = getClipPosition(pairIds.at(1)); if (pos2 > pos1) { int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0))); if (offset != 0) { m_allClips[pairIds.at(1)]->setOffset(offset); m_allClips[pairIds.at(0)]->setOffset(-offset); } } else { int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1))); if (offset != 0) { m_allClips[pairIds.at(0)]->setOffset(offset); m_allClips[pairIds.at(1)]->setOffset(-offset); } } } } result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0; Q_ASSERT(m_currentSelection >= 0); } emit selectionChanged(); return result; } void TimelineModel::setSelected(int itemId, bool sel) { if (isClip(itemId)) { m_allClips[itemId]->setSelected(sel); } else if (isComposition(itemId)) { m_allCompositions[itemId]->setSelected(sel); } else if (isGroup(itemId)) { auto leaves = m_groups->getLeaves(itemId); for (auto &id : leaves) { setSelected(id, true); } } } bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Fun reverse = [this]() { requestClearSelection(false); return true; }; Fun operation = [this, ids]() { return requestSetSelection(ids); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } void TimelineModel::setTrackLockedState(int trackId, bool lock) { QWriteLocker locker(&m_lock); TRACE(trackId, lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; Fun lock_lambda = [this, trackId]() { getTrackById(trackId)->lock(); return true; }; Fun unlock_lambda = [this, trackId]() { getTrackById(trackId)->unlock(); return true; }; if (lock) { if (lock_lambda()) { UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Lock track")); } } else { if (unlock_lambda()) { UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Unlock track")); } } } std::unordered_set TimelineModel::getAllTracksIds() const { READ_LOCK(); std::unordered_set result; std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; }); return result; } void TimelineModel::switchComposition(int cid, const QString &compoId) { Q_ASSERT(isComposition(cid)); std::shared_ptr compo = m_allCompositions.at(cid); int currentPos = compo->getPosition(); int duration = compo->getPlaytime(); int currentTrack = compo->getCurrentTrackId(); int a_track = compo->getATrack(); int forcedTrack = compo->getForcedTrack(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Clear selection requestClearSelection(true); if (m_groups->isInGroup(cid)) { pCore->displayMessage(i18n("Cannot operate on grouped composition, please ungroup"), ErrorMessage); return; } bool res = requestCompositionDeletion(cid, undo, redo); int newId; res = res && requestCompositionInsertion(compoId, currentTrack, a_track, currentPos, duration, nullptr, newId, undo, redo); if (res) { if (forcedTrack > -1 && isComposition(newId)) { m_allCompositions[newId]->setForceTrack(true); } Fun local_redo = [newId, this]() { requestSetSelection({newId}); return true; }; Fun local_undo = [cid, this]() { requestSetSelection({cid}); return true; }; local_redo(); PUSH_LAMBDA(local_redo, redo); PUSH_LAMBDA(local_undo, undo); PUSH_UNDO(undo, redo, i18n("Change composition")); } else { undo(); } } diff --git a/src/timeline2/view/previewmanager.cpp b/src/timeline2/view/previewmanager.cpp index e324bc07b..99f0fdd59 100644 --- a/src/timeline2/view/previewmanager.cpp +++ b/src/timeline2/view/previewmanager.cpp @@ -1,778 +1,780 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "previewmanager.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "profiles/profilemodel.hpp" #include "timeline2/view/timelinecontroller.h" #include #include #include #include PreviewManager::PreviewManager(TimelineController *controller, Mlt::Tractor *tractor) : QObject() , workingPreview(-1) , m_controller(controller) , m_tractor(tractor) , m_previewTrack(nullptr) , m_overlayTrack(nullptr) , m_previewTrackIndex(-1) , m_initialized(false) { m_previewGatherTimer.setSingleShot(true); m_previewGatherTimer.setInterval(200); QObject::connect(&m_previewProcess, QOverload::of(&QProcess::finished), this, &PreviewManager::processEnded); // Find path for Kdenlive renderer #ifdef Q_OS_WIN m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe"); #else m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render"); #endif if (!QFile::exists(m_renderer)) { m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render")); if (m_renderer.isEmpty()) { m_renderer = QStringLiteral("kdenlive_render"); } } connect(this, &PreviewManager::abortPreview, &m_previewProcess, &QProcess::kill, Qt::DirectConnection); connect(&m_previewProcess, &QProcess::readyReadStandardError, this, &PreviewManager::receivedStderr); } PreviewManager::~PreviewManager() { if (m_initialized) { abortRendering(); if (m_undoDir.dirName() == QLatin1String("undo")) { m_undoDir.removeRecursively(); } if ((pCore->currentDoc()->url().isEmpty() && m_cacheDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty()) || m_cacheDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) { if (m_cacheDir.dirName() == QLatin1String("preview")) { m_cacheDir.removeRecursively(); } } } delete m_overlayTrack; delete m_previewTrack; } bool PreviewManager::initialize() { // Make sure our document id does not contain .. tricks bool ok; KdenliveDoc *doc = pCore->currentDoc(); QString documentId = QDir::cleanPath(doc->getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (!ok || documentId.isEmpty()) { // Something is wrong, documentId should be a number (ms since epoch), abort pCore->displayMessage(i18n("Wrong document ID, cannot create temporary folder"), ErrorMessage); return false; } m_cacheDir = doc->getCacheDir(CachePreview, &ok); if (!m_cacheDir.exists() || !ok) { pCore->displayMessage(i18n("Cannot create folder %1", m_cacheDir.absolutePath()), ErrorMessage); return false; } if (m_cacheDir.dirName() != QLatin1String("preview") || m_cacheDir == QDir() || (!m_cacheDir.exists(QStringLiteral("undo")) && !m_cacheDir.mkdir(QStringLiteral("undo"))) || !m_cacheDir.absolutePath().contains(documentId)) { pCore->displayMessage(i18n("Something is wrong with cache folder %1", m_cacheDir.absolutePath()), ErrorMessage); return false; } if (!loadParams()) { pCore->displayMessage(i18n("Invalid timeline preview parameters"), ErrorMessage); return false; } m_undoDir = QDir(m_cacheDir.absoluteFilePath(QStringLiteral("undo"))); // Make sure our cache dirs are inside the temporary folder if (!m_cacheDir.makeAbsolute() || !m_undoDir.makeAbsolute() || !m_undoDir.mkpath(QStringLiteral("."))) { pCore->displayMessage(i18n("Something is wrong with cache folders"), ErrorMessage); return false; } connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews); connect(doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo, Qt::DirectConnection); m_previewTimer.setSingleShot(true); m_previewTimer.setInterval(3000); connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender); connect(this, &PreviewManager::previewRender, this, &PreviewManager::gotPreviewRender, Qt::DirectConnection); connect(&m_previewGatherTimer, &QTimer::timeout, this, &PreviewManager::slotProcessDirtyChunks); m_initialized = true; return true; } bool PreviewManager::buildPreviewTrack() { if (m_previewTrack != nullptr) { return false; } // Create overlay track qDebug() << "/// BUILDING PREVIEW TRACK\n----------------------\n----------------__"; m_previewTrack = new Mlt::Playlist(pCore->getCurrentProfile()->profile()); m_tractor->lock(); reconnectTrack(); m_tractor->unlock(); return true; } void PreviewManager::loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, const QDateTime &documentDate) { if (previewChunks.isEmpty()) { previewChunks = m_renderedChunks; } if (dirtyChunks.isEmpty()) { dirtyChunks = m_dirtyChunks; } for (const auto &frame : previewChunks) { const QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(frame.toInt()).arg(m_extension)); QFile file(fileName); if (file.exists()) { if (!documentDate.isNull() && QFileInfo(file).lastModified() > documentDate) { // Timeline preview file was created after document, invalidate file.remove(); dirtyChunks << frame; } else { gotPreviewRender(frame.toInt(), fileName, 1000); } } else { dirtyChunks << frame; } } if (!previewChunks.isEmpty()) { m_controller->renderedChunksChanged(); } if (!dirtyChunks.isEmpty()) { for (const auto &i : dirtyChunks) { if (!m_dirtyChunks.contains(i)) { m_dirtyChunks << i; } } m_controller->dirtyChunksChanged(); } } void PreviewManager::deletePreviewTrack() { m_tractor->lock(); disconnectTrack(); delete m_previewTrack; m_previewTrack = nullptr; m_dirtyChunks.clear(); m_renderedChunks.clear(); m_controller->dirtyChunksChanged(); m_controller->renderedChunksChanged(); m_tractor->unlock(); } const QDir PreviewManager::getCacheDir() const { return m_cacheDir; } void PreviewManager::reconnectTrack() { disconnectTrack(); if (!m_previewTrack && !m_overlayTrack) { m_previewTrackIndex = -1; return; } m_previewTrackIndex = m_tractor->count(); int increment = 0; if (m_previewTrack) { m_tractor->insert_track(*m_previewTrack, m_previewTrackIndex); std::shared_ptr tk(m_tractor->track(m_previewTrackIndex)); tk->set("hide", 2); tk->set("id", "timeline_preview"); increment++; } if (m_overlayTrack) { m_tractor->insert_track(*m_overlayTrack, m_previewTrackIndex + increment); std::shared_ptr tk(m_tractor->track(m_previewTrackIndex + increment)); tk->set("hide", 2); tk->set("id", "timeline_overlay"); } } void PreviewManager::disconnectTrack() { if (m_previewTrackIndex > -1) { Mlt::Producer *prod = m_tractor->track(m_previewTrackIndex); if (strcmp(prod->get("id"), "timeline_preview") == 0 || strcmp(prod->get("id"), "timeline_overlay") == 0) { m_tractor->remove_track(m_previewTrackIndex); } delete prod; if (m_tractor->count() == m_previewTrackIndex + 1) { // overlay track still here, remove Mlt::Producer *trkprod = m_tractor->track(m_previewTrackIndex); if (strcmp(trkprod->get("id"), "timeline_overlay") == 0) { m_tractor->remove_track(m_previewTrackIndex); } delete trkprod; } } m_previewTrackIndex = -1; } bool PreviewManager::loadParams() { KdenliveDoc *doc = pCore->currentDoc(); m_extension = doc->getDocumentProperty(QStringLiteral("previewextension")); m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts); if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { doc->selectPreviewProfile(); m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts); m_extension = doc->getDocumentProperty(QStringLiteral("previewextension")); } if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { return false; } // Remove the r= and s= parameter (forcing framerate / frame size) as it causes rendering failure. // These parameters should be provided by MLT's profile // NOTE: this is still required for DNxHD so leave it /*for (int i = 0; i < m_consumerParams.count(); i++) { if (m_consumerParams.at(i).startsWith(QStringLiteral("r=")) || m_consumerParams.at(i).startsWith(QStringLiteral("s="))) { m_consumerParams.removeAt(i); i--; } }*/ if (doc->getDocumentProperty(QStringLiteral("resizepreview")).toInt() != 0) { int resizeWidth = doc->getDocumentProperty(QStringLiteral("previewheight")).toInt(); m_consumerParams << QStringLiteral("s=%1x%2").arg((int)(resizeWidth * pCore->getCurrentDar())).arg(resizeWidth); } m_consumerParams << QStringLiteral("an=1"); if (KdenliveSettings::gpu_accel()) { m_consumerParams << QStringLiteral("glsl.=1"); } return true; } void PreviewManager::invalidatePreviews(const QVariantList chunks) { QMutexLocker lock(&m_previewMutex); bool timer = KdenliveSettings::autopreview(); if (m_previewTimer.isActive()) { m_previewTimer.stop(); timer = true; } KdenliveDoc *doc = pCore->currentDoc(); int stackIx = doc->commandStack()->index(); int stackMax = doc->commandStack()->count(); if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) { // We just added a new command in stack, archive existing chunks int ix = stackIx - 1; m_undoDir.mkdir(QString::number(ix)); bool foundPreviews = false; for (const auto &i : chunks) { QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(ix).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { // No preview files found, remove undo folder m_undoDir.rmdir(QString::number(ix)); } else { // new chunks archived, cleanup old ones emit cleanupOldPreviews(); } } else { // Restore existing chunks, delete others // Check if we just undo the last stack action, then backup, otherwise delete bool lastUndo = false; if (stackIx + 1 == stackMax) { if (!m_undoDir.exists(QString::number(stackMax))) { lastUndo = true; bool foundPreviews = false; m_undoDir.mkdir(QString::number(stackMax)); for (const auto &i : chunks) { QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(stackMax).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { m_undoDir.rmdir(QString::number(stackMax)); } } } bool moveFile = true; QDir tmpDir = m_undoDir; if (!tmpDir.cd(QString::number(stackIx))) { moveFile = false; } QVariantList foundChunks; for (const auto &i : chunks) { QString cacheFileName = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (!lastUndo) { m_cacheDir.remove(cacheFileName); } if (moveFile) { if (QFile::copy(tmpDir.absoluteFilePath(cacheFileName), m_cacheDir.absoluteFilePath(cacheFileName))) { foundChunks << i; m_dirtyChunks.removeAll(i); m_renderedChunks << i; } else { qDebug() << "// ERROR PROCESSE CHUNK: " << i << ", " << cacheFileName; } } } if (!foundChunks.isEmpty()) { std::sort(foundChunks.begin(), foundChunks.end()); m_controller->dirtyChunksChanged(); m_controller->renderedChunksChanged(); reloadChunks(foundChunks); } } doc->setModified(true); if (timer) { m_previewTimer.start(); } } void PreviewManager::doCleanupOldPreviews() { if (m_undoDir.dirName() != QLatin1String("undo")) { return; } QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); // Use QCollator to do a natural sorting so that 10 is after 2 QCollator collator; collator.setNumericMode(true); std::sort(dirs.begin(), dirs.end(), [&collator](const QString &file1, const QString &file2) { return collator.compare(file1, file2) < 0; }); bool ok; while (dirs.count() > 5) { QDir tmp = m_undoDir; QString dirName = dirs.takeFirst(); dirName.toInt(&ok); if (ok && tmp.cd(dirName)) { tmp.removeRecursively(); } } } void PreviewManager::clearPreviewRange(bool resetZones) { m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; for (const auto &ix : m_renderedChunks) { m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension)); if (!m_dirtyChunks.contains(ix)) { m_dirtyChunks << ix; } if (!hasPreview) { continue; } int trackIx = m_previewTrack->get_clip_index_at(ix.toInt()); if (!m_previewTrack->is_blank(trackIx)) { Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx); delete prod; } } if (hasPreview) { m_previewTrack->consolidate_blanks(); } m_tractor->unlock(); m_renderedChunks.clear(); // Reload preview params loadParams(); if (resetZones) { m_dirtyChunks.clear(); } m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); } void PreviewManager::addPreviewRange(const QPoint zone, bool add) { int chunkSize = KdenliveSettings::timelinechunks(); int startChunk = zone.x() / chunkSize; int endChunk = rintl(zone.y() / chunkSize); QList toRemove; qDebug() << " // / RESUQEST CHUNKS; " << startChunk << " = " << endChunk; for (int i = startChunk; i <= endChunk; i++) { int frame = i * chunkSize; if (add) { if (!m_renderedChunks.contains(frame) && !m_dirtyChunks.contains(frame)) { m_dirtyChunks << frame; } } else { if (m_renderedChunks.contains(frame)) { toRemove << frame; m_renderedChunks.removeAll(frame); } else { m_dirtyChunks.removeAll(frame); } } } if (add) { qDebug() << "CHUNKS CHANGED: " << m_dirtyChunks; m_controller->dirtyChunksChanged(); if (m_previewProcess.state() == QProcess::NotRunning && KdenliveSettings::autopreview()) { m_previewTimer.start(); } } else { // Remove processed chunks bool isRendering = m_previewProcess.state() != QProcess::NotRunning; m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; for (int ix : toRemove) { m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix).arg(m_extension)); if (!hasPreview) { continue; } int trackIx = m_previewTrack->get_clip_index_at(ix); if (!m_previewTrack->is_blank(trackIx)) { Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx); delete prod; } } if (hasPreview) { m_previewTrack->consolidate_blanks(); } m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); m_tractor->unlock(); if (isRendering || KdenliveSettings::autopreview()) { m_previewTimer.start(); } } } void PreviewManager::abortRendering() { if (m_previewProcess.state() == QProcess::NotRunning) { return; } qDebug() << "/// ABORTING RENDEIGN 1\nRRRRRRRRRR"; emit abortPreview(); m_previewProcess.waitForFinished(); if (m_previewProcess.state() != QProcess::NotRunning) { m_previewProcess.kill(); m_previewProcess.waitForFinished(); } // Re-init time estimation emit previewRender(-1, QString(), 1000); } void PreviewManager::startPreviewRender() { QMutexLocker lock(&m_previewMutex); if (m_renderedChunks.isEmpty() && m_dirtyChunks.isEmpty()) { m_controller->addPreviewRange(true); } if (!m_dirtyChunks.isEmpty()) { // Abort any rendering abortRendering(); m_waitingThumbs.clear(); // clear log m_errorLog.clear(); const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt")); pCore->getMonitor(Kdenlive::ProjectMonitor)->sceneList(m_cacheDir.absolutePath(), sceneList); pCore->currentDoc()->saveMltPlaylist(sceneList); m_previewTimer.stop(); doPreviewRender(sceneList); } } void PreviewManager::receivedStderr() { QStringList resultList = QString::fromLocal8Bit(m_previewProcess.readAllStandardError()).split(QLatin1Char('\n')); for (auto &result : resultList) { qDebug() << "GOT PROCESS RESULT: " << result; if (result.startsWith(QLatin1String("START:"))) { workingPreview = result.section(QLatin1String("START:"), 1).simplified().toInt(); qDebug() << "// GOT START INFO: " << workingPreview; m_controller->workingPreviewChanged(); } else if (result.startsWith(QLatin1String("DONE:"))) { int chunk = result.section(QLatin1String("DONE:"), 1).simplified().toInt(); m_processedChunks++; QString fileName = QStringLiteral("%1.%2").arg(chunk).arg(m_extension); qDebug() << "---------------\nJOB PROGRRESS: " << m_chunksToRender << ", " << m_processedChunks << " = " << (100 * m_processedChunks / m_chunksToRender); emit previewRender(chunk, m_cacheDir.absoluteFilePath(fileName), 1000 * m_processedChunks / m_chunksToRender); } else { m_errorLog.append(result); } } } void PreviewManager::doPreviewRender(const QString &scene) { // initialize progress bar std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end()); if (m_dirtyChunks.isEmpty()) { return; } Q_ASSERT(m_previewProcess.state() == QProcess::NotRunning); QStringList chunks; for (QVariant &frame : m_dirtyChunks) { chunks << frame.toString(); } m_chunksToRender = m_dirtyChunks.count(); m_processedChunks = 0; int chunkSize = KdenliveSettings::timelinechunks(); QStringList args{KdenliveSettings::rendererpath(), scene, m_cacheDir.absolutePath(), QStringLiteral("-split"), chunks.join(QLatin1Char(',')), QString::number(chunkSize - 1), pCore->getCurrentProfilePath(), m_extension, m_consumerParams.join(QLatin1Char(' '))}; qDebug() << " - - -STARTING PREVIEW JOBS: " << args; pCore->currentDoc()->previewProgress(0); m_previewProcess.start(m_renderer, args); if (m_previewProcess.waitForStarted()) { qDebug() << " - - -STARTING PREVIEW JOBS . . . STARTED"; } } void PreviewManager::processEnded(int, QProcess::ExitStatus status) { qDebug() << "// PROCESS IS FINISHED!!!"; const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt")); QFile::remove(sceneList); if (status == QProcess::QProcess::CrashExit) { qDebug() << "// PROCESS CRASHED!!!!!!"; pCore->currentDoc()->previewProgress(-1); if (workingPreview >= 0) { const QString fileName = QStringLiteral("%1.%2").arg(workingPreview).arg(m_extension); if (m_cacheDir.exists(fileName)) { m_cacheDir.remove(fileName); } } } else { pCore->currentDoc()->previewProgress(1000); } workingPreview = -1; m_controller->workingPreviewChanged(); } void PreviewManager::slotProcessDirtyChunks() { if (m_dirtyChunks.isEmpty()) { return; } invalidatePreviews(m_dirtyChunks); if (KdenliveSettings::autopreview()) { m_previewTimer.start(); } } void PreviewManager::slotRemoveInvalidUndo(int ix) { QMutexLocker lock(&m_previewMutex); if (m_undoDir.dirName() != QLatin1String("undo")) { // Make sure we delete correct folder return; } QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); bool ok; for (const QString &dir : dirs) { if (dir.toInt(&ok) >= ix && ok) { QDir tmp = m_undoDir; if (tmp.cd(dir)) { tmp.removeRecursively(); } } } } void PreviewManager::invalidatePreview(int startFrame, int endFrame) { int chunkSize = KdenliveSettings::timelinechunks(); int start = startFrame - startFrame % chunkSize; int end = endFrame - endFrame % chunkSize + chunkSize; std::sort(m_renderedChunks.begin(), m_renderedChunks.end()); m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; bool chunksChanged = false; for (int i = start; i <= end; i += chunkSize) { if (m_renderedChunks.contains(i) && hasPreview) { int ix = m_previewTrack->get_clip_index_at(i); if (m_previewTrack->is_blank(ix)) { continue; } Mlt::Producer *prod = m_previewTrack->replace_with_blank(ix); delete prod; QVariant val(i); m_renderedChunks.removeAll(val); if (!m_dirtyChunks.contains(val)) { m_dirtyChunks << val; chunksChanged = true; } } } if (chunksChanged) { m_previewTrack->consolidate_blanks(); m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); } m_tractor->unlock(); m_previewGatherTimer.start(); } void PreviewManager::reloadChunks(const QVariantList chunks) { if (m_previewTrack == nullptr || chunks.isEmpty()) { return; } m_tractor->lock(); for (const auto &ix : chunks) { if (m_previewTrack->is_blank_at(ix.toInt())) { QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension)); fileName.prepend(QStringLiteral("avformat:")); Mlt::Producer prod(pCore->getCurrentProfile()->profile(), fileName.toUtf8().constData()); if (prod.is_valid()) { // m_ruler->updatePreview(ix, true); prod.set("mlt_service", "avformat-novalidate"); + prod.set("mute_on_pause", 1); m_previewTrack->insert_at(ix.toInt(), &prod, 1); } } } m_previewTrack->consolidate_blanks(); m_tractor->unlock(); } void PreviewManager::gotPreviewRender(int frame, const QString &file, int progress) { if (m_previewTrack == nullptr) { return; } if (frame < 0) { pCore->currentDoc()->previewProgress(1000); return; } if (file.isEmpty() || progress < 0) { pCore->currentDoc()->previewProgress(progress); if (progress < 0) { pCore->displayMessage(i18n("Preview rendering failed, check your parameters. %1Show details...%2", QString("")), QStringLiteral("")), MltError); } return; } if (m_previewTrack->is_blank_at(frame)) { Mlt::Producer prod(pCore->getCurrentProfile()->profile(), QString("avformat:%1").arg(file).toUtf8().constData()); if (prod.is_valid()) { m_dirtyChunks.removeAll(frame); m_renderedChunks << frame; m_controller->renderedChunksChanged(); prod.set("mlt_service", "avformat-novalidate"); + prod.set("mute_on_pause", 1); qDebug() << "|||| PLUGGING PREVIEW CHUNK AT: " << frame; m_tractor->lock(); m_previewTrack->insert_at(frame, &prod, 1); m_previewTrack->consolidate_blanks(); m_tractor->unlock(); pCore->currentDoc()->previewProgress(progress); pCore->currentDoc()->setModified(true); } else { qCDebug(KDENLIVE_LOG) << "* * * INVALID PROD: " << file; corruptedChunk(frame, file); } } else { qCDebug(KDENLIVE_LOG) << "* * * NON EMPTY PROD: " << frame; } } void PreviewManager::corruptedChunk(int frame, const QString &fileName) { emit abortPreview(); m_previewProcess.waitForFinished(); if (workingPreview >= 0) { workingPreview = -1; m_controller->workingPreviewChanged(); } emit previewRender(0, m_errorLog, -1); m_cacheDir.remove(fileName); if (!m_dirtyChunks.contains(frame)) { m_dirtyChunks << frame; std::sort(m_dirtyChunks.begin(), m_dirtyChunks.end()); } } int PreviewManager::setOverlayTrack(Mlt::Playlist *overlay) { m_overlayTrack = overlay; reconnectTrack(); return m_previewTrackIndex; } void PreviewManager::removeOverlayTrack() { delete m_overlayTrack; m_overlayTrack = nullptr; reconnectTrack(); } QPair PreviewManager::previewChunks() const { QStringList renderedChunks; QStringList dirtyChunks; for (const QVariant &frame : m_renderedChunks) { renderedChunks << frame.toString(); } for (const QVariant &frame : m_dirtyChunks) { dirtyChunks << frame.toString(); } return {renderedChunks, dirtyChunks}; } bool PreviewManager::hasOverlayTrack() const { return m_overlayTrack != nullptr; } bool PreviewManager::hasPreviewTrack() const { return m_previewTrack != nullptr; } int PreviewManager::addedTracks() const { if (m_previewTrack) { if (m_overlayTrack) { return 2; } return 1; } else if (m_overlayTrack) { return 1; } return -1; }