/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar 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 "export.h" #include #include #include #include #include #include #include #include #include #include "oakutil/digit.h" #include "oakutil/qtutils.h" #include "oakutil/oaknode.h" #include "dialog/msgbox.h" #include "dialog/task/task.h" #include "exportsavepresetdialog.h" #include "oakengine/color.h" #include "oakengine/events.h" #include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/viewer/vieweroutpututils.h" #include "oakengine/exporter.h" #include "oakengine/project.h" #include "oakengine/task.h" #include "oakengine/encoding.h" #include "oakengine/timeline.h" #include "oakengine/viewer.h" #include "ui/icons/icons.h" #include "widget/timeruler/timeruler.h" #include "common/configwrapper.h" namespace olive { #define super QDialog namespace { // pix_fmt string (e.g. "yuv420p") to its index in the codec's supported // list; 0 (the codec's preferred format) when absent. int pix_fmt_index(int codec, const QString &pix_fmt) { if (pix_fmt.isEmpty()) { return 0; } return oakengine_encoding_pix_fmt_index(codec, pix_fmt.toUtf8().constData()); } // ViewerOutput::get_work_area() snapshot through the C ABI. oakengine_viewer_workarea export_viewer_workarea(const OakEngineNode *viewer) { oakengine_viewer_workarea wa = {}; oakengine_viewer_get_workarea(viewer, &wa); return wa; } // ViewerOutput::get_length() as a Rational through the C ABI. Rational export_viewer_length(const OakEngineNode *viewer) { int64_t num = 0, den = 1; oakengine_viewer_get_length(viewer, &num, &den); return Rational(num, den); } // OakEngineEncodingParams (assembled by the dialog) -> facade POD. One-to-one with // oak_export_options_ex; see oakengine/exporter.h for the field docs. oak_export_options_ex params_to_ex(const OakEngineEncodingParams *p) { oak_export_options_ex o = {}; int64_t vbrate = 0, abrate = 0; int asample_rate = 0; uint64_t ach_layout = 0; int asample_fmt = 0; int vthreads = 0; int scaling = 0; int is_img_seq = 0; oak_video_params vp = {}; oakengine_encoding_params_get_video_params(p, &vp); vbrate = oakengine_encoding_params_video_bit_rate(p); abrate = oakengine_encoding_params_audio_bit_rate(p); vthreads = oakengine_encoding_params_video_threads(p); scaling = oakengine_encoding_params_video_scaling_method(p); is_img_seq = oakengine_encoding_params_video_is_image_sequence(p); if (oakengine_encoding_params_has_custom_range(p)) { o.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM; int64_t r_in_num = 0, r_in_den = 1, r_out_num = 0, r_out_den = 1; oakengine_encoding_params_get_custom_range( p, &r_in_num, &r_in_den, &r_out_num, &r_out_den); o.range_in_ts = Timecode::time_to_timestamp( Rational(r_in_num, r_in_den), Rational(vp.time_base_num, vp.time_base_den)); o.range_out_ts = Timecode::time_to_timestamp( Rational(r_out_num, r_out_den), Rational(vp.time_base_num, vp.time_base_den)); } else { o.range_mode = OAKENGINE_EXPORT_RANGE_ENTIRE; } o.format = oakengine_encoding_params_format(p); o.video_enabled = oakengine_encoding_params_video_enabled(p) ? 1 : 0; o.video_codec = oakengine_encoding_params_video_codec(p); o.audio_enabled = oakengine_encoding_params_audio_enabled(p) ? 1 : 0; o.audio_codec = oakengine_encoding_params_audio_codec(p); o.subtitles_enabled = oakengine_encoding_params_subtitles_enabled(p) ? 1 : 0; o.subtitles_sidecar = oakengine_encoding_params_subtitles_are_sidecar(p) ? 1 : 0; o.subtitles_format = oakengine_encoding_params_subtitles_are_sidecar(p) ? oakengine_encoding_params_subtitles_sidecar_format(p) : 0; o.subtitles_codec = oakengine_encoding_params_subtitles_enabled(p) ? oakengine_encoding_params_subtitles_codec(p) : 0; o.video_bit_rate = vbrate; o.audio_bit_rate = abrate; char pix_fmt_buf[64]; if (oakengine_encoding_params_video_pix_fmt( p, pix_fmt_buf, static_cast(sizeof(pix_fmt_buf))) > 0) { o.video_pix_fmt = oakengine_encoding_pix_fmt_index( o.video_codec, pix_fmt_buf); } else { o.video_pix_fmt = 0; } if (oakengine_encoding_params_get_audio_params( p, &asample_rate, &ach_layout, &asample_fmt) == OAKENGINE_OK) { o.audio_sample_rate = asample_rate; o.audio_channel_layout = ach_layout; o.audio_sample_format = asample_fmt; } char ct_buf[128]; const int ct_ret = oakengine_encoding_params_color_transform_output( p, ct_buf, static_cast(sizeof(ct_buf))); if (ct_ret <= 0 || ct_buf[0] == '\0') { o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE; } else { const QString ct = QString::fromUtf8(ct_buf); if (ct == QStringLiteral("sRGB OETF")) { o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF; } else if (ct == QStringLiteral("Rec.709 OETF")) { o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF; } else if (ct == QStringLiteral("BT.1886 EOTF")) { o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF; } else { o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM; snprintf(o.color_transform_name, sizeof(o.color_transform_name), "%s", ct_buf); } } o.video_width = vp.width; o.video_height = vp.height; o.frame_rate_num = vp.time_base_den; // time_base is frame duration, so rate = den/num o.frame_rate_den = vp.time_base_num; o.pixel_aspect_num = vp.pixel_aspect_num; o.pixel_aspect_den = vp.pixel_aspect_den; o.interlacing = vp.interlacing; o.pixel_format = vp.format; o.scaling_method = scaling; o.color_range = vp.color_range; o.video_threads = vthreads; o.is_image_sequence = is_img_seq; return o; } } // namespace ExportDialog::ExportDialog(OakEngineNode *viewer_node, bool stills_only_mode, QWidget *parent) : super(parent) , viewer_node_(viewer_node) , stills_only_mode_(stills_only_mode) , loading_presets_(false) { QHBoxLayout *layout = new QHBoxLayout(this); QSplitter *splitter = new QSplitter(Qt::Horizontal); splitter->setChildrenCollapsible(false); layout->addWidget(splitter); preferences_area_ = new QWidget(); QGridLayout *preferences_layout = new QGridLayout(preferences_area_); preferences_layout->setContentsMargins(0, 0, 0, 0); int row = 0; QLabel *fn_lbl = new QLabel(tr("Filename:")); fn_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(fn_lbl, row, 0); filename_edit_ = new QLineEdit(); preferences_layout->addWidget(filename_edit_, row, 1, 1, 2); QPushButton *file_browse_btn = new QPushButton(); file_browse_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); file_browse_btn->setIcon(icon::folder); file_browse_btn->setToolTip(tr("Browse for exported file filename")); connect(file_browse_btn, &QPushButton::clicked, this, &ExportDialog::browse_filename); preferences_layout->addWidget(file_browse_btn, row, 3); row++; QLabel *preset_lbl = new QLabel(tr("Preset:")); preset_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(preset_lbl, row, 0); preset_combobox_ = new QComboBox(); load_presets(); connect( preset_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &ExportDialog::preset_combo_box_changed); preferences_layout->addWidget(preset_combobox_, row, 1, 1, 2); /*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);*/ 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::save_preset); row++; preferences_layout->addWidget(QtUtils::create_horizontal_line(), row, 0, 1, 4); row++; preferences_layout->addWidget(new QLabel(tr("Range:")), row, 0); range_combobox_ = new QComboBox(); range_combobox_->addItem(tr("Entire Sequence")); range_combobox_->addItem(tr("In to Out")); range_combobox_->setEnabled(export_viewer_workarea(viewer_node_).enabled); preferences_layout->addWidget(range_combobox_, row, 1, 1, 3); row++; preferences_layout->addWidget(QtUtils::create_horizontal_line(), row, 0, 1, 4); row++; preferences_layout->addWidget(new QLabel(tr("Format:")), row, 0); format_combobox_ = new ExportFormatComboBox(); preferences_layout->addWidget(format_combobox_, row, 1, 1, 3); row++; QHBoxLayout *av_enabled_layout = new QHBoxLayout(); video_enabled_ = new QCheckBox(tr("Export Video")); av_enabled_layout->addWidget(video_enabled_); audio_enabled_ = new QCheckBox(tr("Export Audio")); av_enabled_layout->addWidget(audio_enabled_); subtitles_enabled_ = new QCheckBox(tr("Export Subtitles")); av_enabled_layout->addWidget(subtitles_enabled_); preferences_layout->addLayout(av_enabled_layout, row, 0, 1, 4); row++; preferences_tabs_ = new QTabWidget(); color_manager_ = oakengine_color_manager_from_project( oakengine_node_get_project(viewer_node_)); video_tab_ = new ExportVideoTab(color_manager_); add_preferences_tab(video_tab_, tr("Video")); // Set video tab time and make connections viewer_sub_ = oakengine_event_subscribe( viewer_node, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, [](const oakengine_event *event, void *userdata) { auto *dlg = static_cast(userdata); auto *tab = dlg->video_tab_; tab->set_time(Rational(event->a, event->b)); }, this); connect(video_tab_, &ExportVideoTab::time_changed, this, [viewer_node](const Rational &time) { oakengine_viewer_set_playhead( viewer_node, time.numerator(), time.denominator()); }); { int64_t pn, pd; oakengine_viewer_get_playhead(viewer_node, &pn, &pd); video_tab_->set_time(Rational(pn, pd)); } audio_tab_ = new ExportAudioTab(); add_preferences_tab(audio_tab_, tr("Audio")); subtitle_tab_ = new ExportSubtitlesTab(); add_preferences_tab(subtitle_tab_, tr("Subtitles")); preferences_layout->addWidget(preferences_tabs_, row, 0, 1, 4); row++; { QGroupBox *options_group = new QGroupBox(); preferences_layout->addWidget(options_group, row, 0, 1, 4); QGridLayout *options_layout = new QGridLayout(options_group); int opt_row = 0; export_bkg_box_ = new QCheckBox(tr("Run In Background")); export_bkg_box_->setToolTip(tr( "Exporting in the background allows you to continue using Oak Video Editor while " "exporting, but may result in slower export speeds, and may" "severely impact editing and playback performance.")); options_layout->addWidget(export_bkg_box_, opt_row, 0); import_file_after_export_ = new QCheckBox(tr("Import Result After Export")); options_layout->addWidget(import_file_after_export_, opt_row, 1); connect(export_bkg_box_, &QCheckBox::toggled, import_file_after_export_, [this](bool e) { import_file_after_export_->setEnabled(!e); }); } row++; QHBoxLayout *btn_layout = new QHBoxLayout(); btn_layout->setContentsMargins(0, 0, 0, 0); preferences_layout->addLayout(btn_layout, row, 0, 1, 4); btn_layout->addStretch(); QPushButton *export_btn = new QPushButton(tr("Export")); btn_layout->addWidget(export_btn); connect(export_btn, &QPushButton::clicked, this, &ExportDialog::start_export); QPushButton *cancel_btn = new QPushButton(tr("Cancel")); btn_layout->addWidget(cancel_btn); connect(cancel_btn, &QPushButton::clicked, this, &ExportDialog::reject); btn_layout->addStretch(); splitter->addWidget(preferences_area_); QWidget *preview_area = new QWidget(); QVBoxLayout *preview_layout = new QVBoxLayout(preview_area); preview_layout->addWidget(new QLabel(tr("Preview"))); preview_viewer_ = new ViewerWidget(); preview_viewer_->ruler()->set_marker_editing_enabled(false); preview_viewer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); preview_layout->addWidget(preview_viewer_); splitter->addWidget(preview_area); // Prioritize preview area splitter->setSizes({ 1, 99999 }); // Set default filename set_default_filename(); // Set defaults previously_selected_format_ = OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO; connect(format_combobox_, &ExportFormatComboBox::format_changed, this, &ExportDialog::format_changed); oak::VideoParams vp = viewer_output_video_params(viewer_node_); video_aspect_ratio_ = static_cast(vp.width()) / static_cast(vp.height()); connect(video_tab_->width_slider(), &IntegerSlider::value_changed, this, &ExportDialog::resolution_changed); connect(video_tab_->height_slider(), &IntegerSlider::value_changed, this, &ExportDialog::resolution_changed); connect( video_tab_->scaling_method_combobox(), static_cast(&QComboBox::currentIndexChanged), this, &ExportDialog::update_viewer_dimensions); connect(video_tab_->maintain_aspect_checkbox(), &QCheckBox::toggled, this, &ExportDialog::resolution_changed); connect(video_tab_, &ExportVideoTab::color_space_changed, preview_viewer_, static_cast( &ViewerWidget::set_color_transform)); connect(video_tab_, &ExportVideoTab::image_sequence_check_box_changed, this, &ExportDialog::image_sequence_check_box_changed); // We don't check if the codec supports subtitles because we can always export to a sidecar file bool has_subtitle_tracks = sequence_has_subtitles(); connect(subtitles_enabled_, &QCheckBox::toggled, subtitle_tab_, &QWidget::setEnabled); subtitles_enabled_->setEnabled(has_subtitle_tracks); // If the viewer already has cached params, use them if (!stills_only_mode_ && oakengine_encoding_params_get_last_used( reinterpret_cast(viewer_node_)) != nullptr) { // This will automatically set the param data QtUtils::set_combo_box_data(preset_combobox_, k_preset_last_used); } else { set_defaults(); } // Set viewer to view the node and set its colorspace preview_viewer_->connect_viewer_node(viewer_node_); preview_viewer_->set_color_menu_enabled(false); preview_viewer_->set_color_transform(video_tab_->current_ocio_color_space()); qApp->installEventFilter(this); connect(video_enabled_, &QCheckBox::toggled, video_tab_, &QWidget::setEnabled); video_tab_->setEnabled(video_enabled_->isChecked()); connect(audio_enabled_, &QCheckBox::toggled, audio_tab_, &QWidget::setEnabled); audio_tab_->setEnabled(audio_enabled_->isChecked()); connect(subtitles_enabled_, &QCheckBox::toggled, subtitle_tab_, &QWidget::setEnabled); subtitle_tab_->setEnabled(subtitles_enabled_->isChecked()); } ExportDialog::~ExportDialog() { // Raw C-API subscription carries `this` as userdata; not covered by // Qt's auto-disconnect. if (viewer_sub_ > 0) { oakengine_event_unsubscribe(viewer_sub_); viewer_sub_ = 0; } } Rational ExportDialog::get_selected_timebase() const { return video_tab_->get_selected_frame_rate().flipped(); } void ExportDialog::set_selected_timebase(const Rational &r) { video_tab_->set_selected_frame_rate(r.flipped()); } void ExportDialog::start_export() { if (!video_enabled_->isChecked() && !audio_enabled_->isChecked() && !subtitles_enabled_->isChecked()) { msg_box( this, QMessageBox::Critical, tr("Invalid parameters"), tr("Video, audio, and subtitles are disabled. There's nothing to export.")); return; } // Validate if the entered filename contains the correct extension (the extension is necessary // for both FFmpeg and OIIO to determine the output format) char ext_buf[64]; int ext_len = oakengine_encoding_format_extension(format_combobox_->get_format(), ext_buf, sizeof(ext_buf)); QString necessary_ext = QStringLiteral(".%1").arg(QString::fromUtf8(ext_buf, ext_len)); QString proposed_filename = filename_edit_->text().trimmed(); // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. if (!proposed_filename.endsWith(necessary_ext, Qt::CaseInsensitive)) { if (msg_box( this, QMessageBox::Warning, tr("Invalid filename"), tr("The filename must contain the extension \"%1\". Would you like to append it " "automatically?") .arg(necessary_ext), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { filename_edit_->setText(proposed_filename.append(necessary_ext)); } else { return; } } // Validate the intended path QFileInfo file_info(proposed_filename); QFileInfo dir_info(file_info.path()); // If the directory does not exist, try to create it QDir dest_dir(file_info.path()); if (!FileFunctions::directory_is_valid(dest_dir)) { msg_box( this, QMessageBox::Critical, tr("Failed to create output directory"), tr("The intended output directory doesn't exist and Oak Video Editor couldn't create it. " "Please choose a different filename.")); return; } // Validate if this is an image sequence and if the filename contains enough digits if (video_tab_->is_image_sequence_set()) { // Ensure filename contains digits if (!oakengine_encoding_filename_contains_digit_placeholder(proposed_filename.toUtf8().constData())) { msg_box( this, QMessageBox::Critical, tr("Invalid filename"), tr("Export is set to an image sequence, but the filename does not have a section for digits " "(formatted as [#####] where the amount of # is the amount of digits).")); return; } int64_t frame_count = get_export_length_in_timebase_units(); int64_t needed_digit_count = get_digit_count(frame_count); int current_digit_count = oakengine_encoding_image_sequence_digit_count(proposed_filename.toUtf8().constData()); if (current_digit_count < needed_digit_count) { msg_box( this, QMessageBox::Critical, tr("Invalid filename"), tr("Filename doesn't contain enough digits for the amount of frames " "this export will need (need %1 for %n frame(s)).", nullptr, frame_count) .arg(QString::number(needed_digit_count))); return; } } // Validate if the file exists and whether the user wishes to overwrite it if (file_info.exists()) { if (msg_box( this, QMessageBox::Warning, tr("Confirm Overwrite"), tr("The file \"%1\" already exists. Do you want to overwrite it?") .arg(proposed_filename), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { return; } } // Validate video resolution if (video_enabled_->isChecked() && (video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H264 || video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H265) && (video_tab_->width_slider()->get_value() % 2 != 0 || video_tab_->height_slider()->get_value() % 2 != 0)) { msg_box(this, QMessageBox::Critical, tr("Invalid Parameters"), tr("Width and height must be multiples of 2.")); return; } OakEngineTask *task = oakengine_task_create_export( reinterpret_cast(viewer_node_), generate_params()); if (export_bkg_box_->isChecked()) { // Send to TaskManager to export in background oakengine_task_manager_add(task); this->accept(); } else { // Use modal dialog box TaskDialog *td = new TaskDialog(task, tr("Export"), this); connect(td, &TaskDialog::task_succeeded, this, &ExportDialog::export_finished); td->open(); } } void ExportDialog::export_finished() { TaskDialog *td = static_cast(sender()); if (oakengine_task_is_cancelled(td->get_task())) { // If this task was cancelled, we stay open so the user can potentially queue another export } else { // Accept this dialog and close if (import_file_after_export_->isEnabled() && import_file_after_export_->isChecked()) { QString filename = filename_edit_->text().trimmed(); emit request_import_file(filename); } this->accept(); } } void ExportDialog::image_sequence_check_box_changed(bool e) { QFileInfo current_fileinfo(filename_edit_->text()); QString basename = current_fileinfo.completeBaseName(); QString suffix = current_fileinfo.suffix(); if (e) { if (!oakengine_encoding_filename_contains_digit_placeholder(basename.toUtf8().constData())) { basename.append(QStringLiteral("_[#####]")); } } else { char buf[1024]; oakengine_encoding_filename_remove_digit_placeholder( basename.toUtf8().constData(), buf, sizeof(buf)); basename = QString::fromUtf8(buf); } // Set filename if (!suffix.isEmpty()) { basename.append('.'); basename.append(suffix); } filename_edit_->setText(current_fileinfo.dir().filePath(basename)); } void ExportDialog::save_preset() { ExportSavePresetDialog d(generate_params(), this); if (d.exec() == QDialog::Accepted) { load_presets(); preset_combobox_->setCurrentText(d.get_selected_preset_name()); } } void ExportDialog::preset_combo_box_changed() { if (loading_presets_) { return; } QComboBox *c = static_cast(sender()); int preset_number = c->currentData().toInt(); if (preset_number == k_preset_default) { set_defaults(); } else if (preset_number == k_preset_last_used) { OakEngineEncodingParams *last = oakengine_encoding_params_get_last_used( reinterpret_cast(viewer_node_)); if (last) { set_params(last); } else { set_defaults(); } } else { set_params(presets_.at(preset_number)); } } void ExportDialog::add_preferences_tab(QWidget *inner_widget, const QString &title) { QScrollArea *scroll_area = new QScrollArea(); scroll_area->setWidgetResizable(true); scroll_area->setWidget(inner_widget); preferences_tabs_->addTab(scroll_area, title); } void ExportDialog::browse_filename() { int f = format_combobox_->get_format(); char name_buf[256]; char ext_buf[64]; oakengine_encoding_format_name(f, name_buf, sizeof(name_buf)); oakengine_encoding_format_extension(f, ext_buf, sizeof(ext_buf)); QString browsed_fn = QFileDialog::getSaveFileName( this, "", filename_edit_->text().trimmed(), QStringLiteral("%1 (*.%2)") .arg(QString::fromUtf8(name_buf), QString::fromUtf8(ext_buf)), nullptr, // We don't confirm overwrite here because we do it later QFileDialog::DontConfirmOverwrite); if (!browsed_fn.isEmpty()) { filename_edit_->setText(browsed_fn); } } void ExportDialog::format_changed(int current_format) { QString current_filename = filename_edit_->text().trimmed(); char ext_buf[64]; oakengine_encoding_format_extension(previously_selected_format_, ext_buf, sizeof(ext_buf)); QString previously_selected_ext = QString::fromUtf8(ext_buf); oakengine_encoding_format_extension(current_format, ext_buf, sizeof(ext_buf)); QString currently_selected_ext = QString::fromUtf8(ext_buf); // If the previous extension was added, remove it if (current_filename.endsWith(previously_selected_ext, Qt::CaseInsensitive)) { current_filename.resize(current_filename.size() - previously_selected_ext.size() - 1); } // Add the extension and set it current_filename.append('.'); current_filename.append(currently_selected_ext); filename_edit_->setText(current_filename); previously_selected_format_ = current_format; // Update video and audio comboboxes bool has_video_codecs = video_tab_->set_format(current_format); video_enabled_->setChecked(has_video_codecs); video_enabled_->setEnabled(has_video_codecs); bool has_audio_codecs = audio_tab_->set_format(current_format); audio_enabled_->setChecked(has_audio_codecs); audio_enabled_->setEnabled(has_audio_codecs); if (subtitles_enabled_->isEnabled()) { subtitle_tab_->set_format(current_format); } } void ExportDialog::resolution_changed() { if (video_tab_->maintain_aspect_checkbox()->isChecked()) { // Keep aspect ratio maintained if (sender() == video_tab_->height_slider()) { // Convert height to float double new_width = video_tab_->height_slider()->get_value(); // Generate width from aspect ratio new_width *= video_aspect_ratio_; // Align to even number and set video_tab_->width_slider()->set_value(new_width); } else { // Convert width to float double new_height = video_tab_->width_slider()->get_value(); // Generate height from aspect ratio new_height /= video_aspect_ratio_; // Align to even number and set video_tab_->height_slider()->set_value(new_height); } } update_viewer_dimensions(); } void ExportDialog::load_presets() { loading_presets_ = true; preset_combobox_->clear(); presets_.clear(); preset_combobox_->addItem(tr("Default"), k_preset_default); if (oakengine_encoding_params_get_last_used( reinterpret_cast(viewer_node_)) != nullptr) { preset_combobox_->addItem(tr("Last Used"), k_preset_last_used); } preset_combobox_->insertSeparator(preset_combobox_->count()); QStringList l; { const int n = oakengine_encoding_preset_count(); for (int i = 0; i < n; i++) { char name_buf[256]; if (oakengine_encoding_preset_name( i, name_buf, static_cast(sizeof(name_buf))) > 0) { l.append(QString::fromUtf8(name_buf)); } } } presets_.reserve(l.size()); for (const QString &preset : l) { OakEngineEncodingParams *p = oakengine_encoding_params_create(); char preset_path_buf[1024]; preset_path_buf[0] = '\0'; oakengine_encoding_preset_path( preset_path_buf, static_cast(sizeof(preset_path_buf))); const QByteArray preset_path_utf = QDir(QString::fromUtf8(preset_path_buf)) .filePath(preset) .toUtf8(); const int rc = oakengine_encoding_params_load_file( p, preset_path_utf.constData()); if (rc == OAKENGINE_OK) { preset_combobox_->addItem(preset, int(presets_.size())); presets_.push_back(p); } else { oakengine_encoding_params_destroy(p); } } loading_presets_ = false; } void ExportDialog::set_default_filename() { OakEngineProject *p = oakengine_node_get_project(viewer_node_); char fn_buf[512]; oakengine_project_filename(p, fn_buf, sizeof(fn_buf)); QDir doc_location; if (fn_buf[0] == '\0') { doc_location.setPath(QStandardPaths::writableLocation( QStandardPaths::DocumentsLocation)); } else { doc_location = QFileInfo(fn_buf).dir(); } QString file_location = doc_location.filePath( oak::Node(viewer_node_).get_label()); filename_edit_->setText(file_location); } bool ExportDialog::sequence_has_subtitles() const { OakEngineNode *node = viewer_node_; if (oakengine_node_is_sequence(node)) { OakEngineSequence *seq = reinterpret_cast(node); int track_count = 0; oakengine_sequence_track_count(seq, nullptr, nullptr, &track_count); for (int i = 0; i < track_count; i++) { OakEngineTrack *t = oakengine_sequence_track_at( seq, OAKENGINE_TRACK_TYPE_SUBTITLE, i); if (t && !oakengine_track_is_muted(seq, OAKENGINE_TRACK_TYPE_SUBTITLE, i) && oakengine_track_block_count(t) > 0) { return true; } } } return false; } void ExportDialog::set_defaults() { if (!stills_only_mode_) { format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO); } else { format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_PNG); } format_changed(format_combobox_->get_format()); oak::VideoParams vp = viewer_output_video_params(viewer_node_); AudioParams ap = viewer_output_audio_params(viewer_node_); video_tab_->width_slider()->set_value(vp.width()); video_tab_->width_slider()->SetDefaultValue(vp.width()); video_tab_->height_slider()->set_value(vp.height()); video_tab_->height_slider()->SetDefaultValue(vp.height()); video_tab_->set_selected_frame_rate(vp.frame_rate()); video_tab_->pixel_aspect_combobox()->set_pixel_aspect_ratio( vp.pixel_aspect_ratio()); video_tab_->pixel_format_field()->set_pixel_format( static_cast( OAK_CONFIG("OnlinePixelFormat").toInt())); video_tab_->interlaced_combobox()->set_interlace_mode(vp.interlacing()); audio_tab_->sample_rate_combobox()->set_sample_rate(ap.sample_rate()); audio_tab_->sample_format_combobox()->set_attempt_to_restore_format(false); audio_tab_->channel_layout_combobox()->set_channel_layout( ap.channel_layout()); subtitles_enabled_->setChecked(sequence_has_subtitles()); subtitle_tab_->set_sidecar_format(OAKENGINE_ENCODING_FORMAT_SRT); } OakEngineEncodingParams *ExportDialog::generate_params() const { OakEngineEncodingParams *params = oakengine_encoding_params_create(); oakengine_encoding_params_set_format( params, format_combobox_->get_format()); oakengine_encoding_params_set_filename( params, filename_edit_->text().trimmed().toUtf8().constData()); const Rational export_len = export_viewer_length(viewer_node_); oakengine_encoding_params_set_export_length( params, export_len.numerator(), export_len.denominator()); if (oakengine_encoding_codec_is_still_image(video_tab_->get_selected_codec()) && !video_tab_->is_image_sequence_set()) { // Exporting as image without exporting image sequence, only export one frame Rational export_time = video_tab_->get_still_image_time(); const Rational tb = get_selected_timebase(); oakengine_encoding_params_set_custom_range( params, export_time.numerator(), export_time.denominator(), (export_time + tb).numerator(), (export_time + tb).denominator()); } else if (range_combobox_->currentIndex() == k_range_in_to_out) { const oakengine_viewer_workarea wa = export_viewer_workarea(viewer_node_); oakengine_encoding_params_set_custom_range( params, wa.in_num, wa.in_den, wa.out_num, wa.out_den); } if (video_tab_->scaling_method_combobox()->isEnabled()) { oakengine_encoding_params_set_video_scaling_method( params, video_tab_->scaling_method_combobox()->currentData().toInt()); } if (video_enabled_->isChecked()) { const int video_codec = video_tab_->get_selected_codec(); // Build video params from the tab const int vw = static_cast(video_tab_->width_slider()->get_value()); const int vh = static_cast(video_tab_->height_slider()->get_value()); const Rational tb = get_selected_timebase(); const int pix_fmt = video_tab_->pixel_format_field()->get_pixel_format(); const int ch_count = oakengine_video_params_internal_channel_count(); const Rational par = video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio(); const int interlace = video_tab_->interlaced_combobox()->get_interlace_mode(); oak_video_params vp = {}; vp.width = vw; vp.height = vh; vp.time_base_num = tb.numerator(); vp.time_base_den = tb.denominator(); vp.format = pix_fmt; vp.pixel_aspect_num = par.numerator(); vp.pixel_aspect_den = par.denominator(); vp.interlacing = interlace; vp.color_range = video_tab_->color_range(); oakengine_encoding_params_enable_video(params, &vp, video_codec); oakengine_encoding_params_set_video_threads( params, video_tab_->threads()); if (video_tab_->isVisible()) { video_tab_->get_codec_section()->add_opts(params); } { const QString ct = video_tab_->current_ocio_color_space(); oakengine_encoding_params_set_color_transform( params, ct.isEmpty() ? nullptr : ct.toUtf8().constData()); } { const QString pix_fmt_name = video_tab_->pix_fmt(); oakengine_encoding_params_set_video_pix_fmt( params, pix_fmt_name.isEmpty() ? nullptr : pix_fmt_name.toUtf8().constData()); } oakengine_encoding_params_set_video_is_image_sequence( params, video_tab_->is_image_sequence_set() ? 1 : 0); } if (audio_enabled_->isChecked()) { const int audio_codec = audio_tab_->get_codec(); const int sample_rate = audio_tab_->sample_rate_combobox()->get_sample_rate(); const uint64_t ch_layout = audio_tab_->channel_layout_combobox()->get_channel_layout(); const int sample_fmt = audio_tab_->sample_format_combobox()->get_sample_format(); oakengine_encoding_params_enable_audio( params, sample_rate, ch_layout, sample_fmt, audio_codec); oakengine_encoding_params_set_audio_bit_rate( params, audio_tab_->bit_rate_slider()->get_value() * 1000); } if (subtitles_enabled_->isEnabled() && subtitles_enabled_->isChecked()) { if (!subtitle_tab_->get_sidecar_enabled()) { // Export subtitles embedded in container oakengine_encoding_params_enable_subtitles( params, subtitle_tab_->get_subtitle_codec()); } else { // Export subtitles to a sidecar file oakengine_encoding_params_enable_sidecar_subtitles( params, subtitle_tab_->get_sidecar_format(), subtitle_tab_->get_subtitle_codec()); } } return params; } void ExportDialog::set_params(const OakEngineEncodingParams *e) { format_combobox_->set_format(oakengine_encoding_params_format(e)); format_changed(format_combobox_->get_format()); if (oakengine_encoding_params_has_custom_range(e) && export_viewer_workarea(viewer_node_).enabled) { range_combobox_->setCurrentIndex(k_range_in_to_out); } QtUtils::set_combo_box_data(video_tab_->scaling_method_combobox(), oakengine_encoding_params_video_scaling_method(e)); const int video_enabled = oakengine_encoding_params_video_enabled(e); video_enabled_->setChecked(video_enabled); if (video_enabled) { oak_video_params vp = {}; oakengine_encoding_params_get_video_params(e, &vp); video_tab_->width_slider()->set_value(vp.width); video_tab_->height_slider()->set_value(vp.height); set_selected_timebase(Rational(vp.time_base_num, vp.time_base_den)); video_tab_->pixel_format_field()->set_pixel_format( static_cast(vp.format)); video_tab_->pixel_aspect_combobox()->set_pixel_aspect_ratio( Rational(vp.pixel_aspect_num, vp.pixel_aspect_den)); video_tab_->interlaced_combobox()->set_interlace_mode(vp.interlacing); video_tab_->set_selected_codec(oakengine_encoding_params_video_codec(e)); video_tab_->set_color_range(vp.color_range); video_tab_->set_threads(oakengine_encoding_params_video_threads(e)); if (video_tab_->isVisible()) { video_tab_->get_codec_section()->set_opts(e); } { char ct_buf[128]; if (oakengine_encoding_params_color_transform_output( e, ct_buf, static_cast(sizeof(ct_buf))) > 0) { video_tab_->set_ocio_color_space(QString::fromUtf8(ct_buf)); } else { video_tab_->set_ocio_color_space(QString()); } } { char pix_fmt_buf[64]; if (oakengine_encoding_params_video_pix_fmt( e, pix_fmt_buf, static_cast(sizeof(pix_fmt_buf))) > 0) { video_tab_->set_pix_fmt(QString::fromUtf8(pix_fmt_buf)); } else { video_tab_->set_pix_fmt(QString()); } } video_tab_->set_image_sequence( oakengine_encoding_params_video_is_image_sequence(e)); } const int audio_enabled = oakengine_encoding_params_audio_enabled(e); audio_enabled_->setChecked(audio_enabled); if (audio_enabled) { int asample_rate = 0; uint64_t ach_layout = 0; int asample_fmt = 0; oakengine_encoding_params_get_audio_params( e, &asample_rate, &ach_layout, &asample_fmt); audio_tab_->sample_rate_combobox()->set_sample_rate(asample_rate); audio_tab_->channel_layout_combobox()->set_channel_layout(ach_layout); audio_tab_->sample_format_combobox()->set_sample_format( static_cast(asample_fmt)); audio_tab_->set_codec(oakengine_encoding_params_audio_codec(e)); audio_tab_->bit_rate_slider()->set_value( oakengine_encoding_params_audio_bit_rate(e) / 1000); } if (subtitles_enabled_->isEnabled()) { const int subs_enabled = oakengine_encoding_params_subtitles_enabled(e); subtitles_enabled_->setChecked(subs_enabled); subtitle_tab_->set_sidecar_enabled( oakengine_encoding_params_subtitles_are_sidecar(e)); if (subs_enabled) { subtitle_tab_->set_subtitle_codec( oakengine_encoding_params_subtitles_codec(e)); if (oakengine_encoding_params_subtitles_are_sidecar(e)) { subtitle_tab_->set_sidecar_format( oakengine_encoding_params_subtitles_sidecar_format(e)); } } } } bool ExportDialog::eventFilter(QObject *o, QEvent *e) { // Any parameters in scrollable areas, ignore wheel events so the user doesn't unwittingly change // them while trying to scroll through the pages if (e->type() == QEvent::Wheel) { while ((o = o->parent())) { if (o == video_tab_ || o == audio_tab_ || o == subtitle_tab_) { e->ignore(); return true; } } } return super::eventFilter(o, e); } void ExportDialog::done(int r) { preview_viewer_->connect_viewer_node(nullptr); if (!stills_only_mode_) { OakEngineEncodingParams *p = generate_params(); oakengine_encoding_params_set_last_used( reinterpret_cast(viewer_node_), p); oakengine_encoding_params_destroy(p); } super::done(r); } Rational ExportDialog::get_export_length() const { if (range_combobox_->currentIndex() == k_range_in_to_out) { const oakengine_viewer_workarea wa = export_viewer_workarea(viewer_node_); return Rational(wa.out_num, wa.out_den) - Rational(wa.in_num, wa.in_den); } else { return export_viewer_length(viewer_node_); } } int64_t ExportDialog::get_export_length_in_timebase_units() const { return Timecode::time_to_timestamp(get_export_length(), get_selected_timebase()); } void ExportDialog::update_viewer_dimensions() { preview_viewer_->set_viewer_resolution( static_cast(video_tab_->width_slider()->get_value()), static_cast(video_tab_->height_slider()->get_value())); oak::VideoParams vp = viewer_output_video_params(viewer_node_); float mat16[16]; oakengine_encoding_generate_matrix( video_tab_->scaling_method_combobox()->currentData().toInt(), vp.width(), vp.height(), static_cast(video_tab_->width_slider()->get_value()), static_cast(video_tab_->height_slider()->get_value()), mat16); QMatrix4x4 transform(mat16); preview_viewer_->set_matrix(transform); } }