diff --git a/data/encodingprofiles.rc b/data/encodingprofiles.rc index 20adcfee7..f726dc697 100644 --- a/data/encodingprofiles.rc +++ b/data/encodingprofiles.rc @@ -1,35 +1,35 @@ [decklink] x264=crf=25 ab=192k vcodec=libx264 acodec=libvorbis ab=192k preset=veryfast threads=%threads;mov DNxHD=vcodec=dnxhd vb=145000k acodec=pcm_s16le threads=%threads;mov MPEG=qscale=4 ab=192k vcodec=mpeg2video acodec=mp2 threads=%threads;mpg [proxy] x264=-vf scale=720:-2 -vcodec libx264 -g 1 -bf 0 -vb 0 -crf 20 -preset veryfast -acodec aac -ab 128k;mov x264-vaapi=-hwaccel vaapi -vaapi_device /dev/dri/renderD128 -i -vf format=nv12,hwupload,scale_vaapi=720:-2 -vcodec h264_vaapi -g 1 -bf 0 -vb 0 -crf 20 -acodec aac -ab 128k;mov x264-nvenc=-hwaccel cuvid -c:v %nvcodec -i -filter:v scale_cuda=720:-2 -vcodec h264_nvenc -g 1 -bf 0 -vb 0 -acodec copy;mov MPEG2=-vf scale=720:-2 -g 1 -bf 0 -vb 0 -qscale 6 -ab 128k -vcodec mpeg2video -acodec ac3;mpg MJPEG=-vf yadif,scale=720:-2 -qscale 3 -vcodec mjpeg -acodec pcm_s16le;mkv MJPEG-vaapi=-hwaccel vaapi -vaapi_device /dev/dri/renderD128 -i -vf format=nv12,hwupload,scale_vaapi=720:-2 -vcodec mjpeg_vaapi -acodec copy;mkv ProRes=-vcodec prores_ks -vb 0 -g 1 -bf 0 -vprofile 1 -vendor ap10 -qscale 1;mov [screengrab] X264 mute=-crf 25 -vcodec libx264 -preset veryfast -threads 0;mov X264 with audio=-f alsa -i default -crf 25 -ab 192k -vcodec libx264 -acodec libvorbis -preset veryfast -threads 0;mov [video4linux] x264=crf=25 ab=192k vcodec=libx264 acodec=libvorbis ab=192k preset=veryfast threads=%threads;mov MPEG=qscale=4 ab=192k vcodec=mpeg2video acodec=mp2 threads=%threads;mpg [timelinepreview] -DNxHD 1080p 23.976fps=r=23.976 s=1920x1080 vb=36M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 24fps=r=24 s=1920x1080 vb=36M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 25fps=r=25 s=1920x1080 vb=36M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 29.97fps=r=29.97 s=1920x1080 vb=45M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 30fps=r=30 s=1920x1080 vb=45M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 50fps=r=50 s=1920x1080 vb=75M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 59.94fps=r=59.94 s=1920x1080 vb=90M threads=0 vcodec=dnxhd;mov -DNxHD 1080p 60fps=r=60 s=1920x1080 vb=90M threads=0 vcodec=dnxhd;mov +DNxHD 1080p 23.976fps=r=23.976 s=1920x1080 vb=36M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 24fps=r=24 s=1920x1080 vb=36M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 25fps=r=25 s=1920x1080 vb=36M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 29.97fps=r=29.97 s=1920x1080 vb=45M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 30fps=r=30 s=1920x1080 vb=45M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 50fps=r=50 s=1920x1080 vb=75M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 59.94fps=r=59.94 s=1920x1080 vb=90M threads=0 vcodec=dnxhd progressive=1;mov +DNxHD 1080p 60fps=r=60 s=1920x1080 vb=90M threads=0 vcodec=dnxhd progressive=1;mov ProRes=vcodec=prores_ks vb=0 g=1 bf=0 vprofile=0 vendor=ap10 qscale=4 s=800x450;mov MJPEG=f=avi vcodec=mjpeg progressive=1 qscale=1;avi x264-nvenc=vcodec=h264_nvenc g=1 bf=0 profile=0;mkv x264-vaapi=vcodec=h264_vaapi g=1 bf=0 profile=0;mkv diff --git a/renderer/CMakeLists.txt b/renderer/CMakeLists.txt index 6d2ceafe7..aa914d022 100644 --- a/renderer/CMakeLists.txt +++ b/renderer/CMakeLists.txt @@ -1,22 +1,22 @@ set(QT_DONT_USE_QTGUI 1) set(QT_USE_QTDBUS 1) include_directories( ${MLT_INCLUDE_DIR} ${MLTPP_INCLUDE_DIR} ) set(kdenlive_render_SRCS kdenlive_render.cpp renderjob.cpp ) add_executable(kdenlive_render ${kdenlive_render_SRCS}) ecm_mark_nongui_executable(kdenlive_render) -target_link_libraries(kdenlive_render Qt5::Core Qt5::DBus Qt5::Xml +target_link_libraries(kdenlive_render Qt5::Core Qt5::Widgets Qt5::DBus Qt5::Xml ${MLT_LIBRARIES} ${MLTPP_LIBRARIES}) install(TARGETS kdenlive_render DESTINATION ${BIN_INSTALL_DIR}) diff --git a/renderer/kdenlive_render.cpp b/renderer/kdenlive_render.cpp index 576d238b4..510980003 100644 --- a/renderer/kdenlive_render.cpp +++ b/renderer/kdenlive_render.cpp @@ -1,143 +1,150 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "framework/mlt_version.h" #include "mlt++/Mlt.h" #include "renderjob.h" -#include +#include #include #include #include #include #include #include #include #include int main(int argc, char **argv) { - QCoreApplication app(argc, argv); + QApplication app(argc, argv); QStringList args = app.arguments(); QStringList preargs; QString locale; if (args.count() >= 4) { // Remove program name args.removeFirst(); // renderer path (melt) QString render = args.at(0); args.removeFirst(); // Source playlist path QString playlist = args.at(0); args.removeFirst(); // target - where to save result QString target = args.at(0); args.removeFirst(); int pid = 0; // pid to send back progress if (args.count() > 0 && args.at(0).startsWith(QLatin1String("-pid:"))) { pid = args.at(0).section(QLatin1Char(':'), 1).toInt(); args.removeFirst(); } // Do we want a split render if (args.count() > 0 && args.at(0) == QLatin1String("-split")) { args.removeFirst(); // chunks to render QStringList chunks = args.at(0).split(QLatin1Char(','), QString::SkipEmptyParts); args.removeFirst(); // chunk size in frames int chunkSize = args.at(0).toInt(); args.removeFirst(); // rendered file extension QString extension = args.at(0); args.removeFirst(); // avformat consumer params QStringList consumerParams = args.at(0).split(QLatin1Char(' '), QString::SkipEmptyParts); args.removeFirst(); QDir baseFolder(target); Mlt::Factory::init(); Mlt::Profile profile; Mlt::Producer prod(profile, nullptr, playlist.toUtf8().constData()); if (!prod.is_valid()) { fprintf(stderr, "INVALID playlist: %s \n", playlist.toUtf8().constData()); } + profile.from_producer(prod); + profile.set_explicit(1); + const char *localename = prod.get_lcnumeric(); + QLocale::setDefault(QLocale(localename)); for (const QString &frame : chunks) { fprintf(stderr, "START:%d \n", frame.toInt()); QString fileName = QStringLiteral("%1.%2").arg(frame).arg(extension); if (baseFolder.exists(fileName)) { // Don't overwrite an existing file fprintf(stderr, "DONE:%d \n", frame.toInt()); continue; } QScopedPointer playlst(prod.cut(frame.toInt(), frame.toInt() + chunkSize)); QScopedPointer cons( new Mlt::Consumer(profile, QString("avformat:%1").arg(baseFolder.absoluteFilePath(fileName)).toUtf8().constData())); for (const QString ¶m : consumerParams) { if (param.contains(QLatin1Char('='))) { cons->set(param.section(QLatin1Char('='), 0, 0).toUtf8().constData(), param.section(QLatin1Char('='), 1).toUtf8().constData()); } } + if (!cons->is_valid()) { + fprintf(stderr, " = = = INVALID CONSUMER\n\n"); + } cons->set("terminate_on_pause", 1); cons->connect(*playlst); playlst.reset(); cons->run(); cons->stop(); cons->purge(); fprintf(stderr, "DONE:%d \n", frame.toInt()); } // Mlt::Factory::close(); fprintf(stderr, "+ + + RENDERING FINSHED + + + \n"); return 0; } int in = -1; int out = -1; if (LIBMLT_VERSION_INT < 396544) { // older MLT version, does not support consumer in/out, so read it manually QFile f(playlist); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer")); if (!consumer.isNull()) { in = consumer.attribute("in").toInt(); out = consumer.attribute("out").toInt(); } } auto *rJob = new RenderJob(render, playlist, target, pid, in, out); rJob->start(); return app.exec(); } else { fprintf(stderr, "Kdenlive video renderer for MLT.\nUsage: " "kdenlive_render [-erase] [-kuiserver] [-locale:LOCALE] [in=pos] [out=pos] [render] [profile] [rendermodule] [player] [src] [dest] [[arg1] " "[arg2] ...]\n" " -erase: if that parameter is present, src file will be erased at the end\n" " -kuiserver: if that parameter is present, use KDE job tracker\n" " -locale:LOCALE : set a locale for rendering. For example, -locale:fr_FR.UTF-8 will use a french locale (comma as numeric separator)\n" " in=pos: start rendering at frame pos\n" " out=pos: end rendering at frame pos\n" " render: path to MLT melt renderer\n" " profile: the MLT video profile\n" " rendermodule: the MLT consumer used for rendering, usually it is avformat\n" " player: path to video player to play when rendering is over, use '-' to disable playing\n" " src: source file (usually MLT XML)\n" " dest: destination file\n" " args: space separated libavformat arguments\n"); return 1; } } diff --git a/src/core.cpp b/src/core.cpp index 6a9dbfeda..8d3e3399c 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,741 +1,750 @@ /* Copyright (C) 2014 Till Theato This file is part of kdenlive. See www.kdenlive.org. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. */ #include "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "capture/mediacapture.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() : m_thumbProfile(nullptr) , m_capture(new MediaCapture(this)) { } void Core::prepareShutdown() { m_guiConstructed = false; } Core::~Core() { qDebug() << "deleting core"; if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } -void Core::build(const QString &MltPath) +void Core::build(bool isAppImage, const QString &MltPath) { if (m_self) { return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); - - // Open connection with Mlt - MltConnection::construct(MltPath); + + if (isAppImage) { + QString appPath = qApp->applicationDirPath(); + KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg"))); + KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay"))); + KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe"))); + KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt"))); + MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles"))); + } else { + // Open connection with Mlt + MltConnection::construct(MltPath); + } // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } void Core::initGUI(const QUrl &Url) { m_guiConstructed = true; m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); m_monitorManager = new MonitorManager(this); // Producer queue, creating MLT::Producers on request /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, QString()); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); m_mainWindow->show(); } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { return m_jobManager; } LibraryWidget *Core::library() { return m_library; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; m_thumbProfile.reset(); // inform render widget profileChanged(); m_mainWindow->updateRenderWidgetProfile(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } break; case ObjectType::TimelineComposition: return 0; break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->refreshClipMonitor(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } void Core::displayMessage(const QString &message, MessageType type, int timeout) { if (m_mainWindow) { m_mainWindow->displayMessage(message, type, timeout); } else { qDebug() << message; } } void Core::displayBinMessage(const QString &text, int type, const QList &actions) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId); break; case (int)ObjectType::BinClip: return m_binWidget->getClipEffectStack(itemId); default: return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getVideoTrackNames() { if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(true); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second); break; case ObjectType::TimelineTrack: // TODO: invalidate all clips in track break; default: // bin clip should automatically be reloaded, compositions should not have effects break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip && m_mainWindow) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_mainWindow && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade")) && id.first == ObjectType::TimelineClip) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } Mlt::Profile *Core::thumbProfile() { QMutexLocker lck(&m_thumbProfileMutex); if (!m_thumbProfile) { m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_thumbProfile->set_height(200); int width = 200 * m_thumbProfile->dar(); if (width % 8 > 0) { width += 8 - width % 8; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } int Core::getTimelinePosition() const { if (m_mainWindow && m_guiConstructed) { return m_mainWindow->getCurrentTimeline()->controller()->timelinePosition(); } return 0; } void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } void Core::startMediaCapture(bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(true); } else if (checkAudio) { m_capture->recordAudio(true); } m_mediaCaptureFile = m_capture->getCaptureOutputLocation(); } void Core::stopMediaCapture(bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(false); } else if (checkAudio) { m_capture->recordAudio(false); } } QStringList Core::getAudioCaptureDevices() { return m_capture->getAudioCaptureDevices(); } int Core::getMediaCaptureState() { return m_capture->getState(); } bool Core::isMediaCapturing() { return m_capture->isRecording(); } MediaCapture *Core::getAudioDevice() { return m_capture.get(); } QString Core::getProjectFolderName() { return m_monitorManager->getProjectFolder(); } QString Core::getTimelineClipBinId(int cid) { if (!m_guiConstructed) { return QString(); } if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid); } return QString(); } diff --git a/src/core.h b/src/core.h index d18bc5f18..cf84d57ad 100644 --- a/src/core.h +++ b/src/core.h @@ -1,237 +1,239 @@ /* Copyright (C) 2014 Till Theato This file is part of kdenlive. See www.kdenlive.org. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. */ #ifndef CORE_H #define CORE_H #include "definitions.h" #include "kdenlivecore_export.h" #include "undohelper.hpp" #include #include #include #include #include class Bin; class DocUndoStack; class EffectStackModel; class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class MediaCapture; class Monitor; class MonitorManager; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; class Profile; } // namespace Mlt #define EXIT_RESTART (42) #define pCore Core::self() /** * @class Core * @brief Singleton that provides access to the different parts of Kdenlive * * Needs to be initialize before any widgets are created in MainWindow. * Plugins should be loaded after the widget setup. */ class /*KDENLIVECORE_EXPORT*/ Core : public QObject { Q_OBJECT public: Core(const Core &) = delete; Core &operator=(const Core &) = delete; Core(Core &&) = delete; Core &operator=(Core &&) = delete; ~Core() override; /** * @brief Setup the basics of the application, in particular the connection * with Mlt + * @param isAppImage do we expect an AppImage (if yes, we use App path to deduce + * other binaries paths (melt, ffmpeg, etc) * @param MltPath (optional) path to MLT environment */ - static void build(const QString &MltPath = QString()); + static void build(bool isAppImage, const QString &MltPath = QString()); /** * @brief Init the GUI part of the app and show the main window * @param Url (optional) file to open * If Url is present, it will be opened, otherwise, if openlastproject is * set, latest project will be opened. If no file is open after trying this, * a default new file will be created. */ void initGUI(const QUrl &Url); /** @brief Returns a pointer to the singleton object. */ static std::unique_ptr &self(); /** @brief Delete the global core instance */ static void clean(); /** @brief Returns a pointer to the main window. */ MainWindow *window(); /** @brief Returns a pointer to the project manager. */ ProjectManager *projectManager(); /** @brief Returns a pointer to the current project. */ KdenliveDoc *currentDoc(); /** @brief Returns a pointer to the monitor manager. */ MonitorManager *monitorManager(); /** @brief Returns a pointer to the view of the project bin. */ Bin *bin(); /** @brief Select a clip in the Bin from its id. */ void selectBinClip(const QString &id, int frame = -1, const QPoint &zone = QPoint()); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); /** @brief Returns a pointer to the job manager. Please do not store it. */ std::shared_ptr jobManager(); /** @brief Returns a pointer to the library. */ LibraryWidget *library(); /** @brief Returns a pointer to MLT's repository */ std::unique_ptr &getMltRepository(); /** @brief Returns a pointer to the current profile */ std::unique_ptr &getCurrentProfile() const; const QString &getCurrentProfilePath() const; /** @brief Define the active profile * @returns true if profile exists, false if not found */ bool setCurrentProfile(const QString &profilePath); /** @brief Returns Sample Aspect Ratio of current profile */ double getCurrentSar() const; /** @brief Returns Display Aspect Ratio of current profile */ double getCurrentDar() const; /** @brief Returns frame rate of current profile */ double getCurrentFps() const; /** @brief Returns the frame size (width x height) of current profile */ QSize getCurrentFrameSize() const; /** @brief Returns the frame display size (width x height) of current profile */ QSize getCurrentFrameDisplaySize() const; /** @brief Request project monitor refresh */ void requestMonitorRefresh(); /** @brief Request project monitor refresh if current position is inside range*/ void refreshProjectRange(QSize range); /** @brief Request project monitor refresh if referenced item is under cursor */ void refreshProjectItem(const ObjectId &id); /** @brief Returns a reference to a monitor (clip or project monitor) */ Monitor *getMonitor(int id); /** @brief This function must be called whenever the profile used changes */ void profileChanged(); /** @brief Create and push and undo object based on the corresponding functions Note that if you class permits and requires it, you should use the macro PUSH_UNDO instead*/ void pushUndo(const Fun &undo, const Fun &redo, const QString &text); void pushUndo(QUndoCommand *command); /** @brief display a user info/warning message in statusbar */ void displayMessage(const QString &message, MessageType type, int timeout = -1); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemPosition(const ObjectId &id); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Returns the capabilities of a clip: AudioOnly, VideoOnly or Disabled if both are allowed */ PlaylistState::ClipState getItemState(const ObjectId &id); /** @brief Get a list of video track names with indexes */ QMap getVideoTrackNames(); /** @brief Returns the composition A track (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; std::shared_ptr undoStack(); double getClipSpeed(int id) const; /** @brief Mark an item as invalid for timeline preview */ void invalidateItem(ObjectId itemId); void invalidateRange(QSize range); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** A fade for clip id changed, update timeline */ void updateItemModel(ObjectId id, const QString &service); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); Mlt::Profile *thumbProfile(); /** @brief Returns the current project duration */ int projectDuration() const; /** @brief Returns true if current project has some rendered timeline preview */ bool hasTimelinePreview() const; /** @brief Returns current timeline cursor position */ int getTimelinePosition() const; /** @brief Handles audio and video capture **/ void startMediaCapture(bool, bool); void stopMediaCapture(bool, bool); QStringList getAudioCaptureDevices(); int getMediaCaptureState(); bool isMediaCapturing(); MediaCapture *getAudioDevice(); /** @brief Returns Project Folder name for capture output location */ QString getProjectFolderName(); /** @brief Returns a timeline clip's bin id */ QString getTimelineClipBinId(int cid); private: explicit Core(); static std::unique_ptr m_self; /** @brief Makes sure Qt's locale and system locale settings match. */ void initLocale(); MainWindow *m_mainWindow{nullptr}; ProjectManager *m_projectManager{nullptr}; MonitorManager *m_monitorManager{nullptr}; std::shared_ptr m_projectItemModel; std::shared_ptr m_jobManager; Bin *m_binWidget{nullptr}; LibraryWidget *m_library{nullptr}; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; std::unique_ptr m_thumbProfile; bool m_guiConstructed = false; /** @brief Check that the profile is valid (width is a multiple of 8 and height a multiple of 2 */ void checkProfileValidity(); std::unique_ptr m_capture; QUrl m_mediaCaptureFile; QMutex m_thumbProfileMutex; public slots: void triggerAction(const QString &name); /** @brief display a user info/warning message in the project bin */ void displayBinMessage(const QString &text, int type, const QList &actions = QList()); void displayBinLogMessage(const QString &text, int type, const QString &logInfo); signals: void coreIsReady(); void updateLibraryPath(); /** @brief Call config dialog on a selected page / tab */ void showConfigDialog(int, int); void finalizeRecording(const QString &captureFile); }; #endif diff --git a/src/main.cpp b/src/main.cpp index f06a5ee4e..7b1dd0b7d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,217 +1,215 @@ /*************************************************************************** * Copyright (C) 2007 by Marco Gittler (g.marco@freenet.de) * * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "core.h" #include "dialogs/splash.hpp" #include "logger.hpp" #include #include #include "kxmlgui_version.h" #include #include #ifdef USE_DRMINGW #include #elif defined(KF5_USE_CRASH) #include #endif #include #include #include "definitions.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include //new #include int main(int argc, char *argv[]) { #ifdef USE_DRMINGW ExcHndlInit(); #endif // Force QDomDocument to use a deterministic XML attribute order qSetGlobalQHashSeed(0); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) QCoreApplication::setAttribute(Qt::AA_X11InitThreads); #endif #ifdef Q_OS_WIN qputenv("KDE_FORK_SLAVES", "1"); QString path = qApp->applicationDirPath() + QLatin1Char(';') + qgetenv("PATH"); qputenv("PATH", path.toUtf8().constData()); #endif Logger::init(); QApplication app(argc, argv); app.setApplicationName(QStringLiteral("kdenlive")); app.setOrganizationDomain(QStringLiteral("kde.org")); app.setWindowIcon(QIcon(QStringLiteral(":/pics/kdenlive.png"))); KLocalizedString::setApplicationDomain("kdenlive"); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp(config, "unmanaged"); KConfigGroup initialGroup(config, "version"); if (!initialGroup.exists()) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (env.contains(QStringLiteral("XDG_CURRENT_DESKTOP")) && env.value(QStringLiteral("XDG_CURRENT_DESKTOP")).toLower() == QLatin1String("kde")) { qCDebug(KDENLIVE_LOG) << "KDE Desktop detected, using system icons"; } else { // We are not on a KDE desktop, force breeze icon theme // Check if breeze theme is available QStringList iconThemes = KIconTheme::list(); if (iconThemes.contains(QStringLiteral("breeze"))) { grp.writeEntry("force_breeze", true); grp.writeEntry("use_dark_breeze", true); qCDebug(KDENLIVE_LOG) << "Non KDE Desktop detected, forcing Breeze icon theme"; } } // Set breeze dark as default on first opening KConfigGroup cg(config, "UiSettings"); cg.writeEntry("ColorScheme", "Breeze Dark"); } // Init DBus services KDBusService programDBusService; bool forceBreeze = grp.readEntry("force_breeze", QVariant(false)).toBool(); if (forceBreeze) { bool darkBreeze = grp.readEntry("use_dark_breeze", QVariant(false)).toBool(); QIcon::setThemeName(darkBreeze ? QStringLiteral("breeze-dark") : QStringLiteral("breeze")); } // Create KAboutData KAboutData aboutData(QByteArray("kdenlive"), i18n("Kdenlive"), KDENLIVE_VERSION, i18n("An open source video editor."), KAboutLicense::GPL, i18n("Copyright © 2007–2019 Kdenlive authors"), i18n("Please report bugs to http://bugs.kde.org"), QStringLiteral("https://kdenlive.org")); aboutData.addAuthor(i18n("Jean-Baptiste Mardelle"), i18n("MLT and KDE SC 4 / KF5 port, main developer and maintainer"), QStringLiteral("jb@kdenlive.org")); aboutData.addAuthor(i18n("Nicolas Carion"), i18n("Code re-architecture & timeline rewrite"), QStringLiteral("french.ebook.lover@gmail.com")); aboutData.addAuthor(i18n("Vincent Pinon"), i18n("KF5 port, Windows cross-build, bugs fixing"), QStringLiteral("vpinon@kde.org")); aboutData.addAuthor(i18n("Laurent Montel"), i18n("Bugs fixing, clean up code, optimization etc."), QStringLiteral("montel@kde.org")); aboutData.addAuthor(i18n("Till Theato"), i18n("Bug fixing, etc."), QStringLiteral("root@ttill.de")); aboutData.addAuthor(i18n("Simon A. Eugster"), i18n("Color scopes, bug fixing, etc."), QStringLiteral("simon.eu@gmail.com")); aboutData.addAuthor(i18n("Marco Gittler"), i18n("MLT transitions and effects, timeline, audio thumbs"), QStringLiteral("g.marco@freenet.de")); aboutData.addAuthor(i18n("Dan Dennedy"), i18n("Bug fixing, etc."), QStringLiteral("dan@dennedy.org")); aboutData.addAuthor(i18n("Alberto Villa"), i18n("Bug fixing, logo, etc."), QStringLiteral("avilla@FreeBSD.org")); aboutData.addAuthor(i18n("Jean-Michel Poure"), i18n("Rendering profiles customization"), QStringLiteral("jm@poure.com")); aboutData.addAuthor(i18n("Ray Lehtiniemi"), i18n("Bug fixing, etc."), QStringLiteral("rayl@mail.com")); aboutData.addAuthor(i18n("Steve Guilford"), i18n("Bug fixing, etc."), QStringLiteral("s.guilford@dbplugins.com")); aboutData.addAuthor(i18n("Jason Wood"), i18n("Original KDE 3 version author (not active anymore)"), QStringLiteral("jasonwood@blueyonder.co.uk")); aboutData.addCredit(i18n("Nara Oliveira and Farid Abdelnour | Estúdio Gunga"), i18n("Kdenlive 16.08 icon")); aboutData.setTranslator(i18n("NAME OF TRANSLATORS"), i18n("EMAIL OF TRANSLATORS")); aboutData.setOrganizationDomain(QByteArray("kde.org")); aboutData.setOtherText( i18n("Using:\nMLT version %1\nFFmpeg libraries", mlt_version_get_string())); aboutData.setDesktopFileName(QStringLiteral("org.kde.kdenlive")); // Register about data KAboutData::setApplicationData(aboutData); // Add rcc stored icons to the search path so that we always find our icons KIconLoader *loader = KIconLoader::global(); loader->reconfigure("kdenlive", QStringList() << QStringLiteral(":/pics")); // Set app stuff from about data app.setApplicationDisplayName(aboutData.displayName()); app.setOrganizationDomain(aboutData.organizationDomain()); app.setApplicationVersion(aboutData.version()); app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true); // Create command line parser with options QCommandLineParser parser; aboutData.setupCommandLine(&parser); parser.setApplicationDescription(aboutData.shortDescription()); parser.addVersionOption(); parser.addHelpOption(); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("config"), i18n("Set a custom config file name"), QStringLiteral("config"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mlt-path"), i18n("Set the path for MLT environment"), QStringLiteral("mlt-path"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mlt-log"), i18n("MLT log level"), QStringLiteral("verbose/debug"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("i"), i18n("Comma separated list of clips to add"), QStringLiteral("clips"))); parser.addPositionalArgument(QStringLiteral("file"), i18n("Document to open")); // Parse command line parser.process(app); aboutData.processCommandLine(&parser); #ifdef USE_DRMINGW ExcHndlInit(); #elif defined(KF5_USE_CRASH) KCrash::initialize(); #endif //auto splash = new Splash(&app); //splash->show(); //qApp->processEvents(); qmlRegisterUncreatableMetaObject(PlaylistState::staticMetaObject, // static meta object "com.enums", // import statement 1, 0, // major and minor version of the import "ClipState", // name in QML "Error: only enums"); qmlRegisterUncreatableMetaObject(ClipType::staticMetaObject, // static meta object "com.enums", // import statement 1, 0, // major and minor version of the import "ProducerType", // name in QML "Error: only enums"); - QString mltPath = parser.value(QStringLiteral("mlt-path")); if (parser.value(QStringLiteral("mlt-log")) == QStringLiteral("verbose")) { mlt_log_set_level(MLT_LOG_VERBOSE); } else if (parser.value(QStringLiteral("mlt-log")) == QStringLiteral("debug")) { mlt_log_set_level(MLT_LOG_DEBUG); } QUrl url; if (parser.positionalArguments().count() != 0) { url = QUrl::fromLocalFile(parser.positionalArguments().at(0)); // Make sure we get an absolute URL so that we can autosave correctly QString currentPath = QDir::currentPath(); QUrl startup = QUrl::fromLocalFile(currentPath.endsWith(QDir::separator()) ? currentPath : currentPath + QDir::separator()); url = startup.resolved(url); } - //qApp->processEvents(); - Core::build(mltPath); + Core::build(!parser.value(QStringLiteral("config")).isEmpty(), parser.value(QStringLiteral("mlt-path"))); pCore->initGUI(url); //delete splash; //splash->endSplash(); //qApp->processEvents(); int result = app.exec(); Core::clean(); if (EXIT_RESTART == result) { qCDebug(KDENLIVE_LOG) << "restarting app"; auto *restart = new QProcess; restart->start(app.applicationFilePath(), QStringList()); restart->waitForReadyRead(); restart->waitForFinished(1000); result = EXIT_SUCCESS; } return result; } diff --git a/src/timeline2/view/previewmanager.cpp b/src/timeline2/view/previewmanager.cpp index 382bddb34..dbe9ba6e9 100644 --- a/src/timeline2/view/previewmanager.cpp +++ b/src/timeline2/view/previewmanager.cpp @@ -1,763 +1,771 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "previewmanager.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "profiles/profilemodel.hpp" #include "timeline2/view/timelinecontroller.h" #include #include #include #include #include PreviewManager::PreviewManager(TimelineController *controller, Mlt::Tractor *tractor) : QObject() , workingPreview(-1) , m_controller(controller) , m_tractor(tractor) , m_previewTrack(nullptr) , m_overlayTrack(nullptr) , m_previewTrackIndex(-1) , m_initialized(false) { m_previewGatherTimer.setSingleShot(true); m_previewGatherTimer.setInterval(200); + QObject::connect(&m_previewProcess, QOverload::of(&QProcess::finished), this, &PreviewManager::processEnded); + // Find path for Kdenlive renderer #ifdef Q_OS_WIN m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe"); #else m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render"); #endif if (!QFile::exists(m_renderer)) { m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render")); if (m_renderer.isEmpty()) { m_renderer = QStringLiteral("kdenlive_render"); } } connect(this, &PreviewManager::abortPreview, &m_previewProcess, &QProcess::kill, Qt::DirectConnection); connect(&m_previewProcess, &QProcess::readyReadStandardError, this, &PreviewManager::receivedStderr); } PreviewManager::~PreviewManager() { if (m_initialized) { abortRendering(); if (m_undoDir.dirName() == QLatin1String("undo")) { m_undoDir.removeRecursively(); } if ((pCore->currentDoc()->url().isEmpty() && m_cacheDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty()) || m_cacheDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) { if (m_cacheDir.dirName() == QLatin1String("preview")) { m_cacheDir.removeRecursively(); } } } delete m_overlayTrack; delete m_previewTrack; } bool PreviewManager::initialize() { // Make sure our document id does not contain .. tricks bool ok; KdenliveDoc *doc = pCore->currentDoc(); QString documentId = QDir::cleanPath(doc->getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (!ok || documentId.isEmpty()) { // Something is wrong, documentId should be a number (ms since epoch), abort pCore->displayMessage(i18n("Wrong document ID, cannot create temporary folder"), ErrorMessage); return false; } m_cacheDir = doc->getCacheDir(CachePreview, &ok); if (!m_cacheDir.exists() || !ok) { pCore->displayMessage(i18n("Cannot create folder %1", m_cacheDir.absolutePath()), ErrorMessage); return false; } if (m_cacheDir.dirName() != QLatin1String("preview") || m_cacheDir == QDir() || (!m_cacheDir.exists(QStringLiteral("undo")) && !m_cacheDir.mkdir(QStringLiteral("undo"))) || !m_cacheDir.absolutePath().contains(documentId)) { pCore->displayMessage(i18n("Something is wrong with cache folder %1", m_cacheDir.absolutePath()), ErrorMessage); return false; } if (!loadParams()) { pCore->displayMessage(i18n("Invalid timeline preview parameters"), ErrorMessage); return false; } m_undoDir = QDir(m_cacheDir.absoluteFilePath(QStringLiteral("undo"))); // Make sure our cache dirs are inside the temporary folder if (!m_cacheDir.makeAbsolute() || !m_undoDir.makeAbsolute() || !m_undoDir.mkpath(QStringLiteral("."))) { pCore->displayMessage(i18n("Something is wrong with cache folders"), ErrorMessage); return false; } connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews); connect(doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo, Qt::DirectConnection); m_previewTimer.setSingleShot(true); m_previewTimer.setInterval(3000); connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender); connect(this, &PreviewManager::previewRender, this, &PreviewManager::gotPreviewRender, Qt::DirectConnection); connect(&m_previewGatherTimer, &QTimer::timeout, this, &PreviewManager::slotProcessDirtyChunks); m_initialized = true; return true; } bool PreviewManager::buildPreviewTrack() { if (m_previewTrack != nullptr) { return false; } // Create overlay track qDebug() << "/// BUILDING PREVIEW TRACK\n----------------------\n----------------__"; m_previewTrack = new Mlt::Playlist(pCore->getCurrentProfile()->profile()); m_tractor->lock(); reconnectTrack(); m_tractor->unlock(); return true; } void PreviewManager::loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, const QDateTime &documentDate) { if (previewChunks.isEmpty()) { previewChunks = m_renderedChunks; } if (dirtyChunks.isEmpty()) { dirtyChunks = m_dirtyChunks; } for (const auto &frame : previewChunks) { const QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(frame.toInt()).arg(m_extension)); QFile file(fileName); if (file.exists()) { if (!documentDate.isNull() && QFileInfo(file).lastModified() > documentDate) { // Timeline preview file was created after document, invalidate file.remove(); dirtyChunks << frame; } else { gotPreviewRender(frame.toInt(), fileName, 1000); } } else { dirtyChunks << frame; } } if (!previewChunks.isEmpty()) { m_controller->renderedChunksChanged(); } if (!dirtyChunks.isEmpty()) { for (const auto &i : dirtyChunks) { if (!m_dirtyChunks.contains(i)) { m_dirtyChunks << i; } } m_controller->dirtyChunksChanged(); } } void PreviewManager::deletePreviewTrack() { m_tractor->lock(); disconnectTrack(); delete m_previewTrack; m_previewTrack = nullptr; m_dirtyChunks.clear(); m_renderedChunks.clear(); m_controller->dirtyChunksChanged(); m_controller->renderedChunksChanged(); m_tractor->unlock(); } const QDir PreviewManager::getCacheDir() const { return m_cacheDir; } void PreviewManager::reconnectTrack() { disconnectTrack(); if (!m_previewTrack && !m_overlayTrack) { m_previewTrackIndex = -1; return; } m_previewTrackIndex = m_tractor->count(); int increment = 0; if (m_previewTrack) { m_tractor->insert_track(*m_previewTrack, m_previewTrackIndex); std::shared_ptr tk(m_tractor->track(m_previewTrackIndex)); tk->set("hide", 2); tk->set("id", "timeline_preview"); increment++; } if (m_overlayTrack) { m_tractor->insert_track(*m_overlayTrack, m_previewTrackIndex + increment); std::shared_ptr tk(m_tractor->track(m_previewTrackIndex + increment)); tk->set("hide", 2); tk->set("id", "timeline_overlay"); } } void PreviewManager::disconnectTrack() { if (m_previewTrackIndex > -1) { Mlt::Producer *prod = m_tractor->track(m_previewTrackIndex); if (strcmp(prod->get("id"), "timeline_preview") == 0 || strcmp(prod->get("id"), "timeline_overlay") == 0) { m_tractor->remove_track(m_previewTrackIndex); } delete prod; if (m_tractor->count() == m_previewTrackIndex + 1) { // overlay track still here, remove Mlt::Producer *trkprod = m_tractor->track(m_previewTrackIndex); if (strcmp(trkprod->get("id"), "timeline_overlay") == 0) { m_tractor->remove_track(m_previewTrackIndex); } delete trkprod; } } m_previewTrackIndex = -1; } bool PreviewManager::loadParams() { KdenliveDoc *doc = pCore->currentDoc(); m_extension = doc->getDocumentProperty(QStringLiteral("previewextension")); m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts); if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { doc->selectPreviewProfile(); m_consumerParams = doc->getDocumentProperty(QStringLiteral("previewparameters")).split(QLatin1Char(' '), QString::SkipEmptyParts); m_extension = doc->getDocumentProperty(QStringLiteral("previewextension")); } if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { return false; } // Remove the r= and s= parameter (forcing framerate / frame size) as it causes rendering failure. // These parameters should be provided by MLT's profile - for (int i = 0; i < m_consumerParams.count(); i++) { - if (m_consumerParams.at(i).startsWith(QStringLiteral("r=")) /*|| m_consumerParams.at(i).startsWith(QStringLiteral("s="))*/) { + // NOTE: this is still required for DNxHD so leave it + /*for (int i = 0; i < m_consumerParams.count(); i++) { + if (m_consumerParams.at(i).startsWith(QStringLiteral("r=")) || m_consumerParams.at(i).startsWith(QStringLiteral("s="))) { m_consumerParams.removeAt(i); i--; } - } + }*/ if (doc->getDocumentProperty(QStringLiteral("resizepreview")).toInt() != 0) { int resizeWidth = doc->getDocumentProperty(QStringLiteral("previewheight")).toInt(); m_consumerParams << QStringLiteral("s=%1x%2").arg((int)(resizeWidth * pCore->getCurrentDar())).arg(resizeWidth); } m_consumerParams << QStringLiteral("an=1"); if (KdenliveSettings::gpu_accel()) { m_consumerParams << QStringLiteral("glsl.=1"); } return true; } void PreviewManager::invalidatePreviews(const QVariantList chunks) { QMutexLocker lock(&m_previewMutex); bool timer = KdenliveSettings::autopreview(); if (m_previewTimer.isActive()) { m_previewTimer.stop(); timer = true; } KdenliveDoc *doc = pCore->currentDoc(); int stackIx = doc->commandStack()->index(); int stackMax = doc->commandStack()->count(); if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) { // We just added a new command in stack, archive existing chunks int ix = stackIx - 1; m_undoDir.mkdir(QString::number(ix)); bool foundPreviews = false; for (const auto &i : chunks) { QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(ix).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { // No preview files found, remove undo folder m_undoDir.rmdir(QString::number(ix)); } else { // new chunks archived, cleanup old ones emit cleanupOldPreviews(); } } else { // Restore existing chunks, delete others // Check if we just undo the last stack action, then backup, otherwise delete bool lastUndo = false; if (stackIx + 1 == stackMax) { if (!m_undoDir.exists(QString::number(stackMax))) { lastUndo = true; bool foundPreviews = false; m_undoDir.mkdir(QString::number(stackMax)); for (const auto &i : chunks) { QString current = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (m_cacheDir.rename(current, QStringLiteral("undo/%1/%2").arg(stackMax).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { m_undoDir.rmdir(QString::number(stackMax)); } } } bool moveFile = true; QDir tmpDir = m_undoDir; if (!tmpDir.cd(QString::number(stackIx))) { moveFile = false; } QVariantList foundChunks; for (const auto &i : chunks) { QString cacheFileName = QStringLiteral("%1.%2").arg(i.toInt()).arg(m_extension); if (!lastUndo) { m_cacheDir.remove(cacheFileName); } if (moveFile) { if (QFile::copy(tmpDir.absoluteFilePath(cacheFileName), m_cacheDir.absoluteFilePath(cacheFileName))) { foundChunks << i; m_dirtyChunks.removeAll(i); m_renderedChunks << i; } else { qDebug() << "// ERROR PROCESSE CHUNK: " << i << ", " << cacheFileName; } } } if (!foundChunks.isEmpty()) { qSort(foundChunks); m_controller->dirtyChunksChanged(); m_controller->renderedChunksChanged(); reloadChunks(foundChunks); } } doc->setModified(true); if (timer) { m_previewTimer.start(); } } void PreviewManager::doCleanupOldPreviews() { if (m_undoDir.dirName() != QLatin1String("undo")) { return; } QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); // Use QCollator to do a natural sorting so that 10 is after 2 QCollator collator; collator.setNumericMode(true); std::sort(dirs.begin(), dirs.end(), [&collator](const QString &file1, const QString &file2) { return collator.compare(file1, file2) < 0; }); bool ok; while (dirs.count() > 5) { QDir tmp = m_undoDir; QString dirName = dirs.takeFirst(); dirName.toInt(&ok); if (ok && tmp.cd(dirName)) { tmp.removeRecursively(); } } } void PreviewManager::clearPreviewRange(bool resetZones) { m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; for (const auto &ix : m_renderedChunks) { m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension)); m_dirtyChunks << ix; if (!hasPreview) { continue; } int trackIx = m_previewTrack->get_clip_index_at(ix.toInt()); if (!m_previewTrack->is_blank(trackIx)) { Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx); delete prod; } } if (hasPreview) { m_previewTrack->consolidate_blanks(); } m_tractor->unlock(); m_renderedChunks.clear(); // Reload preview params loadParams(); if (resetZones) { m_dirtyChunks.clear(); } m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); } void PreviewManager::addPreviewRange(const QPoint zone, bool add) { int chunkSize = KdenliveSettings::timelinechunks(); int startChunk = zone.x() / chunkSize; int endChunk = rintl(zone.y() / chunkSize); QList toRemove; qDebug() << " // / RESUQEST CHUNKS; " << startChunk << " = " << endChunk; for (int i = startChunk; i <= endChunk; i++) { int frame = i * chunkSize; if (add) { if (!m_renderedChunks.contains(frame)) { m_dirtyChunks << frame; } } else { if (m_renderedChunks.contains(frame)) { toRemove << frame; m_renderedChunks.removeAll(frame); } else { m_dirtyChunks.removeAll(frame); } } } if (add) { qDebug() << "CHUNKS CHANGED: " << m_dirtyChunks; m_controller->dirtyChunksChanged(); if (m_previewProcess.state() == QProcess::NotRunning && KdenliveSettings::autopreview()) { m_previewTimer.start(); } } else { // Remove processed chunks bool isRendering = m_previewProcess.state() != QProcess::NotRunning; m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; for (int ix : toRemove) { m_cacheDir.remove(QStringLiteral("%1.%2").arg(ix).arg(m_extension)); if (!hasPreview) { continue; } int trackIx = m_previewTrack->get_clip_index_at(ix); if (!m_previewTrack->is_blank(trackIx)) { Mlt::Producer *prod = m_previewTrack->replace_with_blank(trackIx); delete prod; } } if (hasPreview) { m_previewTrack->consolidate_blanks(); } m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); m_tractor->unlock(); if (isRendering || KdenliveSettings::autopreview()) { m_previewTimer.start(); } } } void PreviewManager::abortRendering() { if (m_previewProcess.state() == QProcess::NotRunning) { return; } qDebug() << "/// ABORTING RENDEIGN 1\nRRRRRRRRRR"; emit abortPreview(); m_previewProcess.waitForFinished(); + if (m_previewProcess.state() != QProcess::NotRunning) { + m_previewProcess.kill(); + m_previewProcess.waitForFinished(); + } // Re-init time estimation emit previewRender(-1, QString(), 1000); } void PreviewManager::startPreviewRender() { + QMutexLocker lock(&m_previewMutex); if (m_renderedChunks.isEmpty() && m_dirtyChunks.isEmpty()) { m_controller->addPreviewRange(true); } if (!m_dirtyChunks.isEmpty()) { // Abort any rendering abortRendering(); m_waitingThumbs.clear(); // clear log m_errorLog.clear(); const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt")); pCore->getMonitor(Kdenlive::ProjectMonitor)->sceneList(m_cacheDir.absolutePath(), sceneList); pCore->currentDoc()->saveMltPlaylist(sceneList); m_previewTimer.stop(); doPreviewRender(sceneList); } } void PreviewManager::receivedStderr() { QStringList resultList = QString::fromLocal8Bit(m_previewProcess.readAllStandardError()).split(QLatin1Char('\n')); for (auto &result : resultList) { qDebug() << "GOT PROCESS RESULT: " << result; if (result.startsWith(QLatin1String("START:"))) { workingPreview = result.section(QLatin1String("START:"), 1).simplified().toInt(); qDebug() << "// GOT START INFO: " << workingPreview; m_controller->workingPreviewChanged(); } else if (result.startsWith(QLatin1String("DONE:"))) { int chunk = result.section(QLatin1String("DONE:"), 1).simplified().toInt(); m_processedChunks++; QString fileName = QStringLiteral("%1.%2").arg(chunk).arg(m_extension); qDebug() << "---------------\nJOB PROGRRESS: " << m_chunksToRender << ", " << m_processedChunks << " = " << (100 * m_processedChunks / m_chunksToRender); emit previewRender(chunk, m_cacheDir.absoluteFilePath(fileName), 1000 * m_processedChunks / m_chunksToRender); } else { m_errorLog.append(result); } } } void PreviewManager::doPreviewRender(const QString &scene) { // initialize progress bar qSort(m_dirtyChunks); if (m_dirtyChunks.isEmpty()) { return; } + Q_ASSERT(m_previewProcess.state() == QProcess::NotRunning); - if (m_previewProcess.state() != QProcess::NotRunning) { - m_previewProcess.kill(); - m_previewProcess.waitForFinished(); - } QStringList chunks; for (QVariant &frame : m_dirtyChunks) { chunks << frame.toString(); } m_chunksToRender = m_dirtyChunks.count(); m_processedChunks = 0; int chunkSize = KdenliveSettings::timelinechunks(); QStringList args{KdenliveSettings::rendererpath(), scene, m_cacheDir.absolutePath(), QStringLiteral("-split"), chunks.join(QLatin1Char(',')), QString::number(chunkSize - 1), m_extension, m_consumerParams.join(QLatin1Char(' '))}; qDebug() << " - - -STARTING PREVIEW JOBS: " << args; pCore->currentDoc()->previewProgress(0); - QObject::connect(&m_previewProcess, QOverload::of(&QProcess::finished), [this, scene](int, QProcess::ExitStatus status) { - qDebug() << "// PROCESS IS FINISHED!!!"; - QFile::remove(scene); - if (status == QProcess::QProcess::CrashExit) { - qDebug() << "// PROCESS IS CRASHED!!!!!!"; - pCore->currentDoc()->previewProgress(-1); - if (workingPreview >= 0) { - const QString fileName = QStringLiteral("%1.%2").arg(workingPreview).arg(m_extension); - if (m_cacheDir.exists(fileName)) { - m_cacheDir.remove(fileName); - } - } - } else { - pCore->currentDoc()->previewProgress(1000); - } - workingPreview = -1; - m_controller->workingPreviewChanged(); - }); m_previewProcess.start(m_renderer, args); if (m_previewProcess.waitForStarted()) { qDebug() << " - - -STARTING PREVIEW JOBS . . . STARTED"; } } +void PreviewManager::processEnded(int, QProcess::ExitStatus status) +{ + qDebug() << "// PROCESS IS FINISHED!!!"; + const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt")); + QFile::remove(sceneList); + if (status == QProcess::QProcess::CrashExit) { + qDebug() << "// PROCESS CRASHED!!!!!!"; + pCore->currentDoc()->previewProgress(-1); + if (workingPreview >= 0) { + const QString fileName = QStringLiteral("%1.%2").arg(workingPreview).arg(m_extension); + if (m_cacheDir.exists(fileName)) { + m_cacheDir.remove(fileName); + } + } + } else { + pCore->currentDoc()->previewProgress(1000); + } + workingPreview = -1; + m_controller->workingPreviewChanged(); +} + void PreviewManager::slotProcessDirtyChunks() { if (m_dirtyChunks.isEmpty()) { return; } invalidatePreviews(m_dirtyChunks); if (KdenliveSettings::autopreview()) { m_previewTimer.start(); } } void PreviewManager::slotRemoveInvalidUndo(int ix) { QMutexLocker lock(&m_previewMutex); if (m_undoDir.dirName() != QLatin1String("undo")) { // Make sure we delete correct folder return; } QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); bool ok; for (const QString &dir : dirs) { if (dir.toInt(&ok) >= ix && ok) { QDir tmp = m_undoDir; if (tmp.cd(dir)) { tmp.removeRecursively(); } } } } void PreviewManager::invalidatePreview(int startFrame, int endFrame) { int chunkSize = KdenliveSettings::timelinechunks(); int start = startFrame - startFrame % chunkSize; int end = endFrame - endFrame % chunkSize + chunkSize; qSort(m_renderedChunks); m_previewGatherTimer.stop(); abortRendering(); m_tractor->lock(); bool hasPreview = m_previewTrack != nullptr; bool chunksChanged = false; for (int i = start; i <= end; i += chunkSize) { if (m_renderedChunks.contains(i) && hasPreview) { int ix = m_previewTrack->get_clip_index_at(i); if (m_previewTrack->is_blank(ix)) { continue; } Mlt::Producer *prod = m_previewTrack->replace_with_blank(ix); delete prod; m_renderedChunks.removeAll(QVariant(i)); m_dirtyChunks << QVariant(i); chunksChanged = true; } } if (chunksChanged) { m_previewTrack->consolidate_blanks(); m_controller->renderedChunksChanged(); m_controller->dirtyChunksChanged(); } m_tractor->unlock(); m_previewGatherTimer.start(); } void PreviewManager::reloadChunks(const QVariantList chunks) { if (m_previewTrack == nullptr || chunks.isEmpty()) { return; } m_tractor->lock(); for (const auto &ix : chunks) { if (m_previewTrack->is_blank_at(ix.toInt())) { QString fileName = m_cacheDir.absoluteFilePath(QStringLiteral("%1.%2").arg(ix.toInt()).arg(m_extension)); fileName.prepend(QStringLiteral("avformat:")); Mlt::Producer prod(pCore->getCurrentProfile()->profile(), fileName.toUtf8().constData()); if (prod.is_valid()) { // m_ruler->updatePreview(ix, true); prod.set("mlt_service", "avformat-novalidate"); m_previewTrack->insert_at(ix.toInt(), &prod, 1); } } } m_previewTrack->consolidate_blanks(); m_tractor->unlock(); } void PreviewManager::gotPreviewRender(int frame, const QString &file, int progress) { if (m_previewTrack == nullptr) { return; } if (frame < 0) { pCore->currentDoc()->previewProgress(1000); return; } if (file.isEmpty() || progress < 0) { pCore->currentDoc()->previewProgress(progress); if (progress < 0) { pCore->displayMessage(i18n("Preview rendering failed, check your parameters. %1Show details...%2", QString("")), QStringLiteral("")), MltError); } return; } if (m_previewTrack->is_blank_at(frame)) { Mlt::Producer prod(pCore->getCurrentProfile()->profile(), QString("avformat:%1").arg(file).toUtf8().constData()); if (prod.is_valid()) { m_dirtyChunks.removeAll(frame); m_renderedChunks << frame; m_controller->renderedChunksChanged(); prod.set("mlt_service", "avformat-novalidate"); qDebug() << "|||| PLUGGING PREVIEW CHUNK AT: " << frame; m_tractor->lock(); m_previewTrack->insert_at(frame, &prod, 1); m_previewTrack->consolidate_blanks(); m_tractor->unlock(); pCore->currentDoc()->previewProgress(progress); pCore->currentDoc()->setModified(true); } else { qCDebug(KDENLIVE_LOG) << "* * * INVALID PROD: " << file; corruptedChunk(frame, file); } } else { qCDebug(KDENLIVE_LOG) << "* * * NON EMPTY PROD: " << frame; } } void PreviewManager::corruptedChunk(int frame, const QString &fileName) { emit abortPreview(); m_previewProcess.waitForFinished(); if (workingPreview >= 0) { workingPreview = -1; m_controller->workingPreviewChanged(); } emit previewRender(0, m_errorLog, -1); m_cacheDir.remove(fileName); m_dirtyChunks << frame; qSort(m_dirtyChunks); } int PreviewManager::setOverlayTrack(Mlt::Playlist *overlay) { m_overlayTrack = overlay; reconnectTrack(); return m_previewTrackIndex; } void PreviewManager::removeOverlayTrack() { delete m_overlayTrack; m_overlayTrack = nullptr; reconnectTrack(); } QPair PreviewManager::previewChunks() const { QStringList renderedChunks; QStringList dirtyChunks; for (const QVariant &frame : m_renderedChunks) { renderedChunks << frame.toString(); } for (const QVariant &frame : m_dirtyChunks) { dirtyChunks << frame.toString(); } return {renderedChunks, dirtyChunks}; } bool PreviewManager::hasOverlayTrack() const { return m_overlayTrack != nullptr; } bool PreviewManager::hasPreviewTrack() const { return m_previewTrack != nullptr; } int PreviewManager::addedTracks() const { if (m_previewTrack) { if (m_overlayTrack) { return 2; } return 1; } else if (m_overlayTrack) { return 1; } return -1; } diff --git a/src/timeline2/view/previewmanager.h b/src/timeline2/view/previewmanager.h index a01c0af9d..acdd0d88c 100644 --- a/src/timeline2/view/previewmanager.h +++ b/src/timeline2/view/previewmanager.h @@ -1,159 +1,160 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef PREVIEWMANAGER_H #define PREVIEWMANAGER_H #include "definitions.h" #include #include #include #include #include class TimelineController; namespace Mlt { class Tractor; class Playlist; class Producer; class Profile; } // namespace Mlt /** * @namespace PreviewManager * @brief Handles timeline preview. * This manager creates an additional video track on top of the current timeline and renders * chunks (small video files of 25 frames) that are added on this track when rendered. * This allow us to get a preview with a smooth playback of our project. * Only the preview zone is rendered. Once defined, a preview zone shows as a red line below * the timeline ruler. As chunks are rendered, the zone turns to green. */ class PreviewManager : public QObject { Q_OBJECT public: friend class TimelineController; explicit PreviewManager(TimelineController *controller, Mlt::Tractor *tractor); ~PreviewManager() override; /** @brief: initialize base variables, return false if error. */ bool initialize(); /** @brief: a timeline operation caused changes to frames between startFrame and endFrame. */ void invalidatePreview(int startFrame, int endFrame); /** @brief: after a small delay (some operations trigger several invalidatePreview calls), take care of these invalidated chunks. */ void invalidatePreviews(const QVariantList chunks); /** @brief: user adds current timeline zone to the preview zone. */ void addPreviewRange(const QPoint zone, bool add); /** @brief: Remove all existing previews. */ void clearPreviewRange(bool resetZones); /** @brief: stops current rendering process. */ void abortRendering(); /** @brief: rendering parameters have changed, reload them. */ bool loadParams(); /** @brief: Create the preview track if not existing. */ bool buildPreviewTrack(); /** @brief: Delete the preview track. */ void deletePreviewTrack(); /** @brief: Whenever we save or render our project, we remove the preview track so it is not saved. */ void reconnectTrack(); /** @brief: After project save or render, re-add our preview track. */ void disconnectTrack(); /** @brief: Returns directory currently used to store the preview files. */ const QDir getCacheDir() const; /** @brief: Load existing ruler chunks. */ void loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, const QDateTime &documentDate); int setOverlayTrack(Mlt::Playlist *overlay); /** @brief Remove the effect compare overlay track */ void removeOverlayTrack(); /** @brief The current preview chunk being processed, -1 if none */ int workingPreview; /** @brief Returns the list of existing chunks */ QPair previewChunks() const; bool hasOverlayTrack() const; bool hasPreviewTrack() const; int addedTracks() const; private: TimelineController *m_controller; Mlt::Tractor *m_tractor; Mlt::Playlist *m_previewTrack; Mlt::Playlist *m_overlayTrack; int m_previewTrackIndex; /** @brief: The kdenlive renderer app. */ QString m_renderer; /** @brief: The kdenlive timeline preview process. */ QProcess m_previewProcess; /** @brief: The directory used to store the preview files. */ QDir m_cacheDir; /** @brief: The directory used to store undo history of preview files (child of m_cacheDir). */ QDir m_undoDir; QMutex m_previewMutex; QStringList m_consumerParams; QString m_extension; /** @brief: Timer used to autostart preview rendering. */ QTimer m_previewTimer; /** @brief: Since some timeline operations generate several invalidate calls, use a timer to get them all. */ QTimer m_previewGatherTimer; bool m_initialized; QList m_waitingThumbs; QFuture m_previewThread; /** @brief: The count of chunks to process - to calculate job progress */ int m_chunksToRender; /** @brief: The count of already processed chunks - to calculate job progress */ int m_processedChunks; /** @brief: The render process output, useful in case of failure */ QString m_errorLog; /** @brief: After an undo/redo, if we have preview history, use it. */ void reloadChunks(const QVariantList chunks); /** @brief: A chunk failed to render, abort. */ void corruptedChunk(int workingPreview, const QString &fileName); private slots: /** @brief: To avoid filling the hard drive, remove preview undo history after 5 steps. */ void doCleanupOldPreviews(); /** @brief: Start the real rendering process. */ void doPreviewRender(const QString &scene); // std::shared_ptr sourceProd); /** @brief: If user does an undo, then makes a new timeline operation, delete undo history of more recent stack . */ void slotRemoveInvalidUndo(int ix); /** @brief: When the timer collecting invalid zones is done, process. */ void slotProcessDirtyChunks(); /** @brief: Process preview rendering output. */ void receivedStderr(); + void processEnded(int, QProcess::ExitStatus status); public slots: /** @brief: Prepare and start rendering. */ void startPreviewRender(); /** @brief: A chunk has been created, notify ruler. */ void gotPreviewRender(int frame, const QString &file, int progress); protected: QVariantList m_renderedChunks; QVariantList m_dirtyChunks; signals: void abortPreview(); void cleanupOldPreviews(); void previewRender(int frame, const QString &file, int progress); }; #endif diff --git a/tests/TestMain.cpp b/tests/TestMain.cpp index 3c0599a84..ab0cd66db 100644 --- a/tests/TestMain.cpp +++ b/tests/TestMain.cpp @@ -1,36 +1,36 @@ #define CATCH_CONFIG_RUNNER #include "catch.hpp" #include #include #include #define private public #define protected public #include "core.h" #include "logger.hpp" #include "src/effects/effectsrepository.hpp" #include "src/mltcontroller/clipcontroller.h" /* This file is intended to remain empty. Write your tests in a file with a name corresponding to what you're testing */ int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationName(QStringLiteral("kdenlive")); std::unique_ptr repo(Mlt::Factory::init(nullptr)); qputenv("MLT_TESTS", QByteArray("1")); - Core::build(); + Core::build(false); Logger::init(); // if Kdenlive is not installed, ensure we have one keyframable effect EffectsRepository::get()->reloadCustom(QFileInfo("../data/effects/audiobalance.xml").absoluteFilePath()); int result = Catch::Session().run(argc, argv); ClipController::mediaUnavailable.reset(); // global clean-up... // delete repo; Core::m_self.reset(); Mlt::Factory::close(); return (result < 0xff ? result : 0xff); }