try to index media immediately on import

Since indexing is supplemental, it can be made a background task that can occur
at more or less any time. So we run it as soon as possible.
This commit is contained in:
itsmattkc
2020-01-17 03:58:29 +11:00
parent 63b3c60017
commit 653709dc84
28 changed files with 410 additions and 735 deletions
+14
View File
@@ -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 &params)
qCritical() << "Conform called on an audio decoder that does not have a handler for it:" << id();
abort();
}
void Decoder::Index()
{
}
+11
View File
@@ -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_;
+25 -23
View File
@@ -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 &params)
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 &params)
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()) {
+1 -14
View File
@@ -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 &params);
void ValidateIndex();
/**
* @brief Used internally to load a frame index into frame_index_
*
+16 -32
View File
@@ -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>("Task::Status");
qRegisterMetaType<NodeDependency>();
qRegisterMetaType<rational>();
qRegisterMetaType<OpenGLTexturePtr>();
@@ -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);
+1 -11
View File
@@ -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
-2
View File
@@ -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
+3 -3
View File
@@ -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
-24
View File
@@ -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_;
}
-46
View File
@@ -1,46 +0,0 @@
#ifndef PROJECTFILEMANAGERBASE_H
#define PROJECTFILEMANAGERBASE_H
#include <QObject>
#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
+2 -2
View File
@@ -4,10 +4,10 @@
#include <QFileInfoList>
#include <QUndoCommand>
#include "projectfilemanagerbase.h"
#include "projectviewmodel.h"
#include "task/task.h"
class ProjectImportManager : public ProjectFileManagerBase
class ProjectImportManager : public Task
{
Q_OBJECT
public:
+1 -1
View File
@@ -46,5 +46,5 @@ void ProjectLoadManager::Action()
project_file.close();
}
emit Finished();
emit Succeeeded();
}
+3 -2
View File
@@ -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:
+1 -1
View File
@@ -32,5 +32,5 @@ void ProjectSaveManager::Action()
project_file.close();
}
emit Finished();
emit Succeeeded();
}
+3 -2
View File
@@ -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:
+2 -2
View File
@@ -14,13 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
)
+22
View File
@@ -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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
task/index/index.h
task/index/index.cpp
PARENT_SCOPE
)
+26
View File
@@ -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();
}
}
+20
View File
@@ -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
+10 -139
View File
@@ -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;i<dependencies_.size();i++) {
Task* dependency = dependencies_.at(i);
if (dependency->status() == 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);
}
}
+22 -142
View File
@@ -24,8 +24,6 @@
#include <memory>
#include <QObject>
#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<Task*> 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<Task>;
#endif // TASK_H
+166 -66
View File
@@ -23,119 +23,219 @@
#include <QDebug>
#include <QThread>
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;i<threads_.size();i++) {
QThread* t = new QThread(this);
t->start(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;i<tasks_.size();i++) {
tasks_.at(i)->Cancel();
}
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;i<tasks_.size();i++) {
TaskPtr t = tasks_.at(i);
if (t->status() == 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<Task*> 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;i<threads_.size();i++) {
if (!threads_.at(i).active) {
// This thread is inactive and needs a new Task
Task* task = waiting_tasks.takeFirst();
task->moveToThread(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;i<tasks_.size();i++) {
if (tasks_.at(i).get() == t) {
emit t->Removed();
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;i<threads_.size();i++) {
if (threads_.at(i).thread == task->thread()) {
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<tasks_.size();i++) {
TaskContainer& cont = tasks_[i];
if (cont.task == t) {
cont.status = status;
break;
}
}
}
void TaskManager::TaskCallback(Task::Status status)
void TaskManager::TaskSucceeded()
{
if (status == Task::kFinished || status == Task::kError) {
// The Task has finished, we can start a new one
StartNextWaiting();
Task* task_sender = static_cast<Task*>(sender());
if (status == Task::kFinished) {
// The Task was successful, remove this Task from the queue
DeleteTask(static_cast<Task*>(sender()));
SetTaskStatus(task_sender, kFinished);
TaskFinished(task_sender);
// Delete this task
DeleteTask(task_sender);
}
void TaskManager::TaskFailed()
{
Task* task_sender = static_cast<Task*>(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;i<tasks_.size();i++) {
if (tasks_.at(i).task == task_sender) {
was_deleted = false;
break;
}
}
}
TaskManager::AddTaskCommand::AddTaskCommand(TaskPtr t, QUndoCommand *parent) :
QUndoCommand(parent),
task_(t)
{
}
void TaskManager::AddTaskCommand::redo()
{
TaskManager::instance()->AddTask(task_);
}
void TaskManager::AddTaskCommand::undo()
{
TaskManager::instance()->DeleteTask(task_.get());
task_->ResetState();
if (was_deleted) {
delete task_sender;
}
}
+53 -66
View File
@@ -24,6 +24,7 @@
#include <QVector>
#include <QUndoCommand>
#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<TaskPtr> tasks_;
QVector<TaskContainer> 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<ThreadContainer> 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();
};
-39
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#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_;
}
-50
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#ifndef TASKTHREAD_H
#define TASKTHREAD_H
#include <QThread>
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
+1 -5
View File
@@ -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));
}
+6 -50
View File
@@ -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("<b>%1</b>").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<Task*>(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);
}
+1 -13
View File
@@ -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