diff --git a/src/jobs/loadjob.cpp b/src/jobs/loadjob.cpp index 08ecd433c..a21cadd92 100644 --- a/src/jobs/loadjob.cpp +++ b/src/jobs/loadjob.cpp @@ -1,652 +1,654 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "loadjob.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/dialogs/slideshowclip.h" #include "monitor/monitor.h" #include "xml/xml.hpp" #include #include #include #include #include LoadJob::LoadJob(const QString &binId, const QDomElement &xml, const std::function &readyCallBack) : AbstractClipJob(LOADJOB, binId) , m_xml(xml) , m_readyCallBack(readyCallBack) { } const QString LoadJob::getDescription() const { return i18n("Loading clip %1", m_clipId); } namespace { ClipType::ProducerType getTypeForService(const QString &id, const QString &path) { if (id.isEmpty()) { QString ext = path.section(QLatin1Char('.'), -1); if (ext == QLatin1String("mlt") || ext == QLatin1String("kdenlive")) { return ClipType::Playlist; } return ClipType::Unknown; } if (id == QLatin1String("color") || id == QLatin1String("colour")) { return ClipType::Color; } if (id == QLatin1String("kdenlivetitle")) { return ClipType::Text; } if (id == QLatin1String("qtext")) { return ClipType::QText; } if (id == QLatin1String("xml") || id == QLatin1String("consumer")) { return ClipType::Playlist; } if (id == QLatin1String("webvfx")) { return ClipType::WebVfx; } return ClipType::Unknown; } // Read the properties of the xml and pass them to the producer. Note that some properties like resource are ignored void processProducerProperties(const std::shared_ptr &prod, const QDomElement &xml) { // TODO: there is some duplication with clipcontroller > updateproducer that also copies properties QString value; QStringList internalProperties; internalProperties << QStringLiteral("bypassDuplicate") << QStringLiteral("resource") << QStringLiteral("mlt_service") << QStringLiteral("audio_index") << QStringLiteral("video_index") << QStringLiteral("mlt_type") << QStringLiteral("length"); QDomNodeList props; if (xml.tagName() == QLatin1String("producer")) { props = xml.childNodes(); } else { props = xml.firstChildElement(QStringLiteral("producer")).childNodes(); } for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().tagName() != QStringLiteral("property")) { continue; } QString propertyName = props.at(i).toElement().attribute(QStringLiteral("name")); if (!internalProperties.contains(propertyName) && !propertyName.startsWith(QLatin1Char('_'))) { value = props.at(i).firstChild().nodeValue(); if (propertyName.startsWith(QLatin1String("kdenlive-force."))) { // this is a special forced property, pass it propertyName.remove(0, 15); } prod->set(propertyName.toUtf8().constData(), value.toUtf8().constData()); } } } } // namespace // static std::shared_ptr LoadJob::loadResource(QString resource, const QString &type) { if (!resource.startsWith(type)) { resource.prepend(type); } return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } std::shared_ptr LoadJob::loadPlaylist(QString &resource) { std::unique_ptr xmlProfile(new Mlt::Profile()); xmlProfile->set_explicit(0); std::unique_ptr producer(new Mlt::Producer(*xmlProfile, "xml", resource.toUtf8().constData())); if (!producer->is_valid()) { qDebug() << "////// ERROR, CANNOT LOAD SELECTED PLAYLIST: " << resource; return nullptr; } std::unique_ptr prof(new ProfileParam(xmlProfile.get())); if (static_cast(pCore->getCurrentProfile().get()) == prof.get()) { // We can use the "xml" producer since profile is the same (using it with different profiles corrupts the project. // Beware that "consumer" currently crashes on audio mixes! //resource.prepend(QStringLiteral("xml:")); } else { // This is currently crashing so I guess we'd better reject it for now if (!pCore->getCurrentProfile()->isCompatible(xmlProfile.get())) { m_errorMessage.append(i18n("Playlist has a different framerate (%1/%2fps), not recommended.", xmlProfile->frame_rate_num(), xmlProfile->frame_rate_den())); } QString loader = resource; loader.prepend(QStringLiteral("consumer:")); pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), loader.toUtf8().constData()); } pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), "xml", resource.toUtf8().constData()); } void LoadJob::checkProfile(const QString &clipId, const QDomElement &xml, const std::shared_ptr &producer) { // Check if clip profile matches QString service = producer->get("mlt_service"); // Check for image producer if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { // This is an image, create profile from image size int width = producer->get_int("meta.media.width"); if (width % 8 > 0) { width += 8 - width % 8; } int height = producer->get_int("meta.media.height"); height += height % 2; if (width > 100 && height > 100) { std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); projectProfile->m_width = width; projectProfile->m_height = height; projectProfile->m_sample_aspect_num = 1; projectProfile->m_sample_aspect_den = 1; projectProfile->m_display_aspect_num = width; projectProfile->m_display_aspect_den = height; projectProfile->m_description.clear(); pCore->currentDoc()->switchProfile(projectProfile, clipId, xml); } else { // Very small image, we probably don't want to use this as profile } } else if (service.contains(QStringLiteral("avformat"))) { std::unique_ptr blankProfile(new Mlt::Profile()); blankProfile->set_explicit(0); blankProfile->from_producer(*producer); std::unique_ptr clipProfile(new ProfileParam(blankProfile.get())); std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); clipProfile->adjustDimensions(); if (*clipProfile.get() == *projectProfile.get()) { if (KdenliveSettings::default_profile().isEmpty()) { // Confirm default project format KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); } } else { // Profiles do not match, propose profile adjustment pCore->currentDoc()->switchProfile(clipProfile, clipId, xml); } } } void LoadJob::processSlideShow() { int ttl = Xml::getXmlProperty(m_xml, QStringLiteral("ttl")).toInt(); QString anim = Xml::getXmlProperty(m_xml, QStringLiteral("animation")); if (!anim.isEmpty()) { auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "affine"); if ((filter != nullptr) && filter->is_valid()) { int cycle = ttl; QString geometry = SlideshowClip::animationToGeometry(anim, cycle); if (!geometry.isEmpty()) { if (anim.contains(QStringLiteral("low-pass"))) { auto *blur = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "boxblur"); if ((blur != nullptr) && blur->is_valid()) { m_producer->attach(*blur); } } filter->set("transition.geometry", geometry.toUtf8().data()); filter->set("transition.cycle", cycle); m_producer->attach(*filter); } } } QString fade = Xml::getXmlProperty(m_xml, QStringLiteral("fade")); if (fade == QLatin1String("1")) { // user wants a fade effect to slideshow auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "luma"); if ((filter != nullptr) && filter->is_valid()) { if (ttl != 0) { filter->set("cycle", ttl); } QString luma_duration = Xml::getXmlProperty(m_xml, QStringLiteral("luma_duration")); QString luma_file = Xml::getXmlProperty(m_xml, QStringLiteral("luma_file")); if (!luma_duration.isEmpty()) { filter->set("duration", luma_duration.toInt()); } if (!luma_file.isEmpty()) { filter->set("luma.resource", luma_file.toUtf8().constData()); QString softness = Xml::getXmlProperty(m_xml, QStringLiteral("softness")); if (!softness.isEmpty()) { int soft = softness.toInt(); filter->set("luma.softness", (double)soft / 100.0); } } m_producer->attach(*filter); } } QString crop = Xml::getXmlProperty(m_xml, QStringLiteral("crop")); if (crop == QLatin1String("1")) { // user wants to center crop the slides auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "crop"); if ((filter != nullptr) && filter->is_valid()) { filter->set("center", 1); m_producer->attach(*filter); } } } bool LoadJob::startJob() { if (m_done) { return true; } pCore->getMonitor(Kdenlive::ClipMonitor)->resetPlayOrLoopZone(m_clipId); m_resource = Xml::getXmlProperty(m_xml, QStringLiteral("resource")); int duration = 0; ClipType::ProducerType type = static_cast(m_xml.attribute(QStringLiteral("type")).toInt()); QString service = Xml::getXmlProperty(m_xml, QStringLiteral("mlt_service")); if (type == ClipType::Unknown) { type = getTypeForService(service, m_resource); } switch (type) { case ClipType::Color: m_producer = loadResource(m_resource, QStringLiteral("color:")); break; case ClipType::Text: case ClipType::TextTemplate: { bool ok; int producerLength = 0; QString pLength = Xml::getXmlProperty(m_xml, QStringLiteral("length")); if (pLength.isEmpty()) { producerLength = m_xml.attribute(QStringLiteral("length")).toInt(); } else { producerLength = pLength.toInt(&ok); } m_producer = loadResource(m_resource, QStringLiteral("kdenlivetitle:")); if (!m_resource.isEmpty()) { if (!ok) { producerLength = m_producer->time_to_frames(pLength.toUtf8().constData()); } // Title from .kdenlivetitle file QFile txtfile(m_resource); QDomDocument txtdoc(QStringLiteral("titledocument")); if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { txtfile.close(); if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); } } } else { QString xmlDuration = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:duration")); duration = xmlDuration.toInt(&ok); if (!ok) { // timecode duration duration = m_producer->time_to_frames(xmlDuration.toUtf8().constData()); } } qDebug()<<"===== GOT PRODUCER DURATION: "< 0) { duration = producerLength; } else { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()); } } if (producerLength <= 0) { producerLength = duration; } m_producer->set("length", producerLength); m_producer->set("kdenlive:duration", duration); m_producer->set("out", duration - 1); } break; case ClipType::QText: m_producer = loadResource(m_resource, QStringLiteral("qtext:")); break; case ClipType::Playlist: { m_producer = loadPlaylist(m_resource); if (!m_errorMessage.isEmpty()) { QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, m_errorMessage), Q_ARG(int, (int)KMessageWidget::Warning)); } if (m_resource.endsWith(QLatin1String(".kdenlive"))) { QFile f(m_resource); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement pl = doc.documentElement().firstChildElement(QStringLiteral("playlist")); if (!pl.isNull()) { QString offsetData = Xml::getXmlProperty(pl, QStringLiteral("kdenlive:docproperties.seekOffset")); if (offsetData.isEmpty() && Xml::getXmlProperty(pl, QStringLiteral("kdenlive:docproperties.version")) == QLatin1String("0.98")) { offsetData = QStringLiteral("30000"); } if (!offsetData.isEmpty()) { bool ok = false; int offset = offsetData.toInt(&ok); if (ok) { qDebug()<<" / / / FIXING OFFSET DATA: "<get_playtime() - offset - 1; m_producer->set("out", offset - 1); m_producer->set("length", offset); m_producer->set("kdenlive:duration", offset); } } else { qDebug()<<"// NO OFFSET DAT FOUND\n\n"; } } else { qDebug()<<":_______\n______(pCore->getCurrentProfile()->profile(), nullptr, m_resource.toUtf8().constData()); + break; default: if (!service.isEmpty()) { service.append(QChar(':')); m_producer = loadResource(m_resource, service); } else { m_producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_resource.toUtf8().constData()); } break; } if (!m_producer || m_producer->is_blank() || !m_producer->is_valid()) { qCDebug(KDENLIVE_LOG) << " / / / / / / / / ERROR / / / / // CANNOT LOAD PRODUCER: " << m_resource; m_done = true; m_successful = false; if (m_producer) { m_producer.reset(); } QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, i18n("Cannot open file %1", m_resource)), Q_ARG(int, (int)KMessageWidget::Warning)); m_errorMessage.append(i18n("ERROR: Could not load clip %1: producer is invalid", m_resource)); return false; } processProducerProperties(m_producer, m_xml); QString clipName = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:clipname")); if (clipName.isEmpty()) { clipName = QFileInfo(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:originalurl"))).fileName(); } m_producer->set("kdenlive:clipname", clipName.toUtf8().constData()); QString groupId = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:folderid")); if (!groupId.isEmpty()) { m_producer->set("kdenlive:folderid", groupId.toUtf8().constData()); } int clipOut = 0; if (m_xml.hasAttribute(QStringLiteral("out"))) { clipOut = m_xml.attribute(QStringLiteral("out")).toInt(); } // setup length here as otherwise default length (currently 15000 frames in MLT) will be taken even if outpoint is larger if (duration == 0 && (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || type == ClipType::SlideShow)) { int length; if (m_xml.hasAttribute(QStringLiteral("length"))) { length = m_xml.attribute(QStringLiteral("length")).toInt(); clipOut = qMax(1, length - 1); } else { length = Xml::getXmlProperty(m_xml, QStringLiteral("length")).toInt(); clipOut -= m_xml.attribute(QStringLiteral("in")).toInt(); if (length < clipOut) { length = clipOut == 1 ? 1 : clipOut + 1; } } // Pass duration if it was forced if (m_xml.hasAttribute(QStringLiteral("duration"))) { duration = m_xml.attribute(QStringLiteral("duration")).toInt(); if (length < duration) { length = duration; if (clipOut > 0) { clipOut = length - 1; } } } if (duration == 0) { duration = length; } m_producer->set("length", m_producer->frames_to_time(length, mlt_time_clock)); int kdenlive_duration = m_producer->time_to_frames(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:duration")).toUtf8().constData()); if (kdenlive_duration > 0) { m_producer->set("kdenlive:duration", m_producer->frames_to_time(kdenlive_duration, mlt_time_clock)); } else { m_producer->set("kdenlive:duration", m_producer->get("length")); } } if (clipOut > 0) { m_producer->set_in_and_out(m_xml.attribute(QStringLiteral("in")).toInt(), clipOut); } if (m_xml.hasAttribute(QStringLiteral("templatetext"))) { m_producer->set("templatetext", m_xml.attribute(QStringLiteral("templatetext")).toUtf8().constData()); } if (type == ClipType::SlideShow) { processSlideShow(); } int vindex = -1; double fps = -1; const QString mltService = m_producer->get("mlt_service"); if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { // MLT playlist, create producer with blank profile to get real profile info QString tmpPath = m_resource; if (tmpPath.startsWith(QLatin1String("consumer:"))) { tmpPath = "xml:" + tmpPath.section(QLatin1Char(':'), 1); } Mlt::Profile original_profile; std::unique_ptr tmpProd(new Mlt::Producer(original_profile, nullptr, tmpPath.toUtf8().constData())); original_profile.set_explicit(1); double originalFps = original_profile.fps(); fps = originalFps; if (originalFps > 0 && !qFuzzyCompare(originalFps, pCore->getCurrentFps())) { int originalLength = tmpProd->get_length(); int fixedLength = (int)(originalLength * pCore->getCurrentFps() / originalFps); m_producer->set("length", fixedLength); m_producer->set("out", fixedLength - 1); } } else if (mltService == QLatin1String("avformat")) { // check if there are multiple streams vindex = m_producer->get_int("video_index"); // List streams int streams = m_producer->get_int("meta.media.nb_streams"); m_audio_list.clear(); m_video_list.clear(); for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString stype = m_producer->get(propertyName.data()); if (stype == QLatin1String("audio")) { m_audio_list.append(i); } else if (stype == QLatin1String("video")) { m_video_list.append(i); } } if (vindex > -1) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); fps = m_producer->get_double(property); } if (fps <= 0) { if (m_producer->get_double("meta.media.frame_rate_den") > 0) { fps = m_producer->get_double("meta.media.frame_rate_num") / m_producer->get_double("meta.media.frame_rate_den"); } else { fps = m_producer->get_double("source_fps"); } } } if (fps <= 0 && type == ClipType::Unknown) { // something wrong, maybe audio file with embedded image QMimeDatabase db; QString mime = db.mimeTypeForFile(m_resource).name(); if (mime.startsWith(QLatin1String("audio"))) { m_producer->set("video_index", -1); vindex = -1; } } m_done = m_successful = true; return true; } void LoadJob::processMultiStream() { auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We retrieve the folder containing our clip, because we will set the other streams in the same auto parent = pCore->projectItemModel()->getRootFolder()->clipId(); if (auto ptr = m_binClip->parentItem().lock()) { parent = std::static_pointer_cast(ptr)->clipId(); } else { qDebug() << "Warning, something went wrong while accessing parent of bin clip"; } // This helper lambda request addition of a given stream auto addStream = [this, parentId = std::move(parent)](int vindex, int aindex, Fun &undo, Fun &redo) { auto clone = ProjectClip::cloneProducer(m_producer); clone->set("video_index", vindex); clone->set("audio_index", aindex); QString id; pCore->projectItemModel()->requestAddBinClip(id, clone, parentId, undo, redo); }; Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (KdenliveSettings::automultistreams()) { for (int i = 1; i < m_video_list.count(); ++i) { int vindex = m_video_list.at(i); int aindex = 0; if (i <= m_audio_list.count() - 1) { aindex = m_audio_list.at(i); } addStream(vindex, aindex, undo, redo); } return; } int width = 60.0 * pCore->getCurrentDar(); if (width % 2 == 1) { width++; } QScopedPointer dialog(new QDialog(qApp->activeWindow())); dialog->setWindowTitle(QStringLiteral("Multi Stream Clip")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(dialog.data()); auto *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); okButton->setText(i18n("Import selected clips")); QLabel *lab1 = new QLabel(i18n("Additional streams for clip\n %1", m_resource), mainWidget); mainLayout->addWidget(lab1); QList groupList; QList comboList; // We start loading the list at 1, video index 0 should already be loaded for (int j = 1; j < m_video_list.count(); ++j) { m_producer->set("video_index", m_video_list.at(j)); // TODO this keyframe should be cached QImage thumb = KThumb::getFrame(m_producer.get(), 0, width, 60); QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", m_video_list.at(j)), mainWidget); mainLayout->addWidget(streamFrame); streamFrame->setProperty("vindex", m_video_list.at(j)); groupList << streamFrame; streamFrame->setCheckable(true); streamFrame->setChecked(true); auto *vh = new QVBoxLayout(streamFrame); QLabel *iconLabel = new QLabel(mainWidget); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(QPixmap::fromImage(thumb)); vh->addWidget(iconLabel); if (m_audio_list.count() > 1) { auto *cb = new QComboBox(mainWidget); mainLayout->addWidget(cb); for (int k = 0; k < m_audio_list.count(); ++k) { cb->addItem(i18n("Audio stream %1", m_audio_list.at(k)), m_audio_list.at(k)); } comboList << cb; cb->setCurrentIndex(qMin(j, m_audio_list.count() - 1)); vh->addWidget(cb); } mainLayout->addWidget(streamFrame); } m_producer->set("video_index", m_video_list.at(0)); mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { // import selected streams for (int i = 0; i < groupList.count(); ++i) { if (groupList.at(i)->isChecked()) { int vindex = groupList.at(i)->property("vindex").toInt(); int ax = qMin(i, comboList.size() - 1); int aindex = -1; if (ax >= 0) { // only check audio index if we have several audio streams aindex = comboList.at(ax)->itemData(comboList.at(ax)->currentIndex()).toInt(); } addStream(vindex, aindex, undo, redo); } } pCore->pushUndo(undo, redo, i18n("Add additional streams for clip")); } } bool LoadJob::commitResult(Fun &undo, Fun &redo) { qDebug() << "################### loadjob COMMIT"; Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (!m_successful) { // TODO: Deleting cannot happen at this stage or we endup in a mutex lock pCore->projectItemModel()->requestBinClipDeletion(m_binClip, undo, redo); return false; } if (m_xml.hasAttribute(QStringLiteral("_checkProfile")) && m_producer->get_int("video_index") > -1) { checkProfile(m_clipId, m_xml, m_producer); } if (m_video_list.size() > 1) { processMultiStream(); } // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, prod = std::move(m_producer)]() { clip->setProducer(prod, true); return true; }; auto reverse = []() { // This is probably not invertible, leave as is. return true; }; bool ok = operation(); if (ok) { m_readyCallBack(); if (pCore->projectItemModel()->clipsCount() == 1) { // Always select first added clip pCore->selectBinClip(m_clipId); } UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/proxyclipjob.cpp b/src/jobs/proxyclipjob.cpp index 270f52aa3..1768f16f6 100644 --- a/src/jobs/proxyclipjob.cpp +++ b/src/jobs/proxyclipjob.cpp @@ -1,375 +1,387 @@ /*************************************************************************** * * * Copyright (C) 2011 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 "proxyclipjob.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "macros.hpp" #include #include #include #include ProxyJob::ProxyJob(const QString &binId) : AbstractClipJob(PROXYJOB, binId) , m_jobDuration(0) , m_isFfmpegJob(true) , m_jobProcess(nullptr) , m_done(false) { } const QString ProxyJob::getDescription() const { return i18n("Creating proxy %1", m_clipId); } bool ProxyJob::startJob() { auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); QFileInfo fInfo(dest); if (binClip->getProducerIntProperty(QStringLiteral("_overwriteproxy")) == 0 && fInfo.exists() && fInfo.size() > 0) { // Proxy clip already created m_done = true; return true; } ClipType::ProducerType type = binClip->clipType(); bool result; QString source = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl")); int exif = binClip->getProducerIntProperty(QStringLiteral("_exif_orientation")); if (type == ClipType::Playlist || type == ClipType::SlideShow) { // change FFmpeg params to MLT format m_isFfmpegJob = false; QStringList mltParameters; QTemporaryFile *playlist = nullptr; // set clip origin if (type == ClipType::Playlist) { // Special case: playlists use the special 'consumer' producer to support resizing source.prepend(QStringLiteral("consumer:")); } else { // create temporary playlist to generate proxy // we save a temporary .mlt clip for rendering QDomDocument doc; QDomElement xml = binClip->toXml(doc, false); playlist = new QTemporaryFile(); playlist->setFileTemplate(playlist->fileTemplate() + QStringLiteral(".mlt")); if (playlist->open()) { source = playlist->fileName(); QTextStream out(playlist); out << doc.toString(); playlist->close(); } } mltParameters << source; // set destination mltParameters << QStringLiteral("-consumer") << QStringLiteral("avformat:") + dest; QString parameter = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified(); + if (parameter.isEmpty()) { + // Automatic setting, decide based on hw support + parameter = pCore->currentDoc()->getAutoProxyProfile(); + bool nvenc = parameter.contains(QStringLiteral("%nvcodec")); + if (nvenc) { + parameter = parameter.section(QStringLiteral("-i"), 1); + parameter.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale")); + parameter.replace(QStringLiteral("scale_npp"), QStringLiteral("scale")); + parameter.prepend(QStringLiteral("-pix_fmt yuv420p")); + } + } QStringList params = parameter.split(QLatin1Char('-'), QString::SkipEmptyParts); double display_ratio; if (source.startsWith(QLatin1String("consumer:"))) { display_ratio = KdenliveDoc::getDisplayRatio(source.section(QLatin1Char(':'), 1)); } else { display_ratio = KdenliveDoc::getDisplayRatio(source); } if (display_ratio < 1e-6) { display_ratio = 1; } bool skipNext = false; for (const QString &s : params) { QString t = s.simplified(); if (skipNext) { skipNext = false; continue; } if (t.count(QLatin1Char(' ')) == 0) { t.append(QLatin1String("=1")); } else if (t.startsWith(QLatin1String("vf "))) { skipNext = true; bool ok = false; int width = t.section(QLatin1Char('='), 1, 1).section(QLatin1Char(':'), 0, 0).toInt(&ok); if (!ok) { width = 640; } int height = width / display_ratio; // Make sure we get an even height height += height % 2; mltParameters << QStringLiteral("s=%1x%2").arg(width).arg(height); if (t.contains(QStringLiteral("yadif"))) { mltParameters << QStringLiteral("progressive=1"); } continue; } else { t.replace(QLatin1Char(' '), QLatin1String("=")); } mltParameters << t; } int threadCount = QThread::idealThreadCount(); if (threadCount > 2) { threadCount = qMin(threadCount - 1, 4); } else { threadCount = 1; } mltParameters.append(QStringLiteral("real_time=-%1").arg(threadCount)); mltParameters.append(QStringLiteral("threads=%1").arg(threadCount)); + mltParameters.append(QStringLiteral("terminate_on_pause=1")); // TODO: currently, when rendering an xml file through melt, the display ration is lost, so we enforce it manually mltParameters << QStringLiteral("aspect=") + QLocale().toString(display_ratio); // Ask for progress reporting mltParameters << QStringLiteral("progress=1"); m_jobProcess = new QProcess; // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); connect(this, &ProxyJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection); connect(m_jobProcess, &QProcess::readyReadStandardError, this, &ProxyJob::processLogInfo); m_jobProcess->start(KdenliveSettings::rendererpath(), mltParameters); m_jobProcess->waitForFinished(-1); result = m_jobProcess->exitStatus() == QProcess::NormalExit; delete playlist; } else if (type == ClipType::Image) { m_isFfmpegJob = false; // Image proxy QImage i(source); if (i.isNull()) { m_done = false; m_errorMessage.append(i18n("Cannot load image %1.", source)); return false; } QImage proxy; // Images are scaled to profile size. // TODO: Make it be configurable? if (i.width() > i.height()) { proxy = i.scaledToWidth(KdenliveSettings::proxyimagesize()); } else { proxy = i.scaledToHeight(KdenliveSettings::proxyimagesize()); } if (exif > 1) { // Rotate image according to exif data QImage processed; QMatrix matrix; switch (exif) { case 2: matrix.scale(-1, 1); break; case 3: matrix.rotate(180); break; case 4: matrix.scale(1, -1); break; case 5: matrix.rotate(270); matrix.scale(-1, 1); break; case 6: matrix.rotate(90); break; case 7: matrix.rotate(90); matrix.scale(-1, 1); break; case 8: matrix.rotate(270); break; } processed = proxy.transformed(matrix); processed.save(dest); } else { proxy.save(dest); } m_done = true; return true; } else { m_isFfmpegJob = true; if (KdenliveSettings::ffmpegpath().isEmpty()) { // FFmpeg not detected, cannot process the Job m_errorMessage.prepend(i18n("Failed to create proxy. FFmpeg not found, please set path in Kdenlive's settings Environment")); m_done = true; return false; } // Only output error data QStringList parameters = {QStringLiteral("-hide_banner"), QStringLiteral("-y"), QStringLiteral("-stats"), QStringLiteral("-v"), QStringLiteral("error")}; m_jobDuration = (int)binClip->duration().seconds(); QString proxyParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified(); if (proxyParams.isEmpty()) { // Automatic setting, decide based on hw support proxyParams = pCore->currentDoc()->getAutoProxyProfile(); } bool nvenc = proxyParams.contains(QStringLiteral("%nvcodec")); if (nvenc) { QString pix_fmt = binClip->videoCodecProperty(QStringLiteral("pix_fmt")); QString codec = binClip->videoCodecProperty(QStringLiteral("name")); QStringList supportedCodecs{QStringLiteral("hevc"), QStringLiteral("h264"), QStringLiteral("mjpeg"), QStringLiteral("mpeg1"), QStringLiteral("mpeg2"), QStringLiteral("mpeg4"), QStringLiteral("vc1"), QStringLiteral("vp8"), QStringLiteral("vp9")}; QStringList supportedPixFmts{QStringLiteral("yuv420p"), QStringLiteral("yuyv422"), QStringLiteral("rgb24"), QStringLiteral("bgr24"), QStringLiteral("yuv422p"), QStringLiteral("yuv444p"), QStringLiteral("rgb32"), QStringLiteral("yuv410p"), QStringLiteral("yuv411p")}; bool supported = KdenliveSettings::nvScalingEnabled() && supportedCodecs.contains(codec) && supportedPixFmts.contains(pix_fmt); if (supported) { // Full hardware decoding supported codec.append(QStringLiteral("_cuvid")); proxyParams.replace(QStringLiteral("%nvcodec"), codec); } else { proxyParams = proxyParams.section(QStringLiteral("-i"), 1); proxyParams.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale")); proxyParams.replace(QStringLiteral("scale_npp"), QStringLiteral("scale")); if (!supportedPixFmts.contains(pix_fmt)) { proxyParams.prepend(QStringLiteral("-pix_fmt yuv420p")); } } } bool disableAutorotate = binClip->getProducerProperty(QStringLiteral("autorotate")) == QLatin1String("0"); if (disableAutorotate || proxyParams.contains(QStringLiteral("-noautorotate"))) { // The noautorotate flag must be passed before input source parameters << QStringLiteral("-noautorotate"); } if (proxyParams.contains(QLatin1String("-i "))) { // we have some pre-filename parameters, filename will be inserted later } else { parameters << QStringLiteral("-i") << source; } QString params = proxyParams; for (const QString &s : params.split(QLatin1Char(' '), QString::SkipEmptyParts)) { QString t = s.simplified(); if (t != QLatin1String("-noautorotate")) { parameters << t; if (t == QLatin1String("-i")) { parameters << source; } } } // Make sure we don't block when proxy file already exists parameters << dest; qDebug()<<"/// FULL PROXY PARAMS:\n"<setProcessChannelMode(QProcess::MergedChannels); connect(m_jobProcess, &QProcess::readyReadStandardError, this, &ProxyJob::processLogInfo); connect(this, &ProxyJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection); m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly); m_jobProcess->waitForFinished(-1); result = m_jobProcess->exitStatus() == QProcess::NormalExit; } // remove temporary playlist if it exists if (result) { if (QFileInfo(dest).size() == 0) { QFile::remove(dest); // File was not created m_done = false; m_errorMessage.append(i18n("Failed to create proxy clip.")); } else { m_done = true; } } else { // Proxy process crashed QFile::remove(dest); m_done = false; m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll())); } m_jobProcess->deleteLater(); return result; } void ProxyJob::processLogInfo() { const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError()); m_logDetails.append(buffer); int progress = 0; if (m_isFfmpegJob) { // Parse FFmpeg output if (m_jobDuration == 0) { if (buffer.contains(QLatin1String("Duration:"))) { QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); if (!data.isEmpty()) { QStringList numbers = data.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble()); } } } else if (buffer.contains(QLatin1String("time="))) { QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); if (!time.isEmpty()) { QStringList numbers = time.split(QLatin1Char(':')); if (numbers.size() < 3) { progress = (int)time.toDouble(); if (progress == 0) { return; } } else { progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); } } emit jobProgress((int)(100.0 * progress / m_jobDuration)); } } else { // Parse MLT output if (buffer.contains(QLatin1String("percentage:"))) { progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt(); emit jobProgress(progress); } } } bool ProxyJob::commitResult(Fun &undo, Fun &redo) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); binClip->setProducerProperty(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); return false; } m_resultConsumed = true; auto operation = [clipId = m_clipId]() { auto binClip = pCore->projectItemModel()->getClipByBinID(clipId); binClip->setProducerProperty(QStringLiteral("_overwriteproxy"), QString()); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); binClip->setProducerProperty(QStringLiteral("resource"), dest); pCore->bin()->reloadClip(clipId, false); return true; }; auto reverse = [clipId = m_clipId]() { auto binClip = pCore->projectItemModel()->getClipByBinID(clipId); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl")); binClip->setProducerProperty(QStringLiteral("resource"), dest); pCore->bin()->reloadClip(clipId, false); return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; return true; }