From a818e17c551fd4ff4be1fb020cb6e3831ce62f91 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 18:06:50 +1000 Subject: [PATCH] export: made progress on headless export Still incomplete and unusable, but less so than before. --- app/cli/cliexport/cliexportmanager.cpp | 30 ++++ app/cli/cliexport/cliexportmanager.h | 36 ++++ app/cli/cliprogress/cliprogressdialog.cpp | 9 +- app/cli/cliprogress/cliprogressdialog.h | 4 +- app/cli/clitask/clitaskdialog.cpp | 10 +- app/cli/clitask/clitaskdialog.h | 6 + app/codec/encoder.cpp | 57 ++++++ app/codec/encoder.h | 3 + app/core.cpp | 206 ++++++++++++++++------ app/core.h | 18 +- app/main.cpp | 20 +-- app/task/export/exportparams.cpp | 22 +++ app/task/export/exportparams.h | 2 + app/task/project/load/load.cpp | 2 + 14 files changed, 337 insertions(+), 88 deletions(-) create mode 100644 app/cli/cliexport/cliexportmanager.cpp create mode 100644 app/cli/cliexport/cliexportmanager.h diff --git a/app/cli/cliexport/cliexportmanager.cpp b/app/cli/cliexport/cliexportmanager.cpp new file mode 100644 index 000000000..de0145729 --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.cpp @@ -0,0 +1,30 @@ +/*** + + 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 "cliexportmanager.h" + +OLIVE_NAMESPACE_ENTER + +CLIExportManager::CLIExportManager() +{ + +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/cli/cliexport/cliexportmanager.h b/app/cli/cliexport/cliexportmanager.h new file mode 100644 index 000000000..6d3fc346b --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.h @@ -0,0 +1,36 @@ +/*** + + 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 CLIEXPORTMANAGER_H +#define CLIEXPORTMANAGER_H + +#include "task/export/export.h" + +OLIVE_NAMESPACE_ENTER + +class CLIExportManager : public QObject +{ +public: + CLIExportManager(); +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CLIEXPORTMANAGER_H diff --git a/app/cli/cliprogress/cliprogressdialog.cpp b/app/cli/cliprogress/cliprogressdialog.cpp index fa846c1ff..88c0cfd57 100644 --- a/app/cli/cliprogress/cliprogressdialog.cpp +++ b/app/cli/cliprogress/cliprogressdialog.cpp @@ -27,9 +27,10 @@ OLIVE_NAMESPACE_ENTER CLIProgressDialog::CLIProgressDialog(const QString& title, QObject *parent) : QObject(parent), title_(title), - progress_(0), + progress_(-1), drawn_(false) { + SetProgress(0); } void CLIProgressDialog::Update() @@ -68,7 +69,7 @@ void CLIProgressDialog::Update() std::cout << "["; // Get UI bar progress - int bar_prog = qRound(progress_ * 0.01 * progress_bar_columns); + int bar_prog = qRound(progress_ * progress_bar_columns); // Draw filled in bar for (int i=0;iGetTitle(), parent) + CLIProgressDialog(task->GetTitle(), parent), + task_(task) { - // FIXME: Still developing this, don't try to use + connect(task_, &Task::ProgressChanged, this, &CLITaskDialog::SetProgress); +} + +bool CLITaskDialog::Run() +{ + return task_->Start(); } OLIVE_NAMESPACE_EXIT diff --git a/app/cli/clitask/clitaskdialog.h b/app/cli/clitask/clitaskdialog.h index 6408b238e..c574b6011 100644 --- a/app/cli/clitask/clitaskdialog.h +++ b/app/cli/clitask/clitaskdialog.h @@ -28,9 +28,15 @@ OLIVE_NAMESPACE_ENTER class CLITaskDialog : public CLIProgressDialog { + Q_OBJECT public: CLITaskDialog(Task *task, QObject* parent = nullptr); + bool Run(); + +private: + Task* task_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 43e0ca1a6..388278525 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -158,6 +158,63 @@ void EncodingParams::SetExportLength(const rational &export_length) export_length_ = export_length; } +void EncodingParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("encode")); + + writer->writeTextElement(QStringLiteral("filename"), filename_); + + writer->writeStartElement(QStringLiteral("video")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(video_enabled_)); + + if (video_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(video_codec_)); + writer->writeTextElement(QStringLiteral("width"), QString::number(video_params_.width())); + writer->writeTextElement(QStringLiteral("height"), QString::number(video_params_.height())); + writer->writeTextElement(QStringLiteral("format"), QString::number(video_params_.format())); + writer->writeTextElement(QStringLiteral("timebase"), video_params_.time_base().toString()); + writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params_.divider())); + writer->writeTextElement(QStringLiteral("bitrate"), QString::number(video_bit_rate_)); + writer->writeTextElement(QStringLiteral("maxbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("bufsize"), QString::number(video_buffer_size_)); + writer->writeTextElement(QStringLiteral("threads"), QString::number(video_threads_)); + + if (!video_opts_.isEmpty()) { + writer->writeStartElement(QStringLiteral("opts")); + + QHash::const_iterator i; + for (i=video_opts_.constBegin(); i!=video_opts_.constEnd(); i++) { + writer->writeStartElement(QStringLiteral("entry")); + + writer->writeTextElement(QStringLiteral("key"), i.key()); + writer->writeTextElement(QStringLiteral("value"), i.value()); + + writer->writeEndElement(); // entry + } + + writer->writeEndElement(); // opts + } + } + + writer->writeEndElement(); // video + + writer->writeStartElement(QStringLiteral("audio")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(audio_enabled_)); + + if (audio_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(audio_codec_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(audio_params_.sample_rate())); + writer->writeTextElement(QStringLiteral("channellayout"), QString::number(audio_params_.channel_layout())); + writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); + } + + writer->writeEndElement(); // audio + + writer->writeEndElement(); // encode +} + Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) { Q_UNUSED(id) diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 8a60a75cc..260c735a3 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -23,6 +23,7 @@ #include #include +#include #include "codec/exportcodec.h" #include "codec/exportformat.h" @@ -69,6 +70,8 @@ public: const rational& GetExportLength() const; void SetExportLength(const rational& GetExportLength); + virtual void Save(QXmlStreamWriter* writer) const; + private: QString filename_; diff --git a/app/core.cpp b/app/core.cpp index 70fb71f6c..d737a0563 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -81,18 +81,17 @@ Core *Core::instance() return &instance_; } -bool Core::Start() +int Core::execute(QCoreApplication* a) { - // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes - // the fact that some of the config paths set by default rely on the app name having been set (in main()) - Config::Current().SetDefaults(); + int exit_code = 1; + + // Start core + OLIVE_NAMESPACE::Core::instance()->Start(); // // Parse command line arguments // - QCoreApplication* app = QCoreApplication::instance(); - QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); @@ -111,7 +110,7 @@ bool Core::Start() parser.addOption(headless_export_option); // Parse options - parser.process(*app); + parser.process(*a); QStringList args = parser.positionalArguments(); @@ -120,6 +119,44 @@ bool Core::Start() startup_project_ = args.first(); } + gui_active_ = !parser.isSet(headless_export_option); + + if (gui_active_) { + + // Start GUI + StartGUI(parser.isSet(fullscreen_option)); + + // If we have a startup + QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection); + + // Run application loop and receive exit code + exit_code = a->exec(); + + } else { + + if (parser.isSet(headless_export_option)) { + // Start a headless export + if (StartHeadlessExport()) { + exit_code = 0; + } + } + + } + + + + // Clear core memory + OLIVE_NAMESPACE::Core::instance()->Stop(); + + return exit_code; +} + +void Core::Start() +{ + // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes + // the fact that some of the config paths set by default rely on the app name having been set (in main()) + Config::Current().SetDefaults(); + // Declare custom types for Qt signal/slot system DeclareTypesForQt(); @@ -141,53 +178,6 @@ bool Core::Start() // qInfo() << "Using Qt version:" << qVersion(); - - gui_active_ = !parser.isSet(headless_export_option); - - if (gui_active_) { - - // Start GUI - StartGUI(parser.isSet(fullscreen_option)); - - // Load startup project - if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { - QMessageBox::warning(main_window(), - tr("Failed to open startup file"), - tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), - QMessageBox::Ok); - - startup_project_.clear(); - } - - if (startup_project_.isEmpty()) { - // If no load project is set, create a new one on open - CreateNewProject(); - } else { - OpenProjectInternal(startup_project_); - } - - return true; - - } else { - - if (parser.isSet(headless_export_option)) { - - if (startup_project_.isEmpty()) { - qCritical().noquote() << tr("You must specify a project file to export"); - } else { - OpenProjectInternal(startup_project_); - - qDebug() << "Ready for exporting!"; - - return true; - } - - } - - // Error fallback - return false; - - } } void Core::Stop() @@ -539,6 +529,106 @@ void Core::ProjectWasModified(bool e) } } +bool Core::StartHeadlessExport() +{ + if (startup_project_.isEmpty()) { + qCritical().noquote() << tr("You must specify a project file to export"); + return false; + } + + if (!QFileInfo::exists(startup_project_)) { + qCritical().noquote() << tr("Specified project does not exist"); + return false; + } + + // Start a load task and try running it + ProjectLoadTask plm(startup_project_); + CLITaskDialog task_dialog(&plm); + + if (task_dialog.Run()) { + ProjectPtr p = plm.GetLoadedProjects().first(); + QList items = p->get_items_of_type(Item::kSequence); + + // Check if this project contains sequences + if (items.isEmpty()) { + qCritical().noquote() << tr("Project contains no sequences, nothing to export"); + return false; + } + + SequencePtr sequence = nullptr; + + // Check if this project contains multiple sequences + if (items.size() > 1) { + qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?"); + for (int i=0;iname().toStdString(); + } + + QTextStream stream(stdin); + QString sequence_read; + int sequence_index = -1; + QString quit_code = QStringLiteral("q"); + std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString(); + forever { + std::cout << prompt; + + stream.readLineInto(&sequence_read); + + if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) { + return false; + } + + bool ok; + sequence_index = sequence_read.toInt(&ok); + + if (ok && sequence_index >= 0 && sequence_index < items.size()) { + break; + } else { + qCritical().noquote() << tr("Invalid sequence number"); + } + } + + sequence = std::static_pointer_cast(items.at(sequence_index)); + } else { + sequence = std::static_pointer_cast(items.first()); + } + + ExportParams params; + ExportTask export_task(sequence->viewer_output(), p->color_manager(), params); + CLITaskDialog export_dialog(&export_task); + if (export_dialog.Run()) { + qInfo().noquote() << tr("Export succeeded"); + return true; + } else { + qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError()); + return false; + } + } else { + qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError()); + return false; + } +} + +void Core::OpenStartupProject() +{ + // Load startup project + if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { + QMessageBox::warning(main_window_, + tr("Failed to open startup file"), + tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), + QMessageBox::Ok); + + startup_project_.clear(); + } + + if (startup_project_.isEmpty()) { + // If no load project is set, create a new one on open + CreateNewProject(); + } else { + OpenProjectInternal(startup_project_); + } +} + void Core::DeclareTypesForQt() { qRegisterMetaType(); @@ -559,6 +649,7 @@ void Core::DeclareTypesForQt() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); } void Core::StartGUI(bool full_screen) @@ -877,6 +968,11 @@ bool Core::SaveProjectAs(ProjectPtr p) GetProjectFilter()); if (!fn.isEmpty()) { + QString extension(QStringLiteral(".ove")); + if (!fn.endsWith(extension, Qt::CaseInsensitive)) { + fn.append(extension); + } + p->set_filename(fn); SaveProjectInternal(p); @@ -927,7 +1023,7 @@ void Core::OpenProjectInternal(const QString &filename) //connect(plm, &ProjectLoadManager::ProjectLoaded, this, &Core::AddOpenProject); - CLITaskDialog task_dialog(plm); + } } diff --git a/app/core.h b/app/core.h index 07aeccb08..9b3d25728 100644 --- a/app/core.h +++ b/app/core.h @@ -66,12 +66,14 @@ public: */ static Core* instance(); + int execute(QCoreApplication *a); + /** * @brief Start Olive Core * * Main application launcher. Parses command line arguments and constructs main window (if entering a GUI mode). */ - bool Start(); + void Start(); /** * @brief Stop Olive Core @@ -405,11 +407,6 @@ private: */ void PushRecentlyOpenedProject(const QString &s); - /** - * @brief Internal project open - */ - void OpenProjectInternal(const QString& filename); - /** * @brief Declare custom types/classes for Qt's signal/slot system * @@ -507,6 +504,15 @@ private slots: void ProjectWasModified(bool e); + bool StartHeadlessExport(); + + void OpenStartupProject(); + + /** + * @brief Internal project open + */ + void OpenProjectInternal(const QString& filename); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/main.cpp b/app/main.cpp index e87f1fd34..0026395f0 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -86,23 +86,5 @@ int main(int argc, char *argv[]) { avfilter_register_all(); #endif - int exit_code; - - // Start core - if (OLIVE_NAMESPACE::Core::instance()->Start()) { - - // Run application loop and receive exit code - exit_code = a.exec(); - - } else { - - // Core failed to start, exit now - exit_code = 1; - - } - - // Clear core memory - OLIVE_NAMESPACE::Core::instance()->Stop(); - - return exit_code; + return OLIVE_NAMESPACE::Core::instance()->execute(&a); } diff --git a/app/task/export/exportparams.cpp b/app/task/export/exportparams.cpp index a603735ee..299c1fca3 100644 --- a/app/task/export/exportparams.cpp +++ b/app/task/export/exportparams.cpp @@ -100,4 +100,26 @@ QMatrix4x4 ExportParams::GenerateMatrix(ExportParams::VideoScalingMethod method, return preview_matrix; } +void ExportParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("export")); + + writer->writeTextElement(QStringLiteral("encoder"), encoder_id_); + + writer->writeTextElement(QStringLiteral("vscale"), QString::number(video_scaling_method_)); + + writer->writeTextElement(QStringLiteral("range"), QString::number(has_custom_range_)); + + writer->writeTextElement(QStringLiteral("customrangein"), custom_range_.in().toString()); + + writer->writeTextElement(QStringLiteral("customrangeout"), custom_range_.out().toString()); + + // FIXME: Change this when color chains are implemented + writer->writeTextElement(QStringLiteral("color"), color_transform_.output()); + + EncodingParams::Save(writer); + + writer->writeEndElement(); // export +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/exportparams.h b/app/task/export/exportparams.h index a74ea3c5d..ed6106d67 100644 --- a/app/task/export/exportparams.h +++ b/app/task/export/exportparams.h @@ -56,6 +56,8 @@ public: int source_width, int source_height, int dest_width, int dest_height); + virtual void Save(QXmlStreamWriter* writer) const override; + private: QString encoder_id_; diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 20d05ce7d..932a94c74 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -70,6 +70,8 @@ bool ProjectLoadTask::Run() project_file.close(); + emit ProgressChanged(1); + if (reader.hasError()) { SetError(reader.errorString()); return false;