diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 6a243fc45..551859478 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -26,6 +26,8 @@ #include "codec/ffmpeg/ffmpegdecoder.h" #include "codec/oiio/oiiodecoder.h" +#include "task/index/index.h" +#include "task/taskmanager.h" Decoder::Decoder() : open_(false), @@ -126,6 +128,14 @@ bool Decoder::ProbeMedia(Footage *f) // FIXME: Cache the results so we don't have to probe if this media is added a second time + // Start an index task + foreach (StreamPtr stream, f->streams()) { + qDebug() << "Starting index task on" << stream->footage()->filename() << stream->index(); + + IndexTask* index_task = new IndexTask(stream); + TaskManager::instance()->AddTask(index_task); + } + return true; } } @@ -161,3 +171,7 @@ void Decoder::Conform(const AudioRenderingParams ¶ms) qCritical() << "Conform called on an audio decoder that does not have a handler for it:" << id(); abort(); } + +void Decoder::Index() +{ +} diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 71d31b35d..17d13c94a 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -219,6 +219,17 @@ public: */ virtual void Conform(const AudioRenderingParams& params); + /** + * @brief Create an index for this media + * + * Indexes are used to improve speed and reliability of imported media. Calling Retrieve() will automatically check + * for an index and create one if it doesn't exist. + * + * Indexing is slow so it's recommended to do it in a background thread. Index() must be called while the Decoder is + * open, and does not automatically call Open() and Close() the Decoder. The caller must call thse manually. + */ + virtual void Index(); + protected: bool open_; diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index a67913292..bb6669220 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -62,6 +62,11 @@ bool FFmpegDecoder::Open() return true; } + if (!stream()) { + Error(QStringLiteral("Tried to open a decoder with no footage stream set")); + return false; + } + int error_code; // Convert QString to a C string @@ -344,7 +349,7 @@ FramePtr FFmpegDecoder::RetrieveAudio(const rational &timecode, const rational & return nullptr; } - ValidateIndex(); + Index(); Conform(params); @@ -437,7 +442,7 @@ void FFmpegDecoder::Conform(const AudioRenderingParams ¶ms) return; } - ValidateIndex(); + Index(); // Get indexed WAV file WaveInput input(GetIndexFilename()); @@ -659,7 +664,7 @@ bool FFmpegDecoder::Probe(Footage *f) Open(); // Use index to find duration - ValidateIndex(); + Index(); // Use last frame index as the duration // FIXME: Does this skip the last frame? @@ -706,17 +711,25 @@ void FFmpegDecoder::Index() return; } - // Reset state - Seek(0); + stream()->index_lock_.lock(); + + if (!LoadIndex()) { + + // Reset state + Seek(0); + + if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + IndexVideo(pkt_, frame_); + } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + IndexAudio(pkt_, frame_); + } + + // Reset state + Seek(0); - if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - IndexVideo(pkt_, frame_); - } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - IndexAudio(pkt_, frame_); } - // Reset state - Seek(0); + stream()->index_lock_.unlock(); } QString FFmpegDecoder::GetIndexFilename() @@ -756,17 +769,6 @@ QString FFmpegDecoder::GetConformedFilename(const AudioRenderingParams ¶ms) return index_fn; } -void FFmpegDecoder::ValidateIndex() -{ - stream()->index_lock_.lock(); - - if (!LoadIndex()) { - Index(); - } - - stream()->index_lock_.unlock(); -} - bool FFmpegDecoder::LoadIndex() { switch (avstream_->codecpar->codec_type) { @@ -991,7 +993,7 @@ int64_t FFmpegDecoder::GetClosestTimestampInIndex(const int64_t &ts) { // Index now if we haven't already if (frame_index_.isEmpty()) { - ValidateIndex(); + Index(); } if (frame_index_.isEmpty()) { diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index a313336ef..2d80ffbaf 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -92,18 +92,7 @@ private: */ int GetFrame(AVPacket* pkt, AVFrame* frame); - /** - * @brief Create an index for this media - * - * Indexes are used to improve speed and reliability of imported media. Calling Retrieve() will automatically check - * for an index and create one if it doesn't exist. - * - * Indexing is slow so it's recommended to do it in a background thread. Index() must be called while the Decoder is - * open, and does not automatically call Open() and Close() the Decoder. The caller must call thse manually. - * - * FIXME: This should perhaps become a common function for the base Decoder class - */ - void Index(); + virtual void Index() override; /** * @brief Returns the filename for the index @@ -117,8 +106,6 @@ private: */ QString GetConformedFilename(const AudioRenderingParams ¶ms); - void ValidateIndex(); - /** * @brief Used internally to load a frame index into frame_index_ * diff --git a/app/core.cpp b/app/core.cpp index 2ded33f09..fe38380c4 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -133,6 +133,8 @@ void Core::Stop() MenuShared::DestroyInstance(); + TaskManager::DestroyInstance(); + PanelManager::DestroyInstance(); AudioManager::DestroyInstance(); @@ -184,26 +186,6 @@ const bool &Core::snapping() return snapping_; } -void Core::StartModalTask(Task *t) -{ - QDialog dialog(main_window_); - - QHBoxLayout* layout = new QHBoxLayout(&dialog); - layout->setMargin(0); - - TaskViewItem* task_view = new TaskViewItem(&dialog); - task_view->SetTask(t); - layout->addWidget(task_view); - - connect(t, SIGNAL(Finished()), &dialog, SLOT(accept())); - - // FIXME: Risk of task finishing before dialog execs? - - if (t->Start()) { - dialog.exec(); - } -} - void Core::SetTool(const Tool::Item &tool) { tool_ = tool; @@ -375,7 +357,6 @@ void Core::ImportTaskComplete(QUndoCommand *command) void Core::DeclareTypesForQt() { - qRegisterMetaType("Task::Status"); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); @@ -404,11 +385,14 @@ void Core::StartGUI(bool full_screen) // Initialize disk service DiskManager::CreateInstance(); + // Initialize task manager + TaskManager::CreateInstance(); + // Connect the PanelFocusManager to the application's focus change signal connect(qApp, - SIGNAL(focusChanged(QWidget*, QWidget*)), + &QApplication::focusChanged, PanelManager::instance(), - SLOT(FocusChanged(QWidget*, QWidget*))); + &PanelManager::FocusChanged); // Create main window and open it main_window_ = new MainWindow(); @@ -419,7 +403,7 @@ void Core::StartGUI(bool full_screen) } // When a new project is opened, update the mainwindow - connect(this, SIGNAL(ProjectOpened(Project*)), main_window_, SLOT(ProjectOpen(Project*))); + connect(this, &Core::ProjectOpened, main_window_, &MainWindow::ProjectOpen); // Start autorecovery timer using the config value as its interval SetAutorecoveryInterval(Config::Current()["AutorecoveryInterval"].toInt()); @@ -616,7 +600,7 @@ int Core::CountFilesInFileList(const QFileInfoList &filenames) return file_count; } -void Core::InitiateOpenSaveProcess(ProjectFileManagerBase *manager, const QString& dialog_text, const QString& dialog_title) +void Core::InitiateOpenSaveProcess(Task *manager, const QString& dialog_text, const QString& dialog_title) { // Create save dialog LoadSaveDialog* lsd = new LoadSaveDialog(dialog_text, dialog_title, main_window_); @@ -630,17 +614,17 @@ void Core::InitiateOpenSaveProcess(ProjectFileManagerBase *manager, const QStrin manager->moveToThread(save_thread); // Connect the save manager progress signal to the progress bar update on the dialog - connect(manager, &ProjectFileManagerBase::ProgressChanged, lsd, &LoadSaveDialog::SetProgress, Qt::QueuedConnection); + connect(manager, &Task::ProgressChanged, lsd, &LoadSaveDialog::SetProgress, Qt::QueuedConnection); // Connect cancel signal (must be a direct connection or it'll be queued after the save is already finished) - connect(lsd, &LoadSaveDialog::Cancelled, manager, &ProjectFileManagerBase::Cancel, Qt::DirectConnection); + connect(lsd, &LoadSaveDialog::Cancelled, manager, &Task::Cancel, Qt::DirectConnection); // Connect cleanup functions (ensure everything new'd in this function is deleteLater'd) - connect(manager, &ProjectFileManagerBase::Finished, lsd, &LoadSaveDialog::accept, Qt::QueuedConnection); - connect(manager, &ProjectFileManagerBase::Finished, lsd, &LoadSaveDialog::deleteLater, Qt::QueuedConnection); - connect(manager, &ProjectFileManagerBase::Finished, manager, &ProjectFileManagerBase::deleteLater, Qt::QueuedConnection); - connect(manager, &ProjectFileManagerBase::Finished, save_thread, &QThread::quit, Qt::QueuedConnection); - connect(manager, &ProjectFileManagerBase::Finished, save_thread, &QThread::deleteLater, Qt::QueuedConnection); + connect(manager, &Task::Finished, lsd, &LoadSaveDialog::accept, Qt::QueuedConnection); + connect(manager, &Task::Finished, lsd, &LoadSaveDialog::deleteLater, Qt::QueuedConnection); + connect(manager, &Task::Finished, manager, &Task::deleteLater, Qt::QueuedConnection); + connect(manager, &Task::Finished, save_thread, &QThread::quit, Qt::QueuedConnection); + connect(manager, &Task::Finished, save_thread, &QThread::deleteLater, Qt::QueuedConnection); // Start the save process QMetaObject::invokeMethod(manager, "Start", Qt::QueuedConnection); diff --git a/app/core.h b/app/core.h index 48c70abb0..d8bc81168 100644 --- a/app/core.h +++ b/app/core.h @@ -28,7 +28,6 @@ #include "common/rational.h" #include "project/item/footage/footage.h" #include "project/project.h" -#include "project/projectfilemanagerbase.h" #include "project/projectviewmodel.h" #include "task/task.h" #include "tool/tool.h" @@ -111,15 +110,6 @@ public: */ const bool& snapping(); - /** - * @brief Starts a modal task - * - * This function does NOT take ownership of the Task. - * - * @param t - */ - void StartModalTask(Task* t); - /** * @brief Get the currently active project * @@ -283,7 +273,7 @@ private: * The load and save process are largely similar, both OpenProjectInternal() and SaveProjectInternal() can run * this function with some minor setup differences. */ - void InitiateOpenSaveProcess(ProjectFileManagerBase* manager, const QString &dialog_text, const QString &dialog_title); + void InitiateOpenSaveProcess(Task* manager, const QString &dialog_text, const QString &dialog_title); /** * @brief Declare custom types/classes for Qt's signal/slot system diff --git a/app/project/CMakeLists.txt b/app/project/CMakeLists.txt index 28d6e0639..3507ee25d 100644 --- a/app/project/CMakeLists.txt +++ b/app/project/CMakeLists.txt @@ -20,8 +20,6 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} project/project.h project/project.cpp - project/projectfilemanagerbase.h - project/projectfilemanagerbase.cpp project/projectimportmanager.h project/projectimportmanager.cpp project/projectloadmanager.h diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 085d86853..e6ac10b1a 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -134,11 +134,11 @@ void Footage::set_timestamp(const QDateTime &t) void Footage::add_stream(StreamPtr s) { + // Set its footage parent to this + s->set_footage(this); + // Add a copy of this stream to the list streams_.append(s); - - // Set its footage parent to this - streams_.last()->set_footage(this); } StreamPtr Footage::stream(int index) const diff --git a/app/project/projectfilemanagerbase.cpp b/app/project/projectfilemanagerbase.cpp deleted file mode 100644 index 248e53848..000000000 --- a/app/project/projectfilemanagerbase.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "projectfilemanagerbase.h" - -ProjectFileManagerBase::ProjectFileManagerBase() : - cancelled_(false) -{ - -} - -void ProjectFileManagerBase::Start() -{ - Action(); - - emit Finished(); -} - -void ProjectFileManagerBase::Cancel() -{ - cancelled_ = true; -} - -const QAtomicInt &ProjectFileManagerBase::IsCancelled() const -{ - return cancelled_; -} diff --git a/app/project/projectfilemanagerbase.h b/app/project/projectfilemanagerbase.h deleted file mode 100644 index f240161e3..000000000 --- a/app/project/projectfilemanagerbase.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef PROJECTFILEMANAGERBASE_H -#define PROJECTFILEMANAGERBASE_H - -#include - -#include "project.h" - -class ProjectFileManagerBase : public QObject -{ - Q_OBJECT -public: - ProjectFileManagerBase(); - -public slots: - /** - * @brief Start the save process - * - * It's recommended to invoke this through Qt signals/slots/QueuedConnection after moving this object to a separate - * thread. - */ - void Start(); - - /** - * @brief Cancel the current save - * - * Always connect to this with a DirectConnection. Otherwise, it'll be queued AFTER the save function is already - * complete. - */ - void Cancel(); - -protected: - virtual void Action() = 0; - - const QAtomicInt& IsCancelled() const; - -signals: - void ProgressChanged(int); - - void Finished(); - -private: - QAtomicInt cancelled_; - -}; - -#endif // PROJECTFILEMANAGERBASE_H diff --git a/app/project/projectimportmanager.h b/app/project/projectimportmanager.h index e0be298eb..c757bf928 100644 --- a/app/project/projectimportmanager.h +++ b/app/project/projectimportmanager.h @@ -4,10 +4,10 @@ #include #include -#include "projectfilemanagerbase.h" #include "projectviewmodel.h" +#include "task/task.h" -class ProjectImportManager : public ProjectFileManagerBase +class ProjectImportManager : public Task { Q_OBJECT public: diff --git a/app/project/projectloadmanager.cpp b/app/project/projectloadmanager.cpp index e0f911613..097b56da8 100644 --- a/app/project/projectloadmanager.cpp +++ b/app/project/projectloadmanager.cpp @@ -46,5 +46,5 @@ void ProjectLoadManager::Action() project_file.close(); } - emit Finished(); + emit Succeeeded(); } diff --git a/app/project/projectloadmanager.h b/app/project/projectloadmanager.h index b0306d935..1ab4e1052 100644 --- a/app/project/projectloadmanager.h +++ b/app/project/projectloadmanager.h @@ -1,9 +1,10 @@ #ifndef PROJECTLOADMANAGER_H #define PROJECTLOADMANAGER_H -#include "projectfilemanagerbase.h" +#include "project/project.h" +#include "task/task.h" -class ProjectLoadManager : public ProjectFileManagerBase +class ProjectLoadManager : public Task { Q_OBJECT public: diff --git a/app/project/projectsavemanager.cpp b/app/project/projectsavemanager.cpp index f519a3205..f45d93244 100644 --- a/app/project/projectsavemanager.cpp +++ b/app/project/projectsavemanager.cpp @@ -32,5 +32,5 @@ void ProjectSaveManager::Action() project_file.close(); } - emit Finished(); + emit Succeeeded(); } diff --git a/app/project/projectsavemanager.h b/app/project/projectsavemanager.h index 284491fc3..412911b33 100644 --- a/app/project/projectsavemanager.h +++ b/app/project/projectsavemanager.h @@ -1,9 +1,10 @@ #ifndef PROJECTSAVEMANAGER_H #define PROJECTSAVEMANAGER_H -#include "projectfilemanagerbase.h" +#include "project/project.h" +#include "task/task.h" -class ProjectSaveManager : public ProjectFileManagerBase +class ProjectSaveManager : public Task { Q_OBJECT public: diff --git a/app/task/CMakeLists.txt b/app/task/CMakeLists.txt index f99cf323e..25c6f068b 100644 --- a/app/task/CMakeLists.txt +++ b/app/task/CMakeLists.txt @@ -14,13 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(index) + set(OLIVE_SOURCES ${OLIVE_SOURCES} task/task.h task/task.cpp task/taskmanager.h task/taskmanager.cpp - task/taskthread.h - task/taskthread.cpp PARENT_SCOPE ) diff --git a/app/task/index/CMakeLists.txt b/app/task/index/CMakeLists.txt new file mode 100644 index 000000000..0d3608dc2 --- /dev/null +++ b/app/task/index/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + task/index/index.h + task/index/index.cpp + PARENT_SCOPE +) diff --git a/app/task/index/index.cpp b/app/task/index/index.cpp new file mode 100644 index 000000000..2e1225890 --- /dev/null +++ b/app/task/index/index.cpp @@ -0,0 +1,26 @@ +#include "index.h" + +#include "codec/decoder.h" + +IndexTask::IndexTask(StreamPtr stream) : + stream_(stream) +{ + SetTitle(tr("Indexing %1:%2").arg(stream_->footage()->filename(), QString::number(stream_->index()))); +} + +void IndexTask::Action() +{ + if (stream_->footage()->decoder().isEmpty()) { + emit Failed(QStringLiteral("Stream has no decoder")); + } else { + DecoderPtr decoder = Decoder::CreateFromID(stream_->footage()->decoder()); + + decoder->set_stream(stream_); + + decoder->Open(); + decoder->Index(); + decoder->Close(); + + emit Succeeeded(); + } +} diff --git a/app/task/index/index.h b/app/task/index/index.h new file mode 100644 index 000000000..868d040af --- /dev/null +++ b/app/task/index/index.h @@ -0,0 +1,20 @@ +#ifndef INDEXTASK_H +#define INDEXTASK_H + +#include "project/item/footage/footage.h" +#include "task/task.h" + +class IndexTask : public Task +{ +public: + IndexTask(StreamPtr stream); + +protected: + virtual void Action() override; + +private: + StreamPtr stream_; + +}; + +#endif // INDEXTASK_H diff --git a/app/task/task.cpp b/app/task/task.cpp index 6c3341127..7c92e1087 100644 --- a/app/task/task.cpp +++ b/app/task/task.cpp @@ -21,168 +21,39 @@ #include "task.h" Task::Task() : - status_(kWaiting), - thread_(this), - text_(tr("Task")), + title_(tr("Task")), cancelled_(false) { - connect(&thread_, SIGNAL(finished()), this, SLOT(ThreadComplete())); } -bool Task::Start() +void Task::Start() { - // Tasks can only start if they're waiting - if (status_ != kWaiting) { - return false; - } + Action(); - // Check if this task has any dependencies (tasks that should complete before this one starts) - - for (int i=0;istatus() == kWaiting || dependency->status() == kWorking) { - - // We need this task to finish before this task can start, so keep waiting - return false; - - } else if (dependency->status() == kError) { - - // A dependency errored, so this task is likely invalid too - set_error(tr("A dependency task failed")); - set_status(kError); - return false; - - } - } - - cancelled_ = false; - - // Run Prologue() function - if (!Prologue()) { - set_status(kError); - return false; - } - - set_status(kWorking); - - thread_.start(); - - return true; + emit Finished(); } -bool Task::Prologue() +const QString &Task::GetTitle() { - return true; -} - -bool Task::Action() -{ - return true; -} - -bool Task::Epilogue() -{ - return true; -} - -const Task::Status &Task::status() -{ - return status_; -} - -const QString &Task::text() -{ - return text_; -} - -const QString &Task::error() -{ - return error_; -} - -void Task::AddDependency(Task *dependency) -{ - // Dependencies cannot be added if the Task is working or complete - Q_ASSERT(status_ == kWaiting); - - dependencies_.append(dependency); -} - -void Task::ResetState() -{ - if (status_ == kWaiting) { - return; - } - - if (status_ == kWorking) { - Cancel(); - } - - cancelled_ = false; - - set_error(QString()); - - set_status(kWaiting); + return title_; } void Task::Cancel() { - if (status_ == kWaiting) { - set_status(kFinished); - } - - if (status_ != kWorking) { - return; - } - cancelled_ = true; - - // FIXME: Should we limit the wait time? - thread_.wait(); } -void Task::set_error(const QString &s) +void Task::SetErrorText(const QString &s) { error_ = s; } -void Task::set_text(const QString &s) +void Task::SetTitle(const QString &s) { - text_ = s; + title_ = s; } -bool Task::cancelled() +bool Task::IsCancelled() { return cancelled_; } - -void Task::set_status(const Task::Status &status) -{ - status_ = status; - - if (status_ == kFinished || status_ == kError) { - emit Finished(); - } - - emit StatusChanged(status_); -} - -void Task::ThreadComplete() -{ - // thread_.result() will be set to the return value of Action() - bool succeeded = thread_.result(); - - // Run the Prologue() function for any final tasks - // User cancelling is not considered an error, so we need to check it too - if (succeeded && !cancelled()) { - succeeded = Epilogue(); - } - - // If everything succeeded, we set the status accordingly - if (succeeded) { - set_status(kFinished); - } else { - set_status(kError); - } -} diff --git a/app/task/task.h b/app/task/task.h index 2fc7a86bd..9c2f63f47 100644 --- a/app/task/task.h +++ b/app/task/task.h @@ -24,8 +24,6 @@ #include #include -#include "task/taskthread.h" - /** * @brief A base class for background tasks running in Olive. * @@ -47,31 +45,17 @@ class Task : public QObject { Q_OBJECT public: - /** - * @brief The Status enum - * - * All states that a Task can be in. When subclassing, you don't need to set the Task's status as the base class - * does that automatically. - */ - enum Status { - /// This Task is yet to start - kWaiting, - - /// This Task is currently running (see Action()) - kWorking, - - /// This Task has completed successfully - kFinished, - - /// This Task failed and could not complete - kError - }; - /** * @brief Task Constructor */ Task(); + /** + * @brief Retrieve the current title of this Task + */ + const QString& GetTitle(); + +public slots: /** * @brief Try to start this Task * @@ -81,29 +65,18 @@ public: * This function also checks its dependency Tasks and will only start if all of them are complete. If they are still * working, this function will return FALSE and the status will continue to be kWaiting. If any of them failed, this * Task will also fail - this function will return FALSE and the status will be set to kError. - * - * @return - * - * TRUE if the Task started, FALSE if not. */ - bool Start(); + void Start(); /** - * @brief Perform opening tasks before main Task thread begins + * @brief Cancel the Task * - * If a Task needs to perform any actions in the main thread before starting the Task's thread, (e.g. copying or - * altering information) this function should be overridden and those actions should be performed here. It's - * guaranteed that Prologue() will run in the main thread, and as such, functions here should remain as minimal as - * possible as to not block the main thread for a noticeable amount of time. If your Task does not need any such - * actions, you don't need to override this. - * - * @return - * - * TRUE if the prologue was successful and we can start the Task now. If Prologue returns FALSE, the thread is never - * created and Action()/Epilogue() are never run. + * Sends a signal to the Task to stop as soon as possible. Always call this directly or connect with + * Qt::DirectConnection, or else it'll be queued *after* the task has already finished. */ - virtual bool Prologue(); + void Cancel(); +protected: /** * @brief The main Task function which is run in a separate thread * @@ -119,86 +92,15 @@ public: * could not finish, not if the Task found a negative result (see Task documentation for details). Before returning * FALSE, it's recommended to use set_error() to signal to the user what caused the failure. */ - virtual bool Action(); + virtual void Action() = 0; - /** - * @brief Perform any closing Tasks in the main thread after the Task thread finishes - * - * It's likely your Task modifies data used throughout the program in some way, and to prevent race conditions, it's - * recommended to work with "copies" of that data in Action() (which is run in separate thread) and never - * access/modify any data used in other threads. Then, after the Action() thread is complete, that data can be used to - * "apply" that data in the main thread here. - * - * As this runs in the main thread, these functions shouldn't be kept fairly minimal to prevent blocking the main - * thread. - * - * @return - * - * TRUE if the Epilogue completed successfully. FALSE if not. A FALSE result here is considered a complete failure - * of the Task, even though the bulk of the processing has been performed in Action(). - */ - virtual bool Epilogue(); - - /** - * @brief Current status of the Task - * - * @return - * - * A member of the Task::Status enum. - */ - const Status& status(); - - /** - * @brief Retrieve the current title of this Task - */ - const QString& text(); - - /** - * @brief Retrieve the current error message (empty if no error) - */ - const QString& error(); - - /** - * @brief Add a dependency Task - * - * If another Task needs to complete before this one can begin, it can be added as a "dependency task". If a task - * has dependencies, Start() will not start the task until the dependency tasks have all completed. If any of the - * dependency tasks fail, this Task will also fail before starting. - * - * Dependencies can only be added if the Task is kWaiting. - * - * Naturally Tasks should never be dependent on each other. Circular dependencies will result in Tasks that never - * begin. - * - * @param dependency - */ - void AddDependency(Task* dependency); - - /** - * @brief Reset this Task back to the waiting state - */ - void ResetState(); - -public slots: - /** - * @brief Cancel the Task - * - * Sends a signal to the Task to stop and waits for the Task to finish before returning. Tasks must be responsive to - * cancelling so that the main thread doesn't halt for too long. - * - * Cancel()'s function is fairly simple, it sets cancelled_ to TRUE and waits for the thread to return. It's the - * responsibility of the code in Action() to be able to respond quickly to cancelled_ changing. - */ - void Cancel(); - -protected: /** * @brief Set the error message * * It is recommended to use this if your Action() function ever returns FALSE to tell the user why the failure * occurred. */ - void set_error(const QString& s); + void SetErrorText(const QString& s); /** * @brief Set the Task title @@ -207,19 +109,14 @@ protected: * and shouldn't need to change during the life of the Task. To show an error message, it's recommended to use * set_error() instead. */ - void set_text(const QString& s); + void SetTitle(const QString& s); /** * @brief Returns whether the thread has been explicitly cancelled or not */ - bool cancelled(); + bool IsCancelled(); signals: - /** - * @brief Signal emitted whenever the Task status changes - */ - void StatusChanged(Task::Status s); - /** * @brief Signal emitted whenever progress is made * @@ -231,9 +128,10 @@ signals: */ void ProgressChanged(int p); - /** - * @brief Signal emitted when the Task finishes whether it succeeded or failed - */ + void Succeeeded(); + + void Failed(const QString& error); + void Finished(); /** @@ -242,30 +140,12 @@ signals: void Removed(); private: - /** - * @brief Set the status of this Task (also emits StatusChanged()) - */ - void set_status(const Task::Status& status); - - Status status_; - - TaskThread thread_; - - QString text_; + QString title_; QString error_; - QList dependencies_; + QAtomicInt cancelled_; - bool cancelled_; - -private slots: - /** - * @brief A slot when the inner thread completes either successfully or unsuccessfully - */ - void ThreadComplete(); }; -using TaskPtr = std::shared_ptr; - #endif // TASK_H diff --git a/app/task/taskmanager.cpp b/app/task/taskmanager.cpp index c044fbce8..66413aa71 100644 --- a/app/task/taskmanager.cpp +++ b/app/task/taskmanager.cpp @@ -23,119 +23,219 @@ #include #include -TaskManager TaskManager::instance_; +TaskManager* TaskManager::instance_ = nullptr; -TaskManager::TaskManager() +TaskManager::TaskManager() : + active_thread_count_(0) { - maximum_task_count_ = QThread::idealThreadCount(); + // Initialize threads to run tasks on + threads_.resize(QThread::idealThreadCount()); + + for (int i=0;istart(QThread::LowPriority); + threads_.replace(i, {t, false}); + } } TaskManager::~TaskManager() { - Clear(); + // First send the signal to all tasks to start cancelling + foreach (const TaskContainer& task_info, tasks_) { + if (task_info.status == kWorking) { + task_info.task->Cancel(); + } + } + + // Next, signal each thread to quit as its next event in the queue + foreach (const ThreadContainer& tc, threads_) { + tc.thread->quit(); + } + + // Wait for each thread's event queue to finish + foreach (const ThreadContainer& tc, threads_) { + tc.thread->wait(); + + // This is technically unnecessary since each QThread is a child of this object, but we may as well + delete tc.thread; + } + + // Finally delete all task objects (they shouldn't have been deleted by TaskSucceeded() or TaskFailed() because our + // event queue shouldn't be active by this point + foreach (const TaskContainer& task_info, tasks_) { + delete task_info.task; + } +} + +void TaskManager::CreateInstance() +{ + instance_ = new TaskManager(); +} + +void TaskManager::DestroyInstance() +{ + delete instance_; } TaskManager *TaskManager::instance() { - return &instance_; + return instance_; } -void TaskManager::AddTask(TaskPtr t) +void TaskManager::AddTask(Task* t) { // Connect Task's status signal to the Callback - connect(t.get(), SIGNAL(StatusChanged(Task::Status)), this, SLOT(TaskCallback(Task::Status))); + connect(t, &Task::Succeeeded, this, &TaskManager::TaskSucceeded, Qt::QueuedConnection); + connect(t, &Task::Failed, this, &TaskManager::TaskFailed, Qt::QueuedConnection); // Add the Task to the queue - tasks_.append(t); + tasks_.append({t, kWaiting}); // Emit signal that a Task was added - emit TaskAdded(t.get()); + emit TaskAdded(t); // Scan through queue and start any Tasks that can (including this one) StartNextWaiting(); } -void TaskManager::Clear() -{ - // Delete Tasks from memory - for (int i=0;iCancel(); - } - tasks_.clear(); -} - void TaskManager::StartNextWaiting() { - // Count the tasks that are currently active - int working_count = 0; + // If there are no tasks in the queue, there is nothing to be done + if (tasks_.isEmpty()) { + return; + } - for (int i=0;istatus() == Task::kWorking) { - - // Task is active, add it to the count - working_count++; - - } else if (t->status() == Task::kWaiting) { - - // Task is waiting and we have available threads, try to start it - if (t->Start()) { - // If it started, add it to the working count - working_count++; - } + // If all threads are occupied, nothing to be done + if (active_thread_count_ == threads_.size()) { + return; + } + // Create a list of tasks that are waiting + QList waiting_tasks; + foreach (const TaskContainer& task_info, tasks_) { + if (task_info.status == kWaiting) { + waiting_tasks.append(task_info.task); } + } - // Check if the count exceeds our maximum threads, if so stop here - if (working_count == maximum_task_count_) { - break; + // No tasks waiting to start + if (waiting_tasks.isEmpty()) { + return; + } + + // For any inactive threads, + for (int i=0;imoveToThread(threads_.at(i).thread); + + threads_[i].active = true; + active_thread_count_++; + + SetTaskStatus(task, kWorking); + + QMetaObject::invokeMethod(task, + "Start", + Qt::QueuedConnection); + + if (active_thread_count_ == threads_.size() || waiting_tasks.isEmpty()) { + break; + } } } } void TaskManager::DeleteTask(Task *t) { - // Cancel the task - t->Cancel(); + if (GetTaskStatus(t) == kWorking) { + // Send a signal to the task to cancel, it will likely continue to cancel in the background after it's removed + t->Cancel(); + } // Remove instances of Task from queue for (int i=0;iRemoved(); + if (tasks_.at(i).task == t) { tasks_.removeAt(i); break; } } + emit t->Removed(); + + if (GetTaskStatus(t) != kWorking) { + // If the task isn't doing anything, we can simply delete it + delete t; + } +} + +void TaskManager::TaskFinished(Task* task) +{ + // Set this thread's active value to false + for (int i=0;ithread()) { + threads_[i].active = false; + } + } + + // Decrement the active thread count + active_thread_count_--; +} + +TaskManager::TaskStatus TaskManager::GetTaskStatus(Task *t) +{ + foreach (const TaskContainer& container, tasks_) { + if (container.task == t) { + return container.status; + } + } + + return kError; +} + +void TaskManager::SetTaskStatus(Task *t, TaskStatus status) +{ + for (int i=0;i(sender()); - if (status == Task::kFinished) { - // The Task was successful, remove this Task from the queue - DeleteTask(static_cast(sender())); + SetTaskStatus(task_sender, kFinished); + + TaskFinished(task_sender); + + // Delete this task + DeleteTask(task_sender); +} + +void TaskManager::TaskFailed() +{ + Task* task_sender = static_cast(sender()); + + SetTaskStatus(task_sender, kError); + + TaskFinished(task_sender); + + // If this task has already been deleted, we'll free its memory now + bool was_deleted = true; + + for (int i=0;iAddTask(task_); -} - -void TaskManager::AddTaskCommand::undo() -{ - TaskManager::instance()->DeleteTask(task_.get()); - - task_->ResetState(); + if (was_deleted) { + delete task_sender; + } } diff --git a/app/task/taskmanager.h b/app/task/taskmanager.h index 24962e863..00de77b1c 100644 --- a/app/task/taskmanager.h +++ b/app/task/taskmanager.h @@ -24,6 +24,7 @@ #include #include +#include "common/constructors.h" #include "task/task.h" /** @@ -50,25 +51,11 @@ public: */ virtual ~TaskManager(); - /** - * @brief Deleted copy constructor - */ - TaskManager(const TaskManager& other) = delete; + DISABLE_COPY_MOVE(TaskManager) - /** - * @brief Deleted move constructor - */ - TaskManager(TaskManager&& other) = delete; + static void CreateInstance(); - /** - * @brief Deleted copy assignment - */ - TaskManager& operator=(const TaskManager& other) = delete; - - /** - * @brief Deleted move assignment - */ - TaskManager& operator=(TaskManager&& other) = delete; + static void DestroyInstance(); static TaskManager* instance(); @@ -87,38 +74,7 @@ public: * * The task to add and run. TaskManager takes ownership of this Task and will be responsible for freeing it. */ - void AddTask(TaskPtr t); - - /** - * @brief Forcibly cancel all commands and clear them - */ - void Clear(); - - /** - * @brief Undoable command for adding a Task to the TaskManager - */ - class AddTaskCommand : public QUndoCommand { - public: - AddTaskCommand(TaskPtr t, QUndoCommand* parent = nullptr); - - /** - * @brief Adds the Task to the TaskManager - * - * If there are available threads, TaskManager will start running it. - */ - virtual void redo() override; - - /** - * @brief Undoes adding the Task - * - * If the Task is running, it is cancelled. Then the Task is removed from the TaskManager and the Task's state is - * reset. - */ - virtual void undo() override; - - private: - TaskPtr task_; - }; + void AddTask(Task *t); signals: /** @@ -131,6 +87,36 @@ signals: void TaskAdded(Task* t); private: + /** + * @brief The Status enum + * + * All states that a Task can be in. When subclassing, you don't need to set the Task's status as the base class + * does that automatically. + */ + enum TaskStatus { + /// This Task is yet to start + kWaiting, + + /// This Task is currently running (see Action()) + kWorking, + + /// This Task has completed successfully + kFinished, + + /// This Task failed and could not complete + kError + }; + + struct TaskContainer { + Task* task; + TaskStatus status; + }; + + struct ThreadContainer { + QThread* thread; + bool active; + }; + /** * @brief Scan through the task queue and start any Tasks that are able to start * @@ -155,35 +141,36 @@ private: */ void DeleteTask(Task* t); + void TaskFinished(Task *task); + + TaskStatus GetTaskStatus(Task* t); + + void SetTaskStatus(Task* t, TaskStatus status); + /** * @brief Internal task array */ - QVector tasks_; + QVector tasks_; /** - * @brief Constant set at run-time of how many Tasks can run concurrently - * - * Currently set in the Constructor to QThread::idealThreadCount() + * @brief Background threads to run tasks on */ - int maximum_task_count_; + QVector threads_; + + /** + * @brief Value for how many threads are currently active + */ + int active_thread_count_; /** * @brief TaskManager singleton instance */ - static TaskManager instance_; + static TaskManager* instance_; private slots: - /** - * @brief Callback when a Task's status changes - * - * When a Task is added, it's connected to this so TaskManager is alerted whenever a Task's status changes. It can - * therefore keep track of Tasks starting, completing, or failing. - * - * @param status - * - * The new status of the Task - */ - void TaskCallback(Task::Status status); + void TaskSucceeded(); + + void TaskFailed(); }; diff --git a/app/task/taskthread.cpp b/app/task/taskthread.cpp deleted file mode 100644 index 234fbf8ef..000000000 --- a/app/task/taskthread.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "taskthread.h" - -#include "task/task.h" - -TaskThread::TaskThread(Task *parent) : - parent_(parent), - result_(false) -{ -} - -void TaskThread::run() -{ - result_ = parent_->Action(); -} - -bool TaskThread::result() -{ - return result_; -} diff --git a/app/task/taskthread.h b/app/task/taskthread.h deleted file mode 100644 index 3af5e2fd4..000000000 --- a/app/task/taskthread.h +++ /dev/null @@ -1,50 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef TASKTHREAD_H -#define TASKTHREAD_H - -#include - -class Task; - -/** - * @brief An internal class only used by Task. - * - * TaskThread is a simple QThread subclass designed to create a thread and run the - * Task's Action() function. It also stores the result of Action() which can be read using result() when the thread - * signals that it has finished(). - */ -class TaskThread : public QThread -{ -public: - TaskThread(Task* parent); - - virtual void run() override; - - bool result(); - -private: - Task* parent_; - - bool result_; -}; - -#endif // TASKTHREAD_H diff --git a/app/widget/taskview/taskview.cpp b/app/widget/taskview/taskview.cpp index 6332febc8..499c6cc4f 100644 --- a/app/widget/taskview/taskview.cpp +++ b/app/widget/taskview/taskview.cpp @@ -44,9 +44,5 @@ TaskView::TaskView(QWidget* parent) : void TaskView::AddTask(Task *t) { // Create TaskViewItem (UI representation of a Task) and connect it - TaskViewItem* item = new TaskViewItem(central_widget_); - - item->SetTask(t); - - layout_->insertWidget(0, item); + layout_->insertWidget(0, new TaskViewItem(t)); } diff --git a/app/widget/taskview/taskviewitem.cpp b/app/widget/taskview/taskviewitem.cpp index bd0e28083..d270b07ef 100644 --- a/app/widget/taskview/taskviewitem.cpp +++ b/app/widget/taskview/taskviewitem.cpp @@ -24,9 +24,9 @@ #include "ui/icons/icons.h" -TaskViewItem::TaskViewItem(QWidget *parent) : +TaskViewItem::TaskViewItem(Task* task, QWidget *parent) : QFrame(parent), - task_(nullptr) + task_(task) { // Draw border around this item setFrameShape(QFrame::StyledPanel); @@ -36,6 +36,7 @@ TaskViewItem::TaskViewItem(QWidget *parent) : // Create header label task_name_lbl_ = new QLabel(this); + task_name_lbl_->setText(task_->GetTitle()); layout->addWidget(task_name_lbl_); // Create center layout (combines progress bar and a cancel button) @@ -55,54 +56,9 @@ TaskViewItem::TaskViewItem(QWidget *parent) : // Create status label task_status_lbl_ = new QLabel(this); layout->addWidget(task_status_lbl_); -} - -void TaskViewItem::SetTask(Task *t) -{ - // Check if we already have a task and disconnect from it if so - if (task_ != nullptr) { - disconnect(task_, SIGNAL(StatusChanged(Task::Status)), this, SLOT(TaskStatusChange(Task::Status))); - disconnect(task_, SIGNAL(ProgressChanged(int)), progress_bar_, SLOT(setValue(int))); - disconnect(task_, SIGNAL(destroyed()), this, SLOT(deleteLater())); - } - - // Set task - task_ = t; - - // Set name label to the name (bolded) - task_name_lbl_->setText(QStringLiteral("%1").arg(task_->text())); // Connect to the task - connect(task_, SIGNAL(StatusChanged(Task::Status)), this, SLOT(TaskStatusChange(Task::Status))); - connect(task_, SIGNAL(ProgressChanged(int)), progress_bar_, SLOT(setValue(int))); - connect(task_, SIGNAL(Removed()), this, SLOT(deleteLater())); - connect(cancel_btn_, SIGNAL(clicked(bool)), task_, SLOT(Cancel())); -} - -void TaskViewItem::TaskStatusChange(Task::Status status) -{ - switch (status) { - case Task::kWaiting: - task_status_lbl_->setText(tr("Waiting...")); - progress_bar_->setValue(0); - cancel_btn_->setEnabled(true); - break; - case Task::kWorking: - task_status_lbl_->setText(tr("Working...")); - progress_bar_->setValue(0); - cancel_btn_->setEnabled(true); - break; - case Task::kFinished: - task_status_lbl_->setText(tr("Done")); - progress_bar_->setValue(100); - cancel_btn_->setEnabled(false); - break; - case Task::kError: - task_status_lbl_->setText( - tr("Error: %1").arg(static_cast(sender())->error()) - ); - progress_bar_->setValue(0); - cancel_btn_->setEnabled(false); - break; - } + connect(task_, &Task::ProgressChanged, progress_bar_, &QProgressBar::setValue); + connect(task_, &Task::Removed, this, &TaskViewItem::deleteLater); + connect(cancel_btn_, &QPushButton::clicked, task_, &Task::Cancel, Qt::DirectConnection); } diff --git a/app/widget/taskview/taskviewitem.h b/app/widget/taskview/taskviewitem.h index 9ac712b0c..1c5edf4f3 100644 --- a/app/widget/taskview/taskviewitem.h +++ b/app/widget/taskview/taskviewitem.h @@ -41,16 +41,7 @@ class TaskViewItem : public QFrame { Q_OBJECT public: - TaskViewItem(QWidget* parent); - - /** - * @brief Connects a Task to this object - * - * If a Task has already been connected, this will disconnect this TaskViewItem from the previously connected - * Task before connecting to the next one - however there are very few circumstances where this would be necessary - * since TaskViewItem is designed to delete itself when a Task is complete. - */ - void SetTask(Task* t); + TaskViewItem(Task *task, QWidget* parent = nullptr); private: QLabel* task_name_lbl_; @@ -60,9 +51,6 @@ private: Task* task_; -private slots: - void TaskStatusChange(Task::Status status); - }; #endif // TASKVIEWITEM_H