diff --git a/src/audiomixer/mixermanager.cpp b/src/audiomixer/mixermanager.cpp index 8d8ff4c22..9370c0985 100644 --- a/src/audiomixer/mixermanager.cpp +++ b/src/audiomixer/mixermanager.cpp @@ -1,219 +1,224 @@ /*************************************************************************** * 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(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); 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) +void MixerManager::connectMixer(bool doConnect, bool channelsOnly) { m_connectedWidgets = doConnect; for (auto item : m_mixers) { item.second->connectMixer(m_connectedWidgets); } - if (m_masterMixer != nullptr) { + if (!channelsOnly && m_masterMixer != nullptr) { m_masterMixer->connectMixer(m_connectedWidgets); } } -void MixerManager::collapseMixers(bool collapse) +void MixerManager::collapseMixers() { - if (collapse) { + if (KdenliveSettings::mixerCollapse()) { m_expandedWidth = width(); - m_channelsBox->setMaximumWidth(0); + 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 a402b28c3..5d5cf6012 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); - void collapseMixers(bool collapse); + 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(); 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/audiomixer/mixerwidget.cpp b/src/audiomixer/mixerwidget.cpp index 8b4fe8daa..ff45ee34c 100644 --- a/src/audiomixer/mixerwidget.cpp +++ b/src/audiomixer/mixerwidget.cpp @@ -1,479 +1,480 @@ /*************************************************************************** * 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 "mixerwidget.hpp" #include "mlt++/MltFilter.h" #include "mlt++/MltTractor.h" #include "mlt++/MltEvent.h" #include "mlt++/MltProfile.h" #include "core.h" #include "kdenlivesettings.h" #include "mixermanager.hpp" #include "audiolevelwidget.hpp" #include "capture/mediacapture.h" #include #include #include #include #include #include #include #include #include #include #include #include static inline double IEC_Scale(double dB) { dB = log10(dB) * 20.0; double fScale = 1.0f; if (dB < -70.0f) fScale = 0.0f; else if (dB < -60.0f) fScale = (dB + 70.0f) * 0.0025f; else if (dB < -50.0f) fScale = (dB + 60.0f) * 0.005f + 0.025f; else if (dB < -40.0) fScale = (dB + 50.0f) * 0.0075f + 0.075f; else if (dB < -30.0f) fScale = (dB + 40.0f) * 0.015f + 0.15f; else if (dB < -20.0f) fScale = (dB + 30.0f) * 0.02f + 0.3f; else if (dB < -0.001f || dB > 0.001f) /* if (dB < 0.0f) */ fScale = (dB + 20.0f) * 0.025f + 0.5f; return fScale; } static inline int fromDB(double level) { int value = 60; if (level > 0.) { // increase volume value = 100 - ((pow(10, 1. - level/24) - 1) / .225); } else if (level < 0.) { value = (10 - pow(10, 1. - level/-50)) / -0.11395 + 59; } return value; } void MixerWidget::property_changed( mlt_service , MixerWidget *widget, char *name ) { if (widget && !strcmp(name, "_position")) { mlt_properties filter_props = MLT_FILTER_PROPERTIES( widget->m_monitorFilter->get_filter()); int pos = mlt_properties_get_int(filter_props, "_position"); if (!widget->m_levels.contains(pos)) { widget->m_levels[pos] = {IEC_Scale(mlt_properties_get_double(filter_props, "_audio_level.0")), IEC_Scale(mlt_properties_get_double(filter_props, "_audio_level.1"))}; if (widget->m_levels.size() > widget->m_maxLevels) { widget->m_levels.erase(widget->m_levels.begin()); } } } } MixerWidget::MixerWidget(int tid, std::shared_ptr service, const QString &trackTag, MixerManager *parent) : QWidget(parent) , m_manager(parent) , m_tid(tid) , m_levelFilter(nullptr) , m_monitorFilter(nullptr) , m_balanceFilter(nullptr) , m_maxLevels(qMax(30, (int)(service->get_fps() * 1.5))) , m_solo(nullptr) , m_record(nullptr) , m_collapse(nullptr) , m_lastVolume(0) , m_listener(nullptr) , m_recording(false) { buildUI(service.get(), trackTag); } MixerWidget::MixerWidget(int tid, Mlt::Tractor *service, const QString &trackTag, MixerManager *parent) : QWidget(parent) , m_manager(parent) , m_tid(tid) , m_levelFilter(nullptr) , m_monitorFilter(nullptr) , m_balanceFilter(nullptr) , m_maxLevels(qMax(30, (int)(service->get_fps() * 1.5))) , m_solo(nullptr) , m_record(nullptr) , m_collapse(nullptr) , m_lastVolume(0) , m_listener(nullptr) , m_recording(false) { buildUI(service, trackTag); } MixerWidget::~MixerWidget() { if (m_listener) { delete m_listener; } } void MixerWidget::buildUI(Mlt::Tractor *service, const QString &trackTag) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // Build audio meter widget m_audioMeterWidget.reset(new AudioLevelWidget(width(), this)); // intialize for stereo display m_audioMeterWidget->setAudioValues({-100, -100}); // Build volume widget m_volumeSlider = new QSlider(Qt::Vertical, this); m_volumeSlider->setRange(0, 100); m_volumeSlider->setValue(60); m_volumeSlider->setToolTip(i18n("Volume")); m_volumeSpin = new QDoubleSpinBox(this); m_volumeSpin->setRange(-50, 24); m_volumeSpin->setSuffix(i18n("dB")); m_volumeSpin->setFrame(false); connect(m_volumeSpin, static_cast(&QDoubleSpinBox::valueChanged), [&](double val) { m_volumeSlider->setValue(fromDB(val)); }); m_balanceDial = new QDial(this); m_balanceDial->setRange(-50, 50); m_balanceDial->setValue(0); m_balanceSpin = new QSpinBox(this); m_balanceSpin->setRange(-50, 50); m_balanceSpin->setValue(0); m_balanceSpin->setFrame(false); m_balanceSpin->setToolTip(i18n("Balance")); // Check if we already have build-in filters for this tractor int max = service->filter_count(); for (int i = 0; i < max; i++) { std::shared_ptr fl(service->filter(i)); if (!fl->is_valid()) { continue; } const QString filterService = fl->get("mlt_service"); if (filterService == QLatin1String("audiolevel")) { m_monitorFilter = fl; } else if (filterService == QLatin1String("volume")) { m_levelFilter = fl; int volume = m_levelFilter->get_int("level"); m_volumeSpin->setValue(volume); m_volumeSlider->setValue(fromDB(volume)); } else if (filterService == QLatin1String("panner")) { m_balanceFilter = fl; int val = m_balanceFilter->get_double("start") * 100 - 50; m_balanceSpin->setValue(val); m_balanceDial->setValue(val); } } // Build default filters if not found if (m_levelFilter == nullptr) { m_levelFilter.reset(new Mlt::Filter(service->get_profile(), "volume")); if (m_levelFilter->is_valid()) { m_levelFilter->set("internal_added", 237); m_levelFilter->set("disable", 1); service->attach(*m_levelFilter.get()); } } if (m_balanceFilter == nullptr) { m_balanceFilter.reset(new Mlt::Filter(service->get_profile(), "panner")); if (m_balanceFilter->is_valid()) { m_balanceFilter->set("internal_added", 237); m_balanceFilter->set("start", 0.5); m_balanceFilter->set("disable", 1); service->attach(*m_balanceFilter.get()); } } // Monitoring should be appended last so that other effects are reflected in audio monitor if (m_monitorFilter == nullptr) { m_monitorFilter.reset(new Mlt::Filter(service->get_profile(), "audiolevel")); if (m_monitorFilter->is_valid()) { m_monitorFilter->set("iec_scale", 0); service->attach(*m_monitorFilter.get()); } } m_trackLabel = new QLabel(trackTag, this); m_trackLabel->setAutoFillBackground(true); m_trackLabel->setAlignment(Qt::AlignHCenter); m_trackLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken); m_muteAction = new KDualAction(i18n("Mute track"), i18n("Unmute track"), this); m_muteAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio"))); m_muteAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio"))); connect(m_balanceDial, &QDial::valueChanged, m_balanceSpin, &QSpinBox::setValue); connect(m_muteAction, &KDualAction::activeChangedByUser, [&](bool active) { if (m_tid == -1) { // Muting master, special case if (m_levelFilter) { if (active) { m_lastVolume = m_levelFilter->get_int("level"); m_levelFilter->set("level", -1000); } else { m_levelFilter->set("level", m_lastVolume); } } } else { emit muteTrack(m_tid, !active); clear(true); } updateLabel(); }); QToolButton *mute = new QToolButton(this); mute->setDefaultAction(m_muteAction); mute->setAutoRaise(true); // Setup default width QFontMetrics fm(font()); setFixedWidth(3 * mute->sizeHint().width()); if (m_tid > -1) { // No solo / rec button on master m_solo = new QToolButton(this); m_solo->setCheckable(true); m_solo->setIcon(QIcon::fromTheme("headphones")); m_solo->setToolTip(i18n("Solo mode")); m_solo->setAutoRaise(true); connect(m_solo, &QToolButton::toggled, [&](bool toggled) { emit toggleSolo(m_tid, toggled); updateLabel(); }); m_record = new QToolButton(this); m_record->setIcon(QIcon::fromTheme("media-record")); m_record->setToolTip(i18n("Record")); m_record->setCheckable(true); m_record->setAutoRaise(true); connect(m_record, &QToolButton::clicked, [&]() { m_manager->recordAudio(m_tid); }); } else { m_collapse = new QToolButton(this); m_collapse->setIcon(QIcon::fromTheme("arrow-left")); m_collapse->setToolTip(i18n("Show Channels")); m_collapse->setCheckable(true); m_collapse->setAutoRaise(true); connect(m_collapse, &QToolButton::clicked, [&]() { - m_manager->collapseMixers(m_collapse->isChecked()); + KdenliveSettings::setMixerCollapse(m_collapse->isChecked()); + m_manager->collapseMixers(); }); } connect(m_volumeSlider, &QSlider::valueChanged, [&](int value) { QSignalBlocker bk(m_volumeSpin); if (m_recording) { m_volumeSpin->setValue(value); KdenliveSettings::setAudiocapturevolume(value); //TODO update capture volume } else if (m_levelFilter != nullptr) { double dbValue = 0; if (value > 60) { // increase volume dbValue = 24 * (1 - log10((100 - value)*0.225 + 1)); } else if (value < 60) { dbValue = -50 * (1 - log10(10 - (value - 59)*(-0.11395))); } m_volumeSpin->setValue(dbValue); m_levelFilter->set("level", dbValue); m_levelFilter->set("disable", value == 60 ? 1 : 0); m_levels.clear(); m_manager->purgeCache(); } }); connect(m_balanceSpin, static_cast(&QSpinBox::valueChanged), [&](int value) { QSignalBlocker bk(m_balanceDial); m_balanceDial->setValue(value); if (m_balanceFilter != nullptr) { m_balanceFilter->set("start", (value + 50) / 100.); m_balanceFilter->set("disable", value == 0 ? 1 : 0); m_levels.clear(); m_manager->purgeCache(); } }); QVBoxLayout *lay = new QVBoxLayout; setContentsMargins(0, 0, 0, 0); lay->setMargin(0); lay->addWidget(m_trackLabel); QHBoxLayout *buttonslay = new QHBoxLayout; buttonslay->setSpacing(0); buttonslay->setContentsMargins(0, 0, 0, 0); if (m_collapse) { buttonslay->addWidget(m_collapse); } buttonslay->addWidget(mute); if (m_solo) { buttonslay->addWidget(m_solo); } if (m_record) { buttonslay->addWidget(m_record); } lay->addLayout(buttonslay); lay->addWidget(m_balanceDial); lay->addWidget(m_balanceSpin); QHBoxLayout *hlay = new QHBoxLayout; hlay->addWidget(m_audioMeterWidget.get()); hlay->addWidget(m_volumeSlider); lay->addLayout(hlay); lay->addWidget(m_volumeSpin); lay->setStretch(4, 10); setLayout(lay); if (service->get_int("hide") > 1) { setMute(true); } } void MixerWidget::mousePressEvent(QMouseEvent *event) { if(event->button() == Qt::RightButton) { QWidget *child = childAt(event->pos()); if (child == m_balanceDial) { m_balanceSpin->setValue(0); } else if (child == m_volumeSlider) { m_volumeSlider->setValue(60); } } else { QWidget::mousePressEvent(event); } } void MixerWidget::setMute(bool mute) { m_muteAction->setActive(mute); m_volumeSlider->setEnabled(!mute); m_volumeSpin->setEnabled(!mute); m_audioMeterWidget->setEnabled(!mute); m_balanceSpin->setEnabled(!mute); m_balanceDial->setEnabled(!mute); updateLabel(); } void MixerWidget::updateLabel() { if (m_recording) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, Qt::red); m_trackLabel->setPalette(pal); } else if (m_muteAction->isActive()) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, QColor("#ff8c00")); m_trackLabel->setPalette(pal); } else if (m_solo && m_solo->isChecked()) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, Qt::darkGreen); m_trackLabel->setPalette(pal); } else { QPalette pal = palette(); m_trackLabel->setPalette(pal); } } void MixerWidget::updateAudioLevel(int pos) { QMutexLocker lk(&m_storeMutex); if (m_levels.contains(pos)) { m_audioMeterWidget->setAudioValues({m_levels.value(pos).first, m_levels.value(pos).second}); //m_levels.remove(pos); } else { m_audioMeterWidget->setAudioValues({-100, -100}); } } void MixerWidget::clear(bool reset) { if (reset) { m_audioMeterWidget->setAudioValues({-100, -100}); } QMutexLocker lk(&m_storeMutex); m_levels.clear(); } bool MixerWidget::isMute() const { return m_muteAction->isActive(); } void MixerWidget::unSolo() { if (m_solo) { QSignalBlocker bl(m_solo); m_solo->setChecked(false); } } void MixerWidget::gotRecLevels(QVectorlevels) { switch (levels.size()) { case 0: m_audioMeterWidget->setAudioValues({-100, -100}); break; case 1: m_audioMeterWidget->setAudioValues({IEC_Scale(levels[0]), -100}); break; default: m_audioMeterWidget->setAudioValues({IEC_Scale(levels[0]), IEC_Scale(levels[1])}); break; } } void MixerWidget::setRecordState(bool recording) { m_recording = recording; m_record->setChecked(m_recording); QSignalBlocker bk(m_volumeSpin); QSignalBlocker bk2(m_volumeSlider); if (m_recording) { connect(pCore->getAudioDevice(), &MediaCapture::audioLevels, this, &MixerWidget::gotRecLevels); m_balanceDial->setEnabled(false); m_balanceSpin->setEnabled(false); m_volumeSpin->setRange(0, 100); m_volumeSpin->setSuffix(QStringLiteral("%")); m_volumeSpin->setValue(KdenliveSettings::audiocapturevolume()); m_volumeSlider->setValue(KdenliveSettings::audiocapturevolume()); } else { m_balanceDial->setEnabled(true); m_balanceSpin->setEnabled(true); int level = m_levelFilter->get_int("level"); disconnect(pCore->getAudioDevice(), &MediaCapture::audioLevels, this, &MixerWidget::gotRecLevels); m_volumeSpin->setRange(-100, 60); m_volumeSpin->setSuffix(i18n("dB")); m_volumeSpin->setValue(level); m_volumeSlider->setValue(fromDB(level)); } updateLabel(); } void MixerWidget::connectMixer(bool doConnect) { if (doConnect) { if (m_listener == nullptr) { m_listener = m_monitorFilter->listen("property-changed", this, (mlt_listener)property_changed); } } else { delete m_listener; m_listener = nullptr; } } diff --git a/src/kdenlivesettings.kcfg b/src/kdenlivesettings.kcfg index 681e8c511..4b540acf0 100644 --- a/src/kdenlivesettings.kcfg +++ b/src/kdenlivesettings.kcfg @@ -1,1019 +1,1024 @@ 0 4 false true 1 true true false 00:00:05:00 00:00:05:00 00:00:00:01 00:00:03:00 false false false false false 00:00:05:00 00:00:01:00 true 0 2 2 false false false false 1000 2000 800 0 0 true false false false 140 1 25 false true true true false false true false 0 1 true true true false sdl2_audio 0 sdl2_audio 0 #999999 100 true 1 false 0 1 2 1 /tmp/ false true $HOME true default: 100 2 48000 0 0 /dev/video0 2 default 0 true false 0 0 0 false 0 0 1280 720 15.0 true false false 0 0 capture false 3 false true 0 true 0 25 true false false false true true true false false false false 0x15 0x05 0 0 false 0x07 true true + + + false + + false true #000000 true 320 240 true false false false true 5 3 false false false 10 0 false false true false false true true true 0 onefield nearest volume,lift_gamma_gain,qtblend wipe,qtblend 0 false false true false 2 3 #ff0000 0 diff --git a/src/monitor/glwidget.cpp b/src/monitor/glwidget.cpp index 34bd9e7f2..3b3484490 100644 --- a/src/monitor/glwidget.cpp +++ b/src/monitor/glwidget.cpp @@ -1,2020 +1,2021 @@ /* * 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 "qml/qmlaudiothumb.h" #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_sendFrame(false) , m_isZoneMode(false) , m_isLoopMode(false) , m_offset(QPoint(0, 0)) , m_audioWaveDisplayed(false) , 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"); qmlRegisterType("AudioThumb", 1, 0, "QmlAudioThumb"); 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->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); + connect(this, &GLWidget::buildAudioThumb, this, &GLWidget::setAudioThumb); + 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)) { w = width; h = height; } // Use OpenGL to normalise sample aspect ratio else if (height * video_aspect > width) { w = width; h = width / video_aspect; } else { w = height * video_aspect; 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 scaley = (double)m_rect.width() / ((double)pCore->getCurrentProfile()->height() * pCore->getCurrentProfile()->dar() / pCore->getCurrentProfile()->width()) / pCore->getCurrentProfile()->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()); } } 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)) { 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->bind(); glViewport(0, 0, fullWidth, fullHeight); 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(); } void GLWidget::slotSwitchAudioOverlay(bool enable) { KdenliveSettings::setDisplayAudioOverlay(enable); if (m_audioWaveDisplayed && !enable) { if (m_producer && m_producer->get_int("video_index") != -1) { // We have a video producer, disable filter removeAudioOverlay(); } } if (enable && !m_audioWaveDisplayed && m_producer) { createAudioOverlay(m_producer->get_int("video_index") == -1); } } 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; } if (m_audioWaveDisplayed) { removeAudioOverlay(); } 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 (m_producer->get_int("video_index") == -1) { // This is an audio only clip, attach visualization filter. Currently, the filter crashes MLT when Movit accel is used if (!m_audioWaveDisplayed) { createAudioOverlay(true); } else if (m_consumer) { if (KdenliveSettings::gpu_accel()) { removeAudioOverlay(); } else { adjustAudioOverlay(true); } } } else if (m_audioWaveDisplayed && (m_consumer != nullptr)) { // This is not an audio clip, hide wave if (KdenliveSettings::displayAudioOverlay()) { adjustAudioOverlay(m_producer->get_int("video_index") == -1); } else { removeAudioOverlay(); } } else if (KdenliveSettings::displayAudioOverlay()) { createAudioOverlay(false); } 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::createAudioOverlay(bool isAudio) { if (!m_consumer) { return; } if (isAudio && KdenliveSettings::gpu_accel()) { // Audiowaveform filter crashes on Movit + audio clips) return; } Mlt::Filter f(pCore->getCurrentProfile()->profile(), "audiowaveform"); if (f.is_valid()) { // f.set("show_channel", 1); f.set("color.1", "0xffff0099"); f.set("fill", 1); if (isAudio) { // Fill screen f.set("rect", "0,0,100%,100%"); } else { // Overlay on lower part of the screen f.set("rect", "0,80%,100%,20%"); } m_consumer->attach(f); m_audioWaveDisplayed = true; } } void GLWidget::removeAudioOverlay() { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { sourceService.detach(*filter); delete filter; break; } else { ct++; } filter = sourceService.filter(ct); } m_audioWaveDisplayed = false; } void GLWidget::adjustAudioOverlay(bool isAudio) { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { if (isAudio) { filter->set("rect", "0,0,100%,100%"); } else { filter->set("rect", "0,80%,100%,20%"); } break; } else { ct++; } filter = sourceService.filter(ct); } } 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->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); } } 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_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()}; } 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}; } 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(); } 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_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::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::setAudioThumb(int channels, const QList &audioCache) { if (!rootObject()) return; auto *audioThumbDisplay = rootObject()->findChild(QStringLiteral("audiothumb")); if (!audioThumbDisplay) return; QImage img(width(), height() / 2, QImage::Format_ARGB32_Premultiplied); img.fill(Qt::transparent); if (!audioCache.isEmpty() && channels > 0) { int audioLevelCount = audioCache.count() - 1; // simplified audio QPainter painter(&img); QRectF mappedRect(0, 0, img.width(), img.height()); int channelHeight = mappedRect.height(); double value; double scale = (double)width() / (audioLevelCount / channels); if (scale < 1) { painter.setPen(QColor(80, 80, 150, 200)); for (int i = 0; i < img.width(); i++) { int framePos = i / scale; value = audioCache.at(qMin(framePos * channels, audioLevelCount)) / 256; for (int channel = 1; channel < channels; channel++) { value = qMax(value, audioCache.at(qMin(framePos * channels + channel, audioLevelCount)) / 256); } painter.drawLine(i, mappedRect.bottom() - (value * channelHeight), i, mappedRect.bottom()); } } else { QPainterPath positiveChannelPath; positiveChannelPath.moveTo(0, mappedRect.bottom()); for (int i = 0; i < audioLevelCount / channels; i++) { value = audioCache.at(qMin(i * channels, audioLevelCount)) / 256; for (int channel = 1; channel < channels; channel++) { value = qMax(value, audioCache.at(qMin(i * channels + channel, audioLevelCount)) / 256); } positiveChannelPath.lineTo(i * scale, mappedRect.bottom() - (value * channelHeight)); } positiveChannelPath.lineTo(mappedRect.right(), mappedRect.bottom()); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(QColor(80, 80, 150, 200))); painter.drawPath(positiveChannelPath); } painter.end(); } - audioThumbDisplay->setImage(img); } void GLWidget::refreshSceneLayout() { if (!rootObject()) { return; } rootObject()->setProperty("profile", QPoint(pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->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); } 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 39ab29160..326911a12 100644 --- a/src/monitor/glwidget.h +++ b/src/monitor/glwidget.h @@ -1,343 +1,344 @@ /* * 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; void setAudioThumb(int channels = 0, const QList &audioCache = QList()); 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 slotSwitchAudioOverlay(bool enable); 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(); + void buildAudioThumb(int channels = 0, const QList &audioCache = QList()); 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; bool m_sendFrame; bool m_isZoneMode; bool m_isLoopMode; QPoint m_offset; bool m_audioWaveDisplayed; MonitorProxy *m_proxy; std::shared_ptr m_blackClip; static void on_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); void createAudioOverlay(bool isAudio); void removeAudioOverlay(); void adjustAudioOverlay(bool isAudio); 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 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 b377568a5..ee80bf992 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2126 +1,2126 @@ /*************************************************************************** * 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 "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::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 *overlayAudio = m_contextMenu->addAction(QIcon(), i18n("Overlay audio waveform")); overlayAudio->setCheckable(true); connect(overlayAudio, &QAction::toggled, m_glMonitor, &GLWidget::slotSwitchAudioOverlay); overlayAudio->setChecked(KdenliveSettings::displayAudioOverlay()); m_configMenu->addAction(overlayAudio); 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_qmlManager->enableAudioThumbs(false); m_videoWidget->showFullScreen(); } else { m_videoWidget->setParent(m_glWidget); //m_videoWidget->move(this->pos()); m_videoWidget->showNormal(); m_qmlManager->enableAudioThumbs(true); 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); } void Monitor::enterEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(true); QWidget::enterEvent(event); } void Monitor::leaveEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(false); QWidget::leaveEvent(event); } // 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->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->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_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; m_glMonitor->setAudioThumb(controller->audioChannels(), controller->audioFrameCache); m_controller->getMarkerModel()->registerSnapModel(m_snaps); m_glMonitor->getControllerProxy()->setClipProperties(controller->clipType(), controller->hasAudioAndVideo(), controller->clipName()); // hasEffects = controller->hasEffects(); } else { loadQmlScene(MonitorSceneDefault); m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->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); } 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(int channels, const QList &audioCache) { - m_glMonitor->setAudioThumb(channels, audioCache); + m_glMonitor->buildAudioThumb(channels, audioCache); } 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/monitor/monitorproxy.cpp b/src/monitor/monitorproxy.cpp index 124778491..114aedc62 100644 --- a/src/monitor/monitorproxy.cpp +++ b/src/monitor/monitorproxy.cpp @@ -1,308 +1,311 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "monitorproxy.h" #include "core.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitormanager.h" #include "profiles/profilemodel.hpp" #include #include #include #include #define SEEK_INACTIVE (-1) MonitorProxy::MonitorProxy(GLWidget *parent) : QObject(parent) , q(parent) , m_position(0) , m_seekPosition(-1) , m_zoneIn(0) , m_zoneOut(-1) , m_hasAV(false) + , m_clipType(0) { } int MonitorProxy::seekPosition() const { return m_seekPosition; } bool MonitorProxy::seeking() const { return m_seekPosition != SEEK_INACTIVE; } int MonitorProxy::position() const { return m_position; } int MonitorProxy::rulerHeight() const { return q->m_rulerHeight; } int MonitorProxy::overlayType() const { return (q->m_id == (int)Kdenlive::ClipMonitor ? KdenliveSettings::clipMonitorOverlayGuides() : KdenliveSettings::projectMonitorOverlayGuides()); } void MonitorProxy::setOverlayType(int ix) { if (q->m_id == (int)Kdenlive::ClipMonitor) { KdenliveSettings::setClipMonitorOverlayGuides(ix); } else { KdenliveSettings::setProjectMonitorOverlayGuides(ix); } } QString MonitorProxy::markerComment() const { return m_markerComment; } void MonitorProxy::requestSeekPosition(int pos) { q->activateMonitor(); m_seekPosition = pos; emit seekPositionChanged(); emit seekRequestChanged(); } int MonitorProxy::seekOrCurrentPosition() const { return m_seekPosition == SEEK_INACTIVE ? m_position : m_seekPosition; } bool MonitorProxy::setPosition(int pos) { if (m_seekPosition == pos) { m_position = pos; m_seekPosition = SEEK_INACTIVE; emit seekPositionChanged(); } else if (m_position == pos) { return true; } else { m_position = pos; } emit positionChanged(); return false; } void MonitorProxy::setMarkerComment(const QString &comment) { if (m_markerComment == comment) { return; } m_markerComment = comment; emit markerCommentChanged(); } void MonitorProxy::setSeekPosition(int pos) { m_seekPosition = pos; emit seekPositionChanged(); } void MonitorProxy::pauseAndSeek(int pos) { q->switchPlay(false); requestSeekPosition(pos); } int MonitorProxy::zoneIn() const { return m_zoneIn; } int MonitorProxy::zoneOut() const { return m_zoneOut; } void MonitorProxy::setZoneIn(int pos) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } m_zoneIn = pos; if (pos > 0) { emit addSnap(pos); } emit zoneChanged(); emit saveZone(); } void MonitorProxy::setZoneOut(int pos) { if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneOut = pos; if (pos > 0) { emit addSnap(pos - 1); } emit zoneChanged(); emit saveZone(); } void MonitorProxy::setZone(int in, int out, bool sendUpdate) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneIn = in; m_zoneOut = out; if (m_zoneIn > 0) { emit addSnap(m_zoneIn); } if (m_zoneOut > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); if (sendUpdate) { emit saveZone(); } } void MonitorProxy::setZone(QPoint zone, bool sendUpdate) { setZone(zone.x(), zone.y(), sendUpdate); } void MonitorProxy::resetZone() { m_zoneIn = 0; m_zoneOut = -1; } double MonitorProxy::fps() const { return pCore->getCurrentFps(); } QPoint MonitorProxy::zone() const { return {m_zoneIn, m_zoneOut}; } QImage MonitorProxy::extractFrame(int frame_position, const QString &path, int width, int height, bool useSourceProfile) { if (width == -1) { width = pCore->getCurrentProfile()->width(); height = pCore->getCurrentProfile()->height(); } else if (width % 2 == 1) { width++; } if (!path.isEmpty()) { QScopedPointer producer(new Mlt::Producer(pCore->getCurrentProfile()->profile(), path.toUtf8().constData())); if (producer && producer->is_valid()) { QImage img = KThumb::getFrame(producer.data(), frame_position, width, height); return img; } } if ((q->m_producer == nullptr) || !path.isEmpty()) { QImage pix(width, height, QImage::Format_RGB32); pix.fill(Qt::black); return pix; } Mlt::Frame *frame = nullptr; QImage img; if (useSourceProfile) { // Our source clip's resolution is higher than current profile, export at full res QScopedPointer tmpProfile(new Mlt::Profile()); QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd(new Mlt::Producer(*tmpProfile, service.toUtf8().constData(), q->m_producer->get("resource"))); tmpProfile->from_producer(*tmpProd); width = tmpProfile->width(); height = tmpProfile->height(); if (tmpProd && tmpProd->is_valid()) { Mlt::Filter scaler(*tmpProfile, "swscale"); Mlt::Filter converter(*tmpProfile, "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); // TODO: paste effects // Clip(*tmpProd).addEffects(*q->m_producer); double projectFps = pCore->getCurrentFps(); double currentFps = tmpProfile->fps(); if (qFuzzyCompare(projectFps, currentFps)) { tmpProd->seek(q->m_producer->position()); } else { tmpProd->seek(q->m_producer->position() * currentFps / projectFps); } frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } } else if (KdenliveSettings::gpu_accel()) { QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd( new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), q->m_producer->get("resource"))); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); tmpProd->seek(q->m_producer->position()); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } else { frame = q->m_producer->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } return img; } void MonitorProxy::activateClipMonitor(bool isClipMonitor) { pCore->monitorManager()->activateMonitor(isClipMonitor ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor); } QString MonitorProxy::toTimecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : q->frameToTime(frames); } void MonitorProxy::setClipProperties(ClipType::ProducerType type, bool hasAV, const QString clipName) { - if (m_hasAV != hasAV) { + if (hasAV != m_hasAV) { m_hasAV = hasAV; emit clipHasAVChanged(); } - if (clipName == clipName) { + if (clipName == m_clipName) { m_clipName.clear(); emit clipNameChanged(); } m_clipName = clipName; emit clipNameChanged(); - m_clipType = type; - emit clipTypeChanged(); + if (type != m_clipType) { + m_clipType = type; + emit clipTypeChanged(); + } }