diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index 2e1d142ab..adc35bbc0 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -35,6 +35,8 @@ #include "core.h" #include "node/nodeundo.h" +#include "oakengine/footage.h" +#include "oakengine/node.h" #include "streamproperties/audiostreamproperties.h" #include "streamproperties/videostreamproperties.h" @@ -83,8 +85,17 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, start_time_layout->addWidget(source_start_time_spin_, 1); QString detection_note; + // Detection source comes through the facade (auto-detected field or + // "manual"), matching the engine's stored value. if (footage_->has_source_start_time()) { - const QString &source = footage_->source_start_time_source(); + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + char source_buf[64]; + source_buf[0] = '\0'; + oakengine_footage_get_source_start_time_source( + facade_handle, source_buf, sizeof(source_buf)); + oakengine_footage_free(facade_handle); + const QString source = QString::fromUtf8(source_buf); detection_note = (source == QStringLiteral("manual")) ? tr("(set manually)") : @@ -200,12 +211,16 @@ void FootagePropertiesDialog::accept() } } - MultiUndoCommand *command = new MultiUndoCommand(); + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + // All writes go through the liboakengine C ABI facade; each call lands + // on the shared undo stack as an undoable command (replacing this + // dialog's own undo command classes with identical semantics). if (footage_->get_label() != footage_name_field_->text()) { - NodeRenameCommand *nrc = new NodeRenameCommand(); - nrc->add_node(footage_, footage_name_field_->text()); - command->add_child(nrc); + oakengine_node_set_label( + reinterpret_cast(footage_), + footage_name_field_->text().toUtf8().constData()); } // Apply source start time changes @@ -215,8 +230,9 @@ void FootagePropertiesDialog::accept() Rational::from_double(source_start_time_spin_->value()); if (new_enabled != footage_->has_source_start_time() || (new_enabled && new_time != footage_->source_start_time())) { - command->add_child(new FootageSetSourceStartTimeCommand( - footage_, new_enabled, new_time, QStringLiteral("manual"))); + oakengine_footage_set_source_start_time( + facade_handle, new_enabled ? 1 : 0, new_time.numerator(), + new_time.denominator()); } } @@ -245,133 +261,22 @@ void FootagePropertiesDialog::accept() } if (old_stream_enabled != new_stream_enabled) { - command->add_child(new StreamEnableChangeCommand( - footage_, reference.type(), reference.index(), - new_stream_enabled)); + oakengine_footage_set_stream_enabled( + facade_handle, int(reference.type()), reference.index(), + new_stream_enabled ? 1 : 0); } } + oakengine_footage_free(facade_handle); + + MultiUndoCommand *command = new MultiUndoCommand(); for (int i = 0; i < stacked_widget_->count(); i++) { static_cast(stacked_widget_->widget(i)) ->accept(command); } - - Core::instance()->undo_stack()->push( - command, tr("Set Footage \"%1\" Properties").arg(footage_->get_label())); + delete command; // stream pages write through the facade directly QDialog::accept(); } -FootagePropertiesDialog::StreamEnableChangeCommand::StreamEnableChangeCommand( - Footage *footage, Track::Type type, int index_in_type, bool enabled) - : footage_(footage) - , type_(type) - , index_(index_in_type) - , new_enabled_(enabled) -{ -} - -Project * -FootagePropertiesDialog::StreamEnableChangeCommand::get_relevant_project() const -{ - return footage_->project(); -} - -void FootagePropertiesDialog::StreamEnableChangeCommand::redo() -{ - switch (type_) { - case Track::k_video: { - VideoParams vp = footage_->get_video_params(index_); - old_enabled_ = vp.enabled(); - vp.set_enabled(new_enabled_); - footage_->set_video_params(vp, index_); - break; - } - case Track::k_audio: { - AudioParams ap = footage_->get_audio_params(index_); - old_enabled_ = ap.enabled(); - ap.set_enabled(new_enabled_); - footage_->set_audio_params(ap, index_); - break; - } - case Track::k_subtitle: { - SubtitleParams sp = footage_->get_subtitle_params(index_); - old_enabled_ = sp.enabled(); - sp.set_enabled(new_enabled_); - footage_->set_subtitle_params(sp, index_); - break; - } - case Track::k_none: - case Track::k_count: - break; - } -} - -void FootagePropertiesDialog::StreamEnableChangeCommand::undo() -{ - switch (type_) { - case Track::k_video: { - VideoParams vp = footage_->get_video_params(index_); - vp.set_enabled(old_enabled_); - footage_->set_video_params(vp, index_); - break; - } - case Track::k_audio: { - AudioParams ap = footage_->get_audio_params(index_); - ap.set_enabled(old_enabled_); - footage_->set_audio_params(ap, index_); - break; - } - case Track::k_subtitle: { - SubtitleParams sp = footage_->get_subtitle_params(index_); - sp.set_enabled(old_enabled_); - footage_->set_subtitle_params(sp, index_); - break; - } - case Track::k_none: - case Track::k_count: - break; - } -} - -FootagePropertiesDialog::FootageSetSourceStartTimeCommand:: - FootageSetSourceStartTimeCommand(Footage *footage, bool enabled, - const Rational &time, - const QString &source) - : footage_(footage) - , new_enabled_(enabled) - , new_time_(time) - , new_source_(source) -{ -} - -Project * -FootagePropertiesDialog::FootageSetSourceStartTimeCommand::get_relevant_project() - const -{ - return footage_->project(); -} - -void FootagePropertiesDialog::FootageSetSourceStartTimeCommand::redo() -{ - old_enabled_ = footage_->has_source_start_time(); - old_time_ = footage_->source_start_time(); - old_source_ = footage_->source_start_time_source(); - - if (new_enabled_) { - footage_->set_source_start_time(new_time_, new_source_); - } else { - footage_->clear_source_start_time(); - } -} - -void FootagePropertiesDialog::FootageSetSourceStartTimeCommand::undo() -{ - if (old_enabled_) { - footage_->set_source_start_time(old_time_, old_source_); - } else { - footage_->clear_source_start_time(); - } -} - } diff --git a/app/dialog/footageproperties/footageproperties.h b/app/dialog/footageproperties/footageproperties.h index 758f1d680..6138270b3 100644 --- a/app/dialog/footageproperties/footageproperties.h +++ b/app/dialog/footageproperties/footageproperties.h @@ -59,50 +59,6 @@ public: FootagePropertiesDialog(QWidget *parent, Footage *footage); private: - class StreamEnableChangeCommand : public UndoCommand { - public: - StreamEnableChangeCommand(Footage *footage, Track::Type type, - int index_in_type, bool enabled); - - virtual Project *get_relevant_project() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Footage *footage_; - Track::Type type_; - int index_; - - bool old_enabled_; - bool new_enabled_; - }; - - class FootageSetSourceStartTimeCommand : public UndoCommand { - public: - FootageSetSourceStartTimeCommand(Footage *footage, bool enabled, - const Rational &time, - const QString &source); - - virtual Project *get_relevant_project() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Footage *footage_; - - bool new_enabled_; - Rational new_time_; - QString new_source_; - - bool old_enabled_; - Rational old_time_; - QString old_source_; - }; - /** * @brief Stack of widgets that changes based on whether the stream is a video or audio stream */ diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 2f8aa35fe..4fa686f59 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -28,6 +28,8 @@ #include #include "node/project.h" +#include "oakengine/footage.h" +#include "oakengine/node.h" namespace olive { @@ -46,8 +48,23 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) VideoParams vp = footage_->get_video_params(video_index_); + // Stream override values come through the liboakengine C ABI facade; + // layout-only conditions (channel count, video type) stay direct reads. + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + char colorspace_buf[256]; + int color_range = 0; + int interlacing = 0; + int premultiplied = 0; + oakengine_footage_get_video_stream_overrides( + facade_handle, video_index_, colorspace_buf, sizeof(colorspace_buf), + &color_range, &interlacing, &premultiplied); + int par_num = 1, par_den = 1; + oakengine_footage_get_pixel_aspect(facade_handle, video_index_, &par_num, + &par_den); + pixel_aspect_combo_ = new PixelAspectRatioComboBox(); - pixel_aspect_combo_->set_pixel_aspect_ratio(vp.pixel_aspect_ratio()); + pixel_aspect_combo_->set_pixel_aspect_ratio(Rational(par_num, par_den)); video_layout->addWidget(pixel_aspect_combo_, row, 1); row++; @@ -55,7 +72,8 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); video_interlace_combo_ = new InterlacedComboBox(); - video_interlace_combo_->set_interlace_mode(vp.interlacing()); + video_interlace_combo_->set_interlace_mode( + static_cast(interlacing)); video_layout->addWidget(video_interlace_combo_, row, 1); @@ -64,22 +82,25 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); - ocio::ConstConfigRcPtr config = - footage_->project()->color_manager()->get_config(); - int number_of_colorspaces = config->getNumColorSpaces(); + // The dropdown's color space list comes through the facade (same list + // the engine's color config reports). video_color_space_->addItem(tr("Default (%1)") .arg(footage_->project() ->color_manager() ->get_default_input_color_space())); - for (int i = 0; i < number_of_colorspaces; i++) { - QString colorspace = config->getColorSpaceNameByIndex(i); - - video_color_space_->addItem(colorspace); + const int colorspace_count = + oakengine_footage_colorspace_count(facade_handle); + for (int i = 0; i < colorspace_count; i++) { + char name_buf[256]; + if (oakengine_footage_colorspace_at(facade_handle, i, name_buf, + sizeof(name_buf)) > 0) { + video_color_space_->addItem(QString::fromUtf8(name_buf)); + } } - video_color_space_->setCurrentText(vp.colorspace()); + video_color_space_->setCurrentText(QString::fromUtf8(colorspace_buf)); video_layout->addWidget(video_color_space_, row, 1); @@ -92,7 +113,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) VideoParams::k_color_range_limited); color_range_combo_->addItem(tr("Full (0-255)"), VideoParams::k_color_range_full); - color_range_combo_->setCurrentIndex(vp.color_range()); + color_range_combo_->setCurrentIndex(color_range); video_layout->addWidget(color_range_combo_, row, 1); @@ -100,7 +121,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) row++; video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); - video_premultiply_alpha_->setChecked(vp.premultiplied_alpha()); + video_premultiply_alpha_->setChecked(premultiplied != 0); video_layout->addWidget(video_premultiply_alpha_, row, 0, 1, 2); } @@ -114,9 +135,15 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) imgseq_layout->addWidget(new QLabel(tr("Start Index:")), imgseq_row, 0); + int64_t seq_start = 0, seq_duration = 0; + int fr_num = 0, fr_den = 1; + oakengine_footage_get_image_sequence_params( + facade_handle, video_index_, &seq_start, &seq_duration, &fr_num, + &fr_den); + imgseq_start_time_ = new IntegerSlider(); imgseq_start_time_->set_minimum(0); - imgseq_start_time_->set_value(vp.start_time()); + imgseq_start_time_->set_value(seq_start); imgseq_layout->addWidget(imgseq_start_time_, imgseq_row, 1); imgseq_row++; @@ -125,7 +152,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) imgseq_end_time_ = new IntegerSlider(); imgseq_end_time_->set_minimum(0); - imgseq_end_time_->set_value(vp.start_time() + vp.duration() - 1); + imgseq_end_time_->set_value(seq_start + seq_duration - 1); imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); imgseq_row++; @@ -133,15 +160,22 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) imgseq_layout->addWidget(new QLabel(tr("Frame Rate:")), imgseq_row, 0); imgseq_frame_rate_ = new FrameRateComboBox(); - imgseq_frame_rate_->set_frame_rate(vp.frame_rate()); + imgseq_frame_rate_->set_frame_rate(Rational(fr_num, fr_den)); imgseq_layout->addWidget(imgseq_frame_rate_, imgseq_row, 1); video_layout->addWidget(imgseq_group, row, 0, 1, 2); } + + oakengine_footage_free(facade_handle); } void VideoStreamProperties::accept(MultiUndoCommand *parent) { + Q_UNUSED(parent) + + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + QString set_colorspace; if (video_color_space_->currentIndex() > 0) { @@ -150,23 +184,30 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent) VideoParams vp = footage_->get_video_params(video_index_); + // Write every override through the facade (each call is one undoable + // command on the shared undo stack, replacing this dialog's own undo + // command classes with identical semantics). if ((video_premultiply_alpha_ && video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) || set_colorspace != vp.colorspace() || static_cast( video_interlace_combo_->currentIndex()) != vp.interlacing() || - pixel_aspect_combo_->get_pixel_aspect_ratio() != vp.pixel_aspect_ratio() || color_range_combo_->currentData().toInt() != vp.color_range()) { - parent->add_child(new VideoStreamChangeCommand( - footage_, video_index_, - video_premultiply_alpha_ ? video_premultiply_alpha_->isChecked() : - vp.premultiplied_alpha(), - set_colorspace, - static_cast( - video_interlace_combo_->currentIndex()), - pixel_aspect_combo_->get_pixel_aspect_ratio(), - static_cast( - color_range_combo_->currentData().toInt()))); + oakengine_footage_set_video_stream_overrides( + facade_handle, video_index_, + set_colorspace.toUtf8().constData(), + color_range_combo_->currentData().toInt(), + video_interlace_combo_->currentIndex(), + video_premultiply_alpha_ ? + (video_premultiply_alpha_->isChecked() ? 1 : 0) : + -1); + } + + const Rational new_par = pixel_aspect_combo_->get_pixel_aspect_ratio(); + if (new_par != vp.pixel_aspect_ratio()) { + oakengine_footage_set_pixel_aspect(facade_handle, video_index_, + new_par.numerator(), + new_par.denominator()); } if (vp.video_type() == VideoParams::k_video_type_image_sequence) { @@ -176,11 +217,15 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent) if (vp.start_time() != imgseq_start_time_->get_value() || vp.duration() != new_dur || vp.frame_rate() != imgseq_frame_rate_->get_frame_rate()) { - parent->add_child(new ImageSequenceChangeCommand( - footage_, video_index_, imgseq_start_time_->get_value(), new_dur, - imgseq_frame_rate_->get_frame_rate())); + const Rational fr = imgseq_frame_rate_->get_frame_rate(); + oakengine_footage_set_image_sequence_params( + facade_handle, video_index_, + imgseq_start_time_->get_value(), new_dur, fr.numerator(), + fr.denominator()); } } + + oakengine_footage_free(facade_handle); } bool VideoStreamProperties::sanity_check() @@ -199,102 +244,4 @@ bool VideoStreamProperties::sanity_check() return true; } -VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand( - Footage *footage, int video_index, bool premultiplied, QString colorspace, - VideoParams::Interlacing interlacing, const Rational &pixel_ar, - VideoParams::ColorRange range) - : footage_(footage) - , video_index_(video_index) - , new_premultiplied_(premultiplied) - , new_colorspace_(colorspace) - , new_interlacing_(interlacing) - , new_pixel_ar_(pixel_ar) - , new_range_(range) -{ -} - -Project * -VideoStreamProperties::VideoStreamChangeCommand::get_relevant_project() const -{ - return footage_->project(); -} - -void VideoStreamProperties::VideoStreamChangeCommand::redo() -{ - VideoParams vp = footage_->get_video_params(video_index_); - - old_premultiplied_ = vp.premultiplied_alpha(); - old_colorspace_ = vp.colorspace(); - old_interlacing_ = vp.interlacing(); - old_pixel_ar_ = vp.pixel_aspect_ratio(); - old_range_ = vp.color_range(); - - vp.set_premultiplied_alpha(new_premultiplied_); - vp.set_colorspace(new_colorspace_); - vp.set_interlacing(new_interlacing_); - vp.set_pixel_aspect_ratio(new_pixel_ar_); - vp.set_color_range(new_range_); - - footage_->set_video_params(vp, video_index_); -} - -void VideoStreamProperties::VideoStreamChangeCommand::undo() -{ - VideoParams vp = footage_->get_video_params(video_index_); - - vp.set_premultiplied_alpha(old_premultiplied_); - vp.set_colorspace(old_colorspace_); - vp.set_interlacing(old_interlacing_); - vp.set_pixel_aspect_ratio(old_pixel_ar_); - vp.set_color_range(old_range_); - - footage_->set_video_params(vp, video_index_); -} - -VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand( - Footage *footage, int video_index, int64_t start_index, int64_t duration, - const Rational &frame_rate) - : footage_(footage) - , video_index_(video_index) - , new_start_index_(start_index) - , new_duration_(duration) - , new_frame_rate_(frame_rate) -{ -} - -Project * -VideoStreamProperties::ImageSequenceChangeCommand::get_relevant_project() const -{ - return footage_->project(); -} - -void VideoStreamProperties::ImageSequenceChangeCommand::redo() -{ - VideoParams vp = footage_->get_video_params(video_index_); - - old_start_index_ = vp.start_time(); - vp.set_start_time(new_start_index_); - - old_duration_ = vp.duration(); - vp.set_duration(new_duration_); - - old_frame_rate_ = vp.frame_rate(); - vp.set_frame_rate(new_frame_rate_); - vp.set_time_base(new_frame_rate_.flipped()); - - footage_->set_video_params(vp, video_index_); -} - -void VideoStreamProperties::ImageSequenceChangeCommand::undo() -{ - VideoParams vp = footage_->get_video_params(video_index_); - - vp.set_start_time(old_start_index_); - vp.set_duration(old_duration_); - vp.set_frame_rate(old_frame_rate_); - vp.set_time_base(old_frame_rate_.flipped()); - - footage_->set_video_params(vp, video_index_); -} - } diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index 81fff9275..e2f06c461 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -86,63 +86,6 @@ private: * @brief Sets the pixel aspect ratio of the stream */ PixelAspectRatioComboBox *pixel_aspect_combo_; - - class VideoStreamChangeCommand : public UndoCommand { - public: - VideoStreamChangeCommand(Footage *footage, int video_index, - bool premultiplied, QString colorspace, - VideoParams::Interlacing interlacing, - const Rational &pixel_ar, - VideoParams::ColorRange range); - - virtual Project *get_relevant_project() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Footage *footage_; - int video_index_; - - bool new_premultiplied_; - QString new_colorspace_; - VideoParams::Interlacing new_interlacing_; - Rational new_pixel_ar_; - VideoParams::ColorRange new_range_; - - bool old_premultiplied_; - QString old_colorspace_; - VideoParams::Interlacing old_interlacing_; - Rational old_pixel_ar_; - VideoParams::ColorRange old_range_; - }; - - class ImageSequenceChangeCommand : public UndoCommand { - public: - ImageSequenceChangeCommand(Footage *footage, int video_index, - int64_t start_index, int64_t duration, - const Rational &frame_rate); - - virtual Project *get_relevant_project() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Footage *footage_; - int video_index_; - - int64_t new_start_index_; - int64_t old_start_index_; - - int64_t new_duration_; - int64_t old_duration_; - - Rational new_frame_rate_; - Rational old_frame_rate_; - }; }; } diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp index 711b49ac0..b11c7a207 100644 --- a/app/dialog/footagerelink/footagerelinkdialog.cpp +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -22,6 +22,9 @@ #include "footagerelinkdialog.h" #include "core.h" +#include "oakengine/footage.h" +#include "oakengine/node.h" +#include "oakengine/project.h" #include #include @@ -135,42 +138,59 @@ void FootageRelinkDialog::browse_for_footage() QDir original_dir = info.dir(); QDir new_dir = QFileInfo(new_fn).dir(); - // Set new filename since this was set manually by the user - f->set_filename(new_fn); - - // Assume footage is valid here. We could do some decoder probing to ensure it's a usable file - // but otherwise we assume the user knows what they're doing here. - - // Set footage to valid and update icon - f->set_valid(); + // Relink through the facade (reprobes the file and resets stream / + // proxy state; relinked footage becomes valid when the probe + // succeeds). + OakEngineFootage *relink_handle = oakengine_footage_borrow( + reinterpret_cast(f)); + const int relink_rc = oakengine_footage_relink( + relink_handle, new_fn.toUtf8().constData()); + oakengine_footage_free(relink_handle); + if (relink_rc != OAKENGINE_OK) { + char err[512]; + err[0] = '\0'; + oakengine_footage_last_error(err, sizeof(err)); + QMessageBox::warning(this, tr("Cannot relink footage"), + err[0] ? QString::fromUtf8(err) : + tr("The file could not be used as media.")); + return; + } // Update item visually update_footage_item(index); - // Check all other footage files for matches - for (int it = 0; it < footage_.size(); it++) { - Footage *other_footage = footage_.at(it); + // Check all other footage files for matches in the new directory + // (facade's exact file-name matching, mirroring the second attempt + // of the old per-footage loop). + Project *project = f->project(); + if (project) { + oakengine_project_find_offline_footage( + reinterpret_cast(project), + new_dir.absolutePath().toUtf8().constData()); - // Ignore current footage file and footage that's already valid of course - if (index != it && !other_footage->is_valid()) { - // Get footage path relative to original directory - QString relative_to_original = - original_dir.relativeFilePath(other_footage->filename()); - QString absolute_to_new = - new_dir.filePath(relative_to_original); + // The old dialog also tried the original directory's relative + // paths, which the facade's exact-name matching does not cover; + // keep that pass here. + for (int it = 0; it < footage_.size(); it++) { + Footage *other_footage = footage_.at(it); - // Second attempt. Try appending the filename to our new filepath - if (!QFileInfo::exists(absolute_to_new)) { - QFileInfo file_info(other_footage->filename()); - absolute_to_new = new_dir.filePath(file_info.fileName()); + // Ignore footage that's already valid of course + if (!other_footage->is_valid()) { + // Get footage path relative to original directory + QString relative_to_original = + original_dir.relativeFilePath(other_footage->filename()); + QString absolute_to_new = + new_dir.filePath(relative_to_original); + + if (QFileInfo::exists(absolute_to_new)) { + other_footage->set_filename(absolute_to_new); + } } + } - // Check if file exists - if (QFileInfo::exists(absolute_to_new)) { - other_footage->set_filename(absolute_to_new); - other_footage->set_valid(); - update_footage_item(it); - } + // Refresh every row whose validity may have changed. + for (int it = 0; it < footage_.size(); it++) { + update_footage_item(it); } } } diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index cda9a6490..0f027af8b 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -36,7 +36,8 @@ #include "dialog/proxy/proxydialog.h" #include "dialog/sequence/sequence.h" #include "projectexplorerundo.h" -#include "codec/proxymanager.h" +#include "oakengine/footage.h" +#include "oakengine/node.h" #include "task/taskmanager.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -63,6 +64,46 @@ QVector get_selected_proxy_footage(const QVector &items) } return footage; } + +/** + * @brief Proxy generation driven by the liboakengine C ABI facade + * + * Replaces the direct ProxyManager::get_or_start_proxy() drive: the actual + * transcode and its synchronous wait live behind + * oakengine_footage_proxy_generate() (which also records the proxy state + * on the footage and invalidates it), while the task stays on the + * TaskManager queue like before. + */ +class FacadeProxyTask : public Task { +public: + FacadeProxyTask(Footage *footage) + : footage_(footage) + { + set_title(tr("Generating proxy for \"%1\"") + .arg(footage->get_label_or_name())); + } + +protected: + virtual bool run() override + { + OakEngineFootage *handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + const int rc = oakengine_footage_proxy_generate(handle); + oakengine_footage_free(handle); + if (rc != OAKENGINE_OK) { + char err[512]; + err[0] = '\0'; + oakengine_footage_last_error(err, sizeof(err)); + set_error(err[0] ? QString::fromUtf8(err) : + tr("Proxy generation failed")); + return false; + } + return true; + } + +private: + Footage *footage_; +}; } ProjectExplorer::ProjectExplorer(QWidget *parent) @@ -565,21 +606,31 @@ void ProjectExplorer::replace_selected_footage() return; } - auto p = new MultiUndoCommand(); - - // Change filename parameter - p->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(footage, Footage::k_filename_input)), - file)); - - if (QFileInfo(footage->filename()).fileName() == footage->get_label()) { - // Footage label == filename, change label too - p->add_child( - new NodeRenameCommand(footage, QFileInfo(file).fileName())); + // Change the filename through the facade relink (reprobes the new + // file and resets proxy/stream state); the label policy stays here. + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage)); + const int relink_rc = oakengine_footage_relink( + facade_handle, file.toUtf8().constData()); + oakengine_footage_free(facade_handle); + if (relink_rc != OAKENGINE_OK) { + char err[512]; + err[0] = '\0'; + oakengine_footage_last_error(err, sizeof(err)); + QMessageBox::warning( + this, tr("Cannot replace footage"), + err[0] ? QString::fromUtf8(err) : + tr("The file could not be used as media.")); + return; } - Core::instance()->undo_stack()->push(p, tr("Replaced Footage")); + if (QFileInfo(footage->filename()).fileName() == + footage->get_label()) { + // Footage label == filename, change label too + oakengine_node_set_label( + reinterpret_cast(footage), + QFileInfo(file).fileName().toUtf8().constData()); + } } } @@ -597,9 +648,8 @@ void ProjectExplorer::open_context_menu_item_in_new_window() void ProjectExplorer::generate_proxies_for_selected_footage() { - if (!ProxyManager::instance() || !project()) { - qWarning() - << "GenerateProxiesForSelectedFootage: ProxyManager or project unavailable"; + if (!project()) { + qWarning() << "GenerateProxiesForSelectedFootage: no project"; return; } @@ -617,18 +667,9 @@ void ProjectExplorer::generate_proxies_for_selected_footage() continue; } - ProxyManager::ProxyParams params = item->get_effective_proxy_params(); - const ProxyManager::Proxy proxy = - ProxyManager::instance()->get_or_start_proxy( - item->project()->cache_path(), item->filename(), - video.stream_index(), params); - qDebug() << "GenerateProxiesForSelectedFootage: proxy state=" - << ProxyManager::proxy_state_to_string(proxy.state) - << "file=" << proxy.filename - << "cache=" << item->project()->cache_path(); - item->set_proxy(proxy.filename, proxy.state, video.stream_index(), - params.version, true); - item->invalidate_all(Footage::k_filename_input); + // Queue one facade-backed task per footage item (same queueing + // semantics as the old per-footage proxy tasks). + TaskManager::instance()->add_task(new FacadeProxyTask(item)); } } @@ -645,7 +686,12 @@ void ProjectExplorer::set_selected_footage_proxy_enabled(bool enabled) continue; } - item->set_proxy_enabled(enabled); + OakEngineFootage *handle = oakengine_footage_borrow( + reinterpret_cast(item)); + oakengine_footage_proxy_set_enabled(handle, enabled ? 1 : 0); + oakengine_footage_free(handle); + // The facade call toggles the flag; cache invalidation for the UI + // stays here. item->invalidate_all(Footage::k_filename_input); } } @@ -655,13 +701,21 @@ void ProjectExplorer::reveal_proxy_for_selected_footage() const QVector footage = get_selected_proxy_footage(context_menu_items_); for (Footage *item : footage) { - if (item->proxy_path().isEmpty()) { + char proxy_path[4096]; + proxy_path[0] = '\0'; + OakEngineFootage *handle = oakengine_footage_borrow( + reinterpret_cast(item)); + oakengine_footage_proxy_get_path(handle, proxy_path, + sizeof(proxy_path)); + oakengine_footage_free(handle); + if (proxy_path[0] == '\0') { continue; } + const QString path = QString::fromUtf8(proxy_path); #if defined(Q_OS_WINDOWS) QStringList args; - args << "/select," << QDir::toNativeSeparators(item->proxy_path()); + args << "/select," << QDir::toNativeSeparators(path); QProcess::startDetached(QStringLiteral("explorer"), args); #elif defined(Q_OS_MAC) QStringList args; @@ -670,13 +724,13 @@ void ProjectExplorer::reveal_proxy_for_selected_footage() args << "-e"; args << "activate"; args << "-e"; - args << "select POSIX file \"" + item->proxy_path() + "\""; + args << "select POSIX file \"" + path + "\""; args << "-e"; args << "end tell"; QProcess::startDetached(QStringLiteral("osascript"), args); #else QDesktopServices::openUrl(QUrl::fromLocalFile( - QFileInfo(item->proxy_path()).dir().absolutePath())); + QFileInfo(path).dir().absolutePath())); #endif } } @@ -690,9 +744,12 @@ void ProjectExplorer::delete_proxies_for_selected_footage() continue; } - QFile::remove(item->proxy_path()); - item->clear_proxy(); - item->invalidate_all(Footage::k_filename_input); + // Facade delete: removes the file, clears the proxy state and + // invalidates the footage. + OakEngineFootage *handle = oakengine_footage_borrow( + reinterpret_cast(item)); + oakengine_footage_proxy_delete(handle); + oakengine_footage_free(handle); } } diff --git a/engine/include/oakengine/footage.h b/engine/include/oakengine/footage.h index bfe040b0f..ed93f7981 100644 --- a/engine/include/oakengine/footage.h +++ b/engine/include/oakengine/footage.h @@ -25,6 +25,7 @@ #include "export.h" #include "init.h" +#include "node.h" #include "project.h" #ifdef __cplusplus @@ -52,6 +53,10 @@ extern "C" { * oakengine_footage_free() on a borrowed handle only releases the handle * wrapper, never the node. * + * - Borrowing (oakengine_footage_borrow()): wrap a Footage node the + * caller already holds (e.g. selected in the UI) in the same borrowed + * handle model, without probing or importing anything. + * * Image sequences: the application's import asks the user whether numbered * stills form a sequence (EngineCore::confirm_image_sequence_handler). No * such handler exists behind this facade, so imported stills are always @@ -200,6 +205,19 @@ OAKENGINE_API int oakengine_footage_get_source_start_time( OAKENGINE_API OakEngineFootage *oakengine_project_import_footage( OakEngineProject *project, const char *path); +/** + * @brief Wrap an existing project footage node in a BORROWED handle. + * + * For callers that already hold an engine node (e.g. the application + * wrapping the Footage it has selected) and want to use the media + * management / stream override functions below. `node` must be a footage + * node (anything else yields NULL, see oakengine_footage_last_error()). + * The handle borrows the node -- it becomes invalid when the node's + * project is freed, and oakengine_footage_free() only releases the + * wrapper. + */ +OAKENGINE_API OakEngineFootage *oakengine_footage_borrow(OakEngineNode *node); + /* ---- Media management: relink and proxies ----------------------------------- * * These functions operate on BORROWED import handles (footage nodes living @@ -214,10 +232,10 @@ OAKENGINE_API OakEngineFootage *oakengine_project_import_footage( * * Calls Footage::set_filename(), which triggers the engine's full reprobe * cascade (Footage::clear(): streams, decoder link and the proxy state are - * reset before re-probing). Fails with OAKENGINE_E_NOT_FOUND when - * `new_path` does not exist, and with OAKENGINE_E_FAILED when the new file - * cannot be probed as media. Not undoable (same as the application's - * relink dialog). + * reset before re-probing). The footage label is left unchanged. Fails + * with OAKENGINE_E_NOT_FOUND when `new_path` does not exist, and with + * OAKENGINE_E_FAILED when the new file cannot be probed as media. Not + * undoable (same as the application's relink action). */ OAKENGINE_API int oakengine_footage_relink(OakEngineFootage *footage, const char *new_path); @@ -229,8 +247,8 @@ OAKENGINE_API int oakengine_footage_relink(OakEngineFootage *footage, * Simplified version of the application's relink dialog matching: each * footage whose stored filename does not exist is relinked to * `search_dir`/ when that file exists (exact file-name match, - * no recursion). Returns the number of relinked items (>= 0) or a negative - * code. + * no recursion; labels are left unchanged). Returns the number of + * relinked items (>= 0) or a negative code. */ OAKENGINE_API int oakengine_project_find_offline_footage(OakEngineProject *project, @@ -282,6 +300,123 @@ OAKENGINE_API int oakengine_footage_proxy_set_enabled(OakEngineFootage *self, OAKENGINE_API int oakengine_footage_proxy_get_path(OakEngineFootage *self, char *buf, int buf_size); +/* ---- Stream parameter overrides (footage properties) ------------------------ + * + * Per-stream overrides shown by the application's footage properties + * dialog (app/dialog/footageproperties). All setters here are UNDOABLE + * (pushed onto the global undo stack as one command each, matching the + * dialog's undoable commands; the engine stores these as plain + * VideoParams/AudioParams values on the footage node). Getters are direct + * reads. Everything in this section requires a borrowed import handle + * (probe handles are rejected with OAKENGINE_E_INVALID), and stream + * indexes are the index within the stream's own type (0-based). + */ + +/** + * @brief Read a video stream's override parameters. + * + * Fills the current values (VideoParams): colorspace name (buf/size + * convention, may be NULL to skip), color_range + * (VideoParams::ColorRange), interlacing (VideoParams::Interlacing) and + * premultiplied-alpha flag. Any output pointer may be NULL. Returns + * OAKENGINE_E_NOT_FOUND for an out-of-range stream index. + */ +OAKENGINE_API int oakengine_footage_get_video_stream_overrides( + OakEngineFootage *self, int stream_index, char *colorspace_buf, + int colorspace_size, int *color_range, int *interlacing, + int *premultiplied); + +/** + * @brief Write a video stream's overrides (undoable). + * + * Pass NULL for `colorspace` and -1 for any int field to leave that field + * unchanged. An empty colorspace string ("") clears the override back to + * the project's default input color space. + */ +OAKENGINE_API int oakengine_footage_set_video_stream_overrides( + OakEngineFootage *self, int stream_index, const char *colorspace, + int color_range, int interlacing, int premultiplied); + +/** + * @brief Video stream pixel aspect ratio (num/den). + */ +OAKENGINE_API int oakengine_footage_get_pixel_aspect(OakEngineFootage *self, + int stream_index, int *num, + int *den); + +/** + * @brief Set the video stream pixel aspect ratio (undoable). Both values + * must be > 0. + */ +OAKENGINE_API int oakengine_footage_set_pixel_aspect(OakEngineFootage *self, + int stream_index, int num, + int den); + +/** + * @brief Image-sequence parameters of a video stream: start index, + * duration in frames and frame rate (num/den). Returns + * OAKENGINE_E_NOT_FOUND for an out-of-range index. + */ +OAKENGINE_API int oakengine_footage_get_image_sequence_params( + OakEngineFootage *self, int stream_index, int64_t *start_index, + int64_t *duration, int *frame_rate_num, int *frame_rate_den); + +/** + * @brief Set image-sequence parameters (undoable). `duration` must be > 0 + * and the frame rate positive. + */ +OAKENGINE_API int oakengine_footage_set_image_sequence_params( + OakEngineFootage *self, int stream_index, int64_t start_index, + int64_t duration, int frame_rate_num, int frame_rate_den); + +/** + * @brief 1 if a stream is enabled (VideoParams/AudioParams/SubtitleParams + * ::enabled()). `track_type` is an OAKENGINE_TRACK_TYPE_* value; + * OAKENGINE_E_NOT_FOUND for an out-of-range index. + */ +OAKENGINE_API int oakengine_footage_get_stream_enabled(OakEngineFootage *self, + int track_type, + int index); + +/** + * @brief Enable or disable a stream (undoable). + */ +OAKENGINE_API int oakengine_footage_set_stream_enabled(OakEngineFootage *self, + int track_type, + int index, int enabled); + +/** + * @brief Set or clear the source start time (undoable; mirrors the + * dialog's FootageSetSourceStartTimeCommand with source "manual"). + * `enabled` == 0 clears it; otherwise the time is num/den seconds. + */ +OAKENGINE_API int oakengine_footage_set_source_start_time( + OakEngineFootage *self, int enabled, int64_t num, int64_t den); + +/** + * @brief The detection source of the source start time (e.g. "manual" or + * the metadata field it was read from; empty when unset). buf/size + * convention. + */ +OAKENGINE_API int oakengine_footage_get_source_start_time_source( + OakEngineFootage *self, char *buf, int buf_size); + +/* ---- Colorspace candidates --------------------------------------------------- */ + +/** + * @brief Number of color spaces in the project's color config (the same + * list the footage properties dialog's color space dropdown shows). + */ +OAKENGINE_API int +oakengine_footage_colorspace_count(const OakEngineFootage *self); + +/** + * @brief Color space name at `index` in the project color config + * (buf/size convention). OAKENGINE_E_NOT_FOUND for an out-of-range index. + */ +OAKENGINE_API int oakengine_footage_colorspace_at( + const OakEngineFootage *self, int index, char *buf, int buf_size); + #ifdef __cplusplus } #endif diff --git a/engine/src/capi/footage.cpp b/engine/src/capi/footage.cpp index e6de06519..e18872009 100644 --- a/engine/src/capi/footage.cpp +++ b/engine/src/capi/footage.cpp @@ -149,6 +149,183 @@ olive::Footage *borrowed_node(OakEngineFootage *self) return node; } +// Push an undoable command onto the global undo stack when the engine is +// initialized, otherwise execute it directly. +void push_or_run(olive::UndoCommand *command, const QString &name) +{ + if (olive::EngineCore::instance()) { + olive::EngineCore::instance()->undo_stack()->push(command, name); + } else { + command->redo_now(); + delete command; + } +} + +// Undo commands for footage stream overrides. The engine has no undo +// commands for these (the application's footage properties dialog carries +// them at the app layer), so the facade carries read-modify-write +// equivalents with the same semantics. +class FootageVideoParamsCommand : public olive::UndoCommand { +public: + FootageVideoParamsCommand(olive::Footage *footage, int index, + const olive::VideoParams ¶ms) + : footage_(footage) + , index_(index) + , new_params_(params) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return footage_->project(); + } + +protected: + virtual void redo() override + { + old_params_ = footage_->get_video_params(index_); + footage_->set_video_params(new_params_, index_); + } + + virtual void undo() override + { + footage_->set_video_params(old_params_, index_); + } + +private: + olive::Footage *footage_; + int index_; + olive::VideoParams old_params_; + olive::VideoParams new_params_; +}; + +class FootageAudioParamsCommand : public olive::UndoCommand { +public: + FootageAudioParamsCommand(olive::Footage *footage, int index, + const olive::AudioParams ¶ms) + : footage_(footage) + , index_(index) + , new_params_(params) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return footage_->project(); + } + +protected: + virtual void redo() override + { + old_params_ = footage_->get_audio_params(index_); + footage_->set_audio_params(new_params_, index_); + } + + virtual void undo() override + { + footage_->set_audio_params(old_params_, index_); + } + +private: + olive::Footage *footage_; + int index_; + olive::AudioParams old_params_; + olive::AudioParams new_params_; +}; + +class FootageSubtitleParamsCommand : public olive::UndoCommand { +public: + FootageSubtitleParamsCommand(olive::Footage *footage, int index, + const olive::SubtitleParams ¶ms) + : footage_(footage) + , index_(index) + , new_params_(params) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return footage_->project(); + } + +protected: + virtual void redo() override + { + old_params_ = footage_->get_subtitle_params(index_); + footage_->set_subtitle_params(new_params_, index_); + } + + virtual void undo() override + { + footage_->set_subtitle_params(old_params_, index_); + } + +private: + olive::Footage *footage_; + int index_; + olive::SubtitleParams old_params_; + olive::SubtitleParams new_params_; +}; + +class FootageSourceStartTimeCommand : public olive::UndoCommand { +public: + FootageSourceStartTimeCommand(olive::Footage *footage, bool enabled, + const olive::Rational &time) + : footage_(footage) + , new_enabled_(enabled) + , new_time_(time) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return footage_->project(); + } + +protected: + virtual void redo() override + { + old_enabled_ = footage_->has_source_start_time(); + old_time_ = footage_->source_start_time(); + old_source_ = footage_->source_start_time_source(); + + if (new_enabled_) { + footage_->set_source_start_time(new_time_, + QStringLiteral("manual")); + } else { + footage_->clear_source_start_time(); + } + } + + virtual void undo() override + { + if (old_enabled_) { + footage_->set_source_start_time(old_time_, old_source_); + } else { + footage_->clear_source_start_time(); + } + } + +private: + olive::Footage *footage_; + bool new_enabled_; + olive::Rational new_time_; + bool old_enabled_ = false; + olive::Rational old_time_; + QString old_source_; +}; + +// Stream access by facade track type; returns false for unknown indexes. +bool video_stream_at(const olive::Footage *f, int index, + olive::VideoParams *out) +{ + if (index < 0 || index >= f->get_video_stream_count()) { + return false; + } + *out = f->get_video_params(index); + return true; +} + } // namespace // Internal cross-family accessor (not part of the public C ABI): returns @@ -383,6 +560,21 @@ OakEngineFootage *oakengine_project_import_footage(OakEngineProject *project, return wrap(state); } +OakEngineFootage *oakengine_footage_borrow(OakEngineNode *node) +{ + set_error(QString()); + auto *footage = + dynamic_cast(reinterpret_cast(node)); + if (!footage) { + set_error(QStringLiteral("node is not a footage node")); + return nullptr; + } + auto *state = new OakEngineFootageState(); + state->borrowed = true; + state->node = footage; + return wrap(state); +} + int oakengine_footage_relink(OakEngineFootage *footage, const char *new_path) { set_error(QString()); @@ -400,10 +592,11 @@ int oakengine_footage_relink(OakEngineFootage *footage, const char *new_path) return OAKENGINE_E_NOT_FOUND; } - // Same as the application's relink dialog: set_filename() triggers - // clear() (streams, decoder and proxy state reset) and a reprobe. + // Same as the application's relink action: set_filename() triggers + // clear() (streams, decoder and proxy state reset) and a reprobe. The + // label is intentionally left alone (relinking changes the path, not + // the user's naming). node->set_filename(path); - node->set_label(QFileInfo(path).fileName()); if (!node->is_valid()) { set_error(QStringLiteral("failed to probe \"%1\" as media") .arg(path)); @@ -438,12 +631,12 @@ int oakengine_project_find_offline_footage(OakEngineProject *project, if (filename.isEmpty() || QFileInfo::exists(filename)) { continue; } - // Exact file-name match in the search directory (no recursion). + // Exact file-name match in the search directory (no recursion). The + // label is intentionally left alone. const QString candidate = dir.filePath(QFileInfo(filename).fileName()); if (QFileInfo::exists(candidate)) { footage->set_filename(candidate); - footage->set_label(QFileInfo(candidate).fileName()); if (footage->is_valid()) { relinked++; } @@ -575,4 +768,318 @@ int oakengine_footage_proxy_get_path(OakEngineFootage *self, char *buf, return string_to_buf(impl(self)->node->proxy_path(), buf, buf_size); } +/* ---- Stream parameter overrides --------------------------------------------- */ + +int oakengine_footage_get_video_stream_overrides( + OakEngineFootage *self, int stream_index, char *colorspace_buf, + int colorspace_size, int *color_range, int *interlacing, + int *premultiplied) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + if (colorspace_buf) { + string_to_buf(vp.colorspace(), colorspace_buf, colorspace_size); + } + if (color_range) { + *color_range = int(vp.color_range()); + } + if (interlacing) { + *interlacing = int(vp.interlacing()); + } + if (premultiplied) { + *premultiplied = vp.premultiplied_alpha() ? 1 : 0; + } + return OAKENGINE_OK; +} + +int oakengine_footage_set_video_stream_overrides( + OakEngineFootage *self, int stream_index, const char *colorspace, + int color_range, int interlacing, int premultiplied) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + if (colorspace) { + vp.set_colorspace(QString::fromUtf8(colorspace)); + } + if (color_range >= 0) { + vp.set_color_range( + static_cast(color_range)); + } + if (interlacing >= 0) { + vp.set_interlacing( + static_cast(interlacing)); + } + if (premultiplied >= 0) { + vp.set_premultiplied_alpha(premultiplied != 0); + } + push_or_run(new FootageVideoParamsCommand(node, stream_index, vp), + QStringLiteral("Set Video Stream Overrides")); + return OAKENGINE_OK; +} + +int oakengine_footage_get_pixel_aspect(OakEngineFootage *self, + int stream_index, int *num, int *den) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + if (num) { + *num = vp.pixel_aspect_ratio().numerator(); + } + if (den) { + *den = vp.pixel_aspect_ratio().denominator(); + } + return OAKENGINE_OK; +} + +int oakengine_footage_set_pixel_aspect(OakEngineFootage *self, + int stream_index, int num, int den) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + if (num <= 0 || den <= 0) { + set_error(QStringLiteral("invalid pixel aspect ratio %1/%2") + .arg(num) + .arg(den)); + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + vp.set_pixel_aspect_ratio(olive::Rational(num, den)); + push_or_run(new FootageVideoParamsCommand(node, stream_index, vp), + QStringLiteral("Set Pixel Aspect Ratio")); + return OAKENGINE_OK; +} + +int oakengine_footage_get_image_sequence_params( + OakEngineFootage *self, int stream_index, int64_t *start_index, + int64_t *duration, int *frame_rate_num, int *frame_rate_den) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + if (start_index) { + *start_index = vp.start_time(); + } + if (duration) { + *duration = vp.duration(); + } + if (frame_rate_num) { + *frame_rate_num = vp.frame_rate().numerator(); + } + if (frame_rate_den) { + *frame_rate_den = vp.frame_rate().denominator(); + } + return OAKENGINE_OK; +} + +int oakengine_footage_set_image_sequence_params( + OakEngineFootage *self, int stream_index, int64_t start_index, + int64_t duration, int frame_rate_num, int frame_rate_den) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + if (start_index < 0 || duration <= 0 || frame_rate_num <= 0 || + frame_rate_den <= 0) { + set_error(QStringLiteral( + "invalid image sequence parameters (need start >= 0, duration > " + "0, positive frame rate)")); + return OAKENGINE_E_INVALID; + } + olive::VideoParams vp; + if (!video_stream_at(node, stream_index, &vp)) { + set_error(QStringLiteral("no video stream at index %1") + .arg(stream_index)); + return OAKENGINE_E_NOT_FOUND; + } + vp.set_start_time(start_index); + vp.set_duration(duration); + const olive::Rational frame_rate(frame_rate_num, frame_rate_den); + vp.set_frame_rate(frame_rate); + vp.set_time_base(frame_rate.flipped()); + push_or_run(new FootageVideoParamsCommand(node, stream_index, vp), + QStringLiteral("Set Image Sequence Parameters")); + return OAKENGINE_OK; +} + +int oakengine_footage_get_stream_enabled(OakEngineFootage *self, + int track_type, int index) +{ + if (!self || !impl(self)->node) { + return OAKENGINE_E_INVALID; + } + const olive::Footage *node = impl(self)->node; + switch (track_type) { + case 0: + if (index >= 0 && index < node->get_video_stream_count()) { + return node->get_video_params(index).enabled() ? 1 : 0; + } + break; + case 1: + if (index >= 0 && index < node->get_audio_stream_count()) { + return node->get_audio_params(index).enabled() ? 1 : 0; + } + break; + case 2: + if (index >= 0 && index < node->get_subtitle_stream_count()) { + return node->get_subtitle_params(index).enabled() ? 1 : 0; + } + break; + default: + break; + } + return OAKENGINE_E_NOT_FOUND; +} + +int oakengine_footage_set_stream_enabled(OakEngineFootage *self, + int track_type, int index, + int enabled) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + switch (track_type) { + case 0: + if (index >= 0 && index < node->get_video_stream_count()) { + olive::VideoParams vp = node->get_video_params(index); + vp.set_enabled(enabled != 0); + push_or_run( + new FootageVideoParamsCommand(node, index, vp), + QStringLiteral("Set Stream Enabled")); + return OAKENGINE_OK; + } + break; + case 1: + if (index >= 0 && index < node->get_audio_stream_count()) { + olive::AudioParams ap = node->get_audio_params(index); + ap.set_enabled(enabled != 0); + push_or_run( + new FootageAudioParamsCommand(node, index, ap), + QStringLiteral("Set Stream Enabled")); + return OAKENGINE_OK; + } + break; + case 2: + if (index >= 0 && index < node->get_subtitle_stream_count()) { + olive::SubtitleParams sp = node->get_subtitle_params(index); + sp.set_enabled(enabled != 0); + push_or_run( + new FootageSubtitleParamsCommand(node, index, sp), + QStringLiteral("Set Stream Enabled")); + return OAKENGINE_OK; + } + break; + default: + set_error(QStringLiteral("unknown track type %1").arg(track_type)); + return OAKENGINE_E_INVALID; + } + set_error(QStringLiteral("no stream of type %1 at index %2") + .arg(track_type) + .arg(index)); + return OAKENGINE_E_NOT_FOUND; +} + +int oakengine_footage_set_source_start_time(OakEngineFootage *self, + int enabled, int64_t num, + int64_t den) +{ + set_error(QString()); + olive::Footage *node = borrowed_node(self); + if (!node) { + return OAKENGINE_E_INVALID; + } + if (enabled && den == 0) { + set_error(QStringLiteral("invalid time denominator 0")); + return OAKENGINE_E_INVALID; + } + push_or_run(new FootageSourceStartTimeCommand( + node, enabled != 0, + olive::Rational::from_double( + den != 0 ? double(num) / double(den) : 0.0)), + QStringLiteral("Set Source Start Time")); + return OAKENGINE_OK; +} + +int oakengine_footage_get_source_start_time_source(OakEngineFootage *self, + char *buf, int buf_size) +{ + if (!self || !impl(self)->node) { + return OAKENGINE_E_INVALID; + } + return string_to_buf(impl(self)->node->source_start_time_source(), buf, + buf_size); +} + +/* ---- Colorspace candidates ----------------------------------------------------- */ + +int oakengine_footage_colorspace_count(const OakEngineFootage *self) +{ + if (!self || !impl(self)->node || !impl(self)->node->project()) { + return 0; + } + return impl(self)->node->project()->color_manager()->get_config() + ->getNumColorSpaces(); +} + +int oakengine_footage_colorspace_at(const OakEngineFootage *self, int index, + char *buf, int buf_size) +{ + if (!self || !impl(self)->node || !impl(self)->node->project()) { + return OAKENGINE_E_INVALID; + } + const ocio::ConstConfigRcPtr config = + impl(self)->node->project()->color_manager()->get_config(); + if (index < 0 || index >= config->getNumColorSpaces()) { + return OAKENGINE_E_NOT_FOUND; + } + return string_to_buf(config->getColorSpaceNameByIndex(index), buf, + buf_size); +} + } // extern "C" diff --git a/engine/tests/oakengine_footage_test.cpp b/engine/tests/oakengine_footage_test.cpp index 9b6520fc7..b33665091 100644 --- a/engine/tests/oakengine_footage_test.cpp +++ b/engine/tests/oakengine_footage_test.cpp @@ -217,6 +217,10 @@ static void test_failures(void) assert(oakengine_project_import_footage(NULL, path) == NULL); oakengine_project_free(project); + // Borrowing nothing yields no handle. + assert(oakengine_footage_borrow(NULL) == NULL); + assert(oakengine_footage_last_error(err, sizeof(err)) > 0); + // NULL safety. oakengine_footage_free(NULL); assert(oakengine_footage_get_decoder_name(NULL, err, sizeof(err)) == @@ -409,6 +413,181 @@ static void test_proxy(void) oakengine_project_free(project); } +static void test_stream_overrides(void) +{ + OakEngineProject *project = oakengine_project_create(); + assert(project != NULL); + assert(oakengine_project_new(project) == OAKENGINE_OK); + + char path[4096]; + demo_path(path, sizeof(path)); + OakEngineFootage *f = oakengine_project_import_footage(project, path); + assert(f != NULL); + + char cs[128]; + int range = -1, interlace = -1, premult = -1; + + // Defaults from the probe. + assert(oakengine_footage_get_video_stream_overrides( + f, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_OK); + assert(range == 0); // limited + assert(interlace == 0); // progressive + assert(premult == 0); + + // Full override write + read-back + undo/redo. + assert(oakengine_footage_set_video_stream_overrides( + f, 0, "Rec.709 OETF", 1, 1, 1) == OAKENGINE_OK); + assert(oakengine_footage_get_video_stream_overrides( + f, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_OK); + assert(strcmp(cs, "Rec.709 OETF") == 0); + assert(range == 1 && interlace == 1 && premult == 1); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_footage_get_video_stream_overrides( + f, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_OK); + assert(range == 0 && interlace == 0 && premult == 0); + assert(oakengine_project_redo(project) == OAKENGINE_OK); + assert(oakengine_footage_get_video_stream_overrides( + f, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_OK); + assert(range == 1 && interlace == 1 && premult == 1); + + // Partial write: NULL/-1 leaves fields alone. + assert(oakengine_footage_set_video_stream_overrides(f, 0, NULL, 0, -1, + -1) == OAKENGINE_OK); + assert(oakengine_footage_get_video_stream_overrides( + f, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_OK); + assert(range == 0 && interlace == 1 && premult == 1); + + // Pixel aspect ratio. + int num = 0, den = 0; + assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) == + OAKENGINE_OK); + assert(num == 1 && den == 1); + assert(oakengine_footage_set_pixel_aspect(f, 0, 4, 3) == OAKENGINE_OK); + assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) == + OAKENGINE_OK); + assert(num == 4 && den == 3); + assert(oakengine_footage_set_pixel_aspect(f, 0, 0, 3) == + OAKENGINE_E_INVALID); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) == + OAKENGINE_OK); + assert(num == 1 && den == 1); + + // Image-sequence parameters round-trip (stored even for non-sequence + // footage; the dialog shows them only for image sequences). + int64_t start = -1, dur = -1; + assert(oakengine_footage_get_image_sequence_params(f, 0, &start, &dur, + &num, &den) == + OAKENGINE_OK); + assert(oakengine_footage_set_image_sequence_params(f, 0, 10, 100, 25, + 1) == OAKENGINE_OK); + assert(oakengine_footage_get_image_sequence_params(f, 0, &start, &dur, + &num, &den) == + OAKENGINE_OK); + assert(start == 10 && dur == 100 && num == 25 && den == 1); + assert(oakengine_footage_set_image_sequence_params(f, 0, 0, 0, 25, 1) == + OAKENGINE_E_INVALID); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + + // Stream enable toggles. + assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 1); + assert(oakengine_footage_set_stream_enabled(f, 0, 0, 0) == OAKENGINE_OK); + assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 0); + assert(oakengine_footage_set_stream_enabled(f, 0, 0, 1) == OAKENGINE_OK); + assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 1); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 0); + assert(oakengine_footage_get_stream_enabled(f, 1, 0) == 1); + assert(oakengine_footage_get_stream_enabled(f, 0, 99) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_set_stream_enabled(f, 9, 0, 1) == + OAKENGINE_E_INVALID); + + // Source start time set/clear with source label. + int snum = 0, sden = 0; + assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1); + char source[64]; + assert(oakengine_footage_set_source_start_time(f, 1, 42, 1) == + OAKENGINE_OK); + assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1); + assert(snum == 42 && sden == 1); + assert(oakengine_footage_get_source_start_time_source(f, source, + sizeof(source)) > 0); + assert(strcmp(source, "manual") == 0); + assert(oakengine_footage_set_source_start_time(f, 0, 0, 1) == + OAKENGINE_OK); + assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 0); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1); + assert(snum == 42); + + // Out-of-range and NULL. + assert(oakengine_footage_get_video_stream_overrides( + f, 9, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_set_video_stream_overrides(f, 9, "x", 0, 0, + 0) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_get_pixel_aspect(f, 9, &num, &den) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_get_image_sequence_params(f, 9, &start, &dur, + &num, &den) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_get_video_stream_overrides( + NULL, 0, cs, sizeof(cs), &range, &interlace, &premult) == + OAKENGINE_E_INVALID); + OakEngineFootage *probed = oakengine_footage_probe(path); + assert(probed != NULL); + assert(oakengine_footage_set_video_stream_overrides(probed, 0, "x", 0, + 0, 0) == + OAKENGINE_E_INVALID); + assert(oakengine_footage_colorspace_count(probed) == 0); + oakengine_footage_free(probed); + + oakengine_footage_free(f); + oakengine_project_free(project); +} + +static void test_colorspace_candidates(void) +{ + OakEngineProject *project = oakengine_project_create(); + assert(project != NULL); + assert(oakengine_project_new(project) == OAKENGINE_OK); + + char path[4096]; + demo_path(path, sizeof(path)); + OakEngineFootage *f = oakengine_project_import_footage(project, path); + assert(f != NULL); + + const int count = oakengine_footage_colorspace_count(f); + assert(count > 0); + char name[128]; + int found_rec709 = 0; + for (int i = 0; i < count; i++) { + assert(oakengine_footage_colorspace_at(f, i, name, sizeof(name)) > 0); + assert(strlen(name) > 0); + if (strcmp(name, "Rec.709 OETF") == 0) { + found_rec709 = 1; + } + } + assert(found_rec709 == 1); + assert(oakengine_footage_colorspace_at(f, count, name, sizeof(name)) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_colorspace_at(f, -1, name, sizeof(name)) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_footage_colorspace_at(NULL, 0, name, sizeof(name)) == + OAKENGINE_E_INVALID); + assert(oakengine_footage_colorspace_count(NULL) == 0); + + oakengine_footage_free(f); + oakengine_project_free(project); +} + int main(void) { make_tmpdir(); @@ -429,6 +608,8 @@ int main(void) test_relink(); test_find_offline(); test_proxy(); + test_stream_overrides(); + test_colorspace_candidates(); assert(oakengine_shutdown() == OAKENGINE_OK);