diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index b4ef307a3..cb5080402 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -23,6 +23,7 @@ #include #include "common/timecodefunctions.h" +#include "common/xmlutils.h" #include "ffmpeg/ffmpegencoder.h" #include "oiio/oiioencoder.h" @@ -99,6 +100,17 @@ EncodingParams::EncodingParams() : { } +QDir EncodingParams::GetPresetPath() +{ + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("exportpresets")); +} + +QStringList EncodingParams::GetListOfPresets() +{ + QDir d = EncodingParams::GetPresetPath(); + return d.entryList(QDir::Files); +} + void EncodingParams::EnableVideo(const VideoParams &video_params, const ExportCodec::Codec &vcodec) { video_enabled_ = true; @@ -142,9 +154,49 @@ void EncodingParams::DisableSubtitles() subtitles_enabled_ = false; } +bool EncodingParams::Load(QXmlStreamReader *reader) +{ + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("export")) { + int version = 0; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("version")) { + version = attr.value().toInt(); + } + } + + switch (version) { + case 1: + return LoadV1(reader); + } + } else { + reader->skipCurrentElement(); + } + } + + return false; +} + +bool EncodingParams::Load(QIODevice *device) +{ + QXmlStreamReader reader(device); + return Load(&reader); +} + +void EncodingParams::Save(QIODevice *device) const +{ + QXmlStreamWriter writer(device); + Save(&writer); +} + void EncodingParams::Save(QXmlStreamWriter *writer) const { - writer->writeTextElement(QStringLiteral("version"), QString::number(kEncoderParamsVersion)); + writer->writeStartDocument(); + + writer->writeStartElement(QStringLiteral("export")); + + writer->writeAttribute(QStringLiteral("version"), QString::number(kEncoderParamsVersion)); writer->writeTextElement(QStringLiteral("filename"), filename_); writer->writeTextElement(QStringLiteral("format"), QString::number(format_)); @@ -222,6 +274,10 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // subtitles writer->writeEndElement(); // audio + + writer->writeEndElement(); // export + + writer->writeEndDocument(); } Encoder* Encoder::CreateFromID(Type id, const EncodingParams& params) @@ -311,4 +367,131 @@ QMatrix4x4 EncodingParams::GenerateMatrix(EncodingParams::VideoScalingMethod met return preview_matrix; } +bool EncodingParams::LoadV1(QXmlStreamReader *reader) +{ + rational custom_range_in, custom_range_out; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("filename")) { + filename_ = reader->readElementText(); + } else if (reader->name() == QStringLiteral("format")) { + format_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("range")) { + has_custom_range_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("customrangein")) { + custom_range_in = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("customrangeout")) { + custom_range_out = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("video")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + video_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("codec")) { + video_codec_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("width")) { + video_params_.set_width(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("height")) { + video_params_.set_height(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("format")) { + video_params_.set_format(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("timebase")) { + video_params_.set_time_base(rational::fromString(reader->readElementText())); + } else if (reader->name() == QStringLiteral("divider")) { + video_params_.set_divider(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("bitrate")) { + video_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("minbitrate")) { + video_min_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("maxbitrate")) { + video_max_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("bufsize")) { + video_buffer_size_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("threads")) { + video_threads_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("pixfmt")) { + video_pix_fmt_ = reader->readElementText(); + } else if (reader->name() == QStringLiteral("imgseq")) { + video_is_image_sequence_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("color")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("output")) { + color_transform_ = reader->readElementText(); + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("vscale")) { + video_scaling_method_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("opts")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("entry")) { + QString key, value; + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("key")) { + key = reader->readElementText(); + } else if (reader->name() == QStringLiteral("value")) { + value = reader->readElementText(); + } else { + reader->skipCurrentElement(); + } + } + set_video_option(key, value); + } else { + reader->skipCurrentElement(); + } + } + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("audio")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + audio_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("codec")) { + audio_codec_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("samplerate")) { + audio_params_.set_sample_rate(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("channellayout")) { + audio_params_.set_channel_layout(reader->readElementText().toULongLong()); + } else if (reader->name() == QStringLiteral("format")) { + audio_params_.set_format(static_cast(reader->readElementText().toInt())); + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("subtitles")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + subtitles_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("sidecar")) { + subtitles_are_sidecar_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("sidecarformat")) { + subtitle_sidecar_fmt_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("codec")) { + subtitles_codec_ = static_cast(reader->readElementText().toInt()); + } else { + reader->skipCurrentElement(); + } + } + } else { + reader->skipCurrentElement(); + } + } + + return true; +} + } diff --git a/app/codec/encoder.h b/app/codec/encoder.h index a8496065e..e33f1de9a 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -52,6 +52,9 @@ public: EncodingParams(); + static QDir GetPresetPath(); + static QStringList GetListOfPresets(); + bool IsValid() const { return video_enabled_ || audio_enabled_ || subtitles_enabled_; @@ -113,6 +116,10 @@ public: const rational& GetExportLength() const { return export_length_; } void SetExportLength(const rational& export_length) { export_length_ = export_length; } + bool Load(QIODevice *device); + bool Load(QXmlStreamReader *reader); + + void Save(QIODevice *device) const; void Save(QXmlStreamWriter* writer) const; bool has_custom_range() const { return has_custom_range_; } @@ -133,6 +140,8 @@ public: private: static const int kEncoderParamsVersion = 1; + bool LoadV1(QXmlStreamReader *reader); + QString filename_; ExportFormat::Format format_; diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 2a309a6fc..8847c4ba4 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -26,6 +26,8 @@ set(OLIVE_SOURCES dialog/export/exportaudiotab.h dialog/export/exportformatcombobox.cpp dialog/export/exportformatcombobox.h + dialog/export/exportsavepresetdialog.cpp + dialog/export/exportsavepresetdialog.h dialog/export/exportsubtitlestab.cpp dialog/export/exportsubtitlestab.h dialog/export/exportvideotab.cpp diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index f9b0ec6a1..7f5a66a07 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -33,6 +33,7 @@ #include "common/digit.h" #include "common/qtutils.h" #include "dialog/task/task.h" +#include "exportsavepresetdialog.h" #include "node/project/project.h" #include "node/project/sequence/sequence.h" #include "task/taskmanager.h" @@ -81,21 +82,21 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : QLabel* preset_lbl = new QLabel(tr("Preset:")); preset_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(preset_lbl, row, 0); - QComboBox* preset_combobox = new QComboBox(); - preset_combobox->addItem(tr("Same As Source - High Quality")); - preset_combobox->addItem(tr("Same As Source - Medium Quality")); - preset_combobox->addItem(tr("Same As Source - Low Quality")); - preferences_layout->addWidget(preset_combobox, row, 1); + preset_combobox_ = new QComboBox(); + LoadPresets(); + connect(preset_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &ExportDialog::PresetComboBoxChanged); + preferences_layout->addWidget(preset_combobox_, row, 1, 1, 2); - QPushButton* preset_load_btn = new QPushButton(); + /*QPushButton* preset_load_btn = new QPushButton(); preset_load_btn->setIcon(icon::Open); preset_load_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - preferences_layout->addWidget(preset_load_btn, row, 2); + preferences_layout->addWidget(preset_load_btn, row, 2);*/ QPushButton* preset_save_btn = new QPushButton(); preset_save_btn->setIcon(icon::Save); preset_save_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(preset_save_btn, row, 3); + connect(preset_save_btn, &QPushButton::clicked, this, &ExportDialog::SavePreset); row++; @@ -197,25 +198,9 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : // Set defaults previously_selected_format_ = ExportFormat::kFormatMPEG4Video; - format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); connect(format_combobox_, &ExportFormatComboBox::FormatChanged, this, &ExportDialog::FormatChanged); - FormatChanged(format_combobox_->GetFormat()); VideoParams vp = viewer_node_->GetVideoParams(); - AudioParams ap = viewer_node_->GetAudioParams(); - - video_tab_->width_slider()->SetValue(vp.width()); - video_tab_->width_slider()->SetDefaultValue(vp.width()); - video_tab_->height_slider()->SetValue(vp.height()); - video_tab_->height_slider()->SetDefaultValue(vp.height()); - video_tab_->SetSelectedFrameRate(vp.frame_rate()); - video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); - video_tab_->pixel_format_field()->SetPixelFormat(static_cast(OLIVE_CONFIG("OnlinePixelFormat").toInt())); - video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); - audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); - audio_tab_->sample_format_combobox()->SetAttemptToRestoreFormat(false); - audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); - video_aspect_ratio_ = static_cast(vp.width()) / static_cast(vp.height()); connect(video_tab_->width_slider(), @@ -247,21 +232,22 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : this, &ExportDialog::ImageSequenceCheckBoxChanged); - // Set viewer to view the node - preview_viewer_->ConnectViewerNode(viewer_node_); - preview_viewer_->SetColorMenuEnabled(false); - preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); - // We don't check if the codec supports subtitles because we can always export to a sidecar file - bool has_subtitle_codecs = SequenceHasSubtitles(); + bool has_subtitle_tracks = SequenceHasSubtitles(); connect(subtitles_enabled_, &QCheckBox::toggled, subtitle_tab_, &QWidget::setEnabled); - subtitles_enabled_->setChecked(has_subtitle_codecs); - subtitles_enabled_->setEnabled(has_subtitle_codecs); + subtitles_enabled_->setEnabled(has_subtitle_tracks); // If the viewer already has cached params, use them if (viewer_node_->GetLastUsedEncodingParams().IsValid()) { SetParams(viewer_node_->GetLastUsedEncodingParams()); + } else { + SetDefaults(); } + + // Set viewer to view the node and set its colorspace + preview_viewer_->ConnectViewerNode(viewer_node_); + preview_viewer_->SetColorMenuEnabled(false); + preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); } rational ExportDialog::GetSelectedTimebase() const @@ -402,6 +388,29 @@ void ExportDialog::ImageSequenceCheckBoxChanged(bool e) filename_edit_->setText(current_fileinfo.dir().filePath(basename)); } +void ExportDialog::SavePreset() +{ + ExportSavePresetDialog d(GenerateParams(), this); + if (d.exec() == QDialog::Accepted) { + LoadPresets(); + preset_combobox_->setCurrentText(d.GetSelectedPresetName()); + } +} + +void ExportDialog::PresetComboBoxChanged() +{ + QComboBox *c = static_cast(sender()); + + int preset_number = c->currentData().toInt(); + if (preset_number == kPresetDefault) { + SetDefaults(); + } else if (preset_number == kPresetLastUsed) { + SetParams(viewer_node_->GetLastUsedEncodingParams()); + } else { + SetParams(presets_.at(preset_number)); + } +} + void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title) { QScrollArea* scroll_area = new QScrollArea(); @@ -494,7 +503,32 @@ void ExportDialog::ResolutionChanged() void ExportDialog::LoadPresets() { + preset_combobox_->clear(); + presets_.clear(); + preset_combobox_->addItem(tr("Default"), kPresetDefault); + + if (viewer_node_->GetLastUsedEncodingParams().IsValid()) { + preset_combobox_->addItem(tr("Last Used"), kPresetLastUsed); + } + + preset_combobox_->insertSeparator(preset_combobox_->count()); + + QStringList l = EncodingParams::GetListOfPresets(); + presets_.reserve(l.size()); + + for (const QString &preset : l) { + EncodingParams p; + + QFile f(EncodingParams::GetPresetPath().filePath(preset)); + if (f.open(QFile::ReadOnly)) { + if (p.Load(&f)) { + preset_combobox_->addItem(preset, int(presets_.size())); + presets_.push_back(p); + } + f.close(); + } + } } void ExportDialog::SetDefaultFilename() @@ -527,6 +561,28 @@ bool ExportDialog::SequenceHasSubtitles() const return false; } +void ExportDialog::SetDefaults() +{ + format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); + FormatChanged(format_combobox_->GetFormat()); + + VideoParams vp = viewer_node_->GetVideoParams(); + AudioParams ap = viewer_node_->GetAudioParams(); + + video_tab_->width_slider()->SetValue(vp.width()); + video_tab_->width_slider()->SetDefaultValue(vp.width()); + video_tab_->height_slider()->SetValue(vp.height()); + video_tab_->height_slider()->SetDefaultValue(vp.height()); + video_tab_->SetSelectedFrameRate(vp.frame_rate()); + video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + video_tab_->pixel_format_field()->SetPixelFormat(static_cast(OLIVE_CONFIG("OnlinePixelFormat").toInt())); + video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); + audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); + audio_tab_->sample_format_combobox()->SetAttemptToRestoreFormat(false); + audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); + subtitles_enabled_->setChecked(SequenceHasSubtitles()); +} + EncodingParams ExportDialog::GenerateParams() const { VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), @@ -604,7 +660,6 @@ EncodingParams ExportDialog::GenerateParams() const void ExportDialog::SetParams(const EncodingParams &e) { format_combobox_->SetFormat(e.format()); - filename_edit_->setText(e.filename()); if (e.has_custom_range() && viewer_node_->GetWorkArea()->enabled()) { range_combobox_->setCurrentIndex(kRangeInToOut); diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index abf242575..06674ed8d 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -69,6 +69,8 @@ private: bool SequenceHasSubtitles() const; + void SetDefaults(); + ViewerOutput* viewer_node_; ExportFormat::Format previously_selected_format_; @@ -81,9 +83,16 @@ private: kRangeInToOut }; + enum AutoPreset { + kPresetDefault = -1, + kPresetLastUsed = -2, + }; + QTabWidget* preferences_tabs_; + QComboBox* preset_combobox_; QComboBox* range_combobox_; + std::vector presets_; QCheckBox* video_enabled_; QCheckBox* audio_enabled_; @@ -119,6 +128,10 @@ private slots: void ImageSequenceCheckBoxChanged(bool e); + void SavePreset(); + + void PresetComboBoxChanged(); + }; } diff --git a/app/dialog/export/exportsavepresetdialog.cpp b/app/dialog/export/exportsavepresetdialog.cpp new file mode 100644 index 000000000..03052c4ec --- /dev/null +++ b/app/dialog/export/exportsavepresetdialog.cpp @@ -0,0 +1,97 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 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 "exportsavepresetdialog.h" + +#include +#include +#include +#include + +namespace olive { + +ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p, QWidget *parent) : + QDialog(parent), + params_(p) +{ + auto layout = new QVBoxLayout(this); + + name_edit_ = new QLineEdit(); + + // Populate existing list + QStringList l = EncodingParams::GetListOfPresets(); + if (!l.empty()) { + auto list_widget_ = new QListWidget(); + for (const QString &f : l) { + list_widget_->addItem(f); + } + connect(list_widget_, &QListWidget::currentTextChanged, name_edit_, &QLineEdit::setText); + layout->addWidget(list_widget_); + } + + auto name_layout = new QHBoxLayout(); + layout->addLayout(name_layout); + + name_layout->addWidget(new QLabel(tr("Name:"))); + + name_edit_->setFocus(); + name_layout->addWidget(name_edit_); + + auto btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(btns, &QDialogButtonBox::accepted, this, &ExportSavePresetDialog::accept); + connect(btns, &QDialogButtonBox::rejected, this, &ExportSavePresetDialog::reject); + layout->addWidget(btns); + + setWindowTitle(tr("Save Export Preset")); +} + +void ExportSavePresetDialog::accept() +{ + if (name_edit_->text().isEmpty()) { + QMessageBox::critical(this, tr("Invalid Name"), tr("You must enter a name to save an export preset.")); + return; + } + + QDir d(EncodingParams::GetPresetPath()); + if (!d.exists()) { + d.mkpath(QStringLiteral(".")); + } + + QFile f(d.filePath(name_edit_->text())); + if (f.exists()) { + if (QMessageBox::question(this, tr("Overwrite Preset"), tr("A preset with the name \"%1\" already exists. Do you wish to overwrite it?").arg(name_edit_->text()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return; + } + } + + if (!f.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Write Error"), tr("Failed to open file \"%1\" for writing.").arg(f.fileName())); + return; + } + + params_.Save(&f); + + f.close(); + + QDialog::accept(); +} + +} diff --git a/app/dialog/export/exportsavepresetdialog.h b/app/dialog/export/exportsavepresetdialog.h new file mode 100644 index 000000000..1f8ca09b9 --- /dev/null +++ b/app/dialog/export/exportsavepresetdialog.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 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 EXPORTSAVEPRESETDIALOG_H +#define EXPORTSAVEPRESETDIALOG_H + +#include +#include +#include + +#include "codec/encoder.h" + +namespace olive { + +class ExportSavePresetDialog : public QDialog +{ + Q_OBJECT +public: + ExportSavePresetDialog(const EncodingParams &p, QWidget *parent = nullptr); + + QString GetSelectedPresetName() const + { + return name_edit_->text(); + } + +public slots: + virtual void accept() override; + +private: + QLineEdit *name_edit_; + + EncodingParams params_; + +}; + +} + +#endif // EXPORTSAVEPRESETDIALOG_H