diff --git a/include/node/node.h b/include/node/node.h index d94b30afd..7bb8397c1 100644 --- a/include/node/node.h +++ b/include/node/node.h @@ -103,6 +103,9 @@ typedef struct OakNodeNode OakNodeNode; /* Re-declared here so node.h is self-contained; see node/project.h. */ typedef struct OakNodeProject OakNodeProject; +/* Re-declared here so node.h is self-contained; see node/footage.h. */ +typedef struct OakNodeFootage OakNodeFootage; + /** * @brief Opaque borrowed handles to the timeline data owned by viewer * nodes (TimelineMarkerList / TimelineWorkArea in oaktimeline). @@ -588,6 +591,14 @@ int oaknode_viewer_set_video_params(OakNodeNode *viewer, int oaknode_viewer_set_audio_params(OakNodeNode *viewer, const OakAudioParams *params); +/** + * @brief Find a footage node upstream of this node's inputs + * (Node::find_input_nodes(), first match). *out is a + * borrowed handle or NULL when none. + */ +int oaknode_node_find_input_footage(const OakNodeNode *node, + OakNodeFootage **out); + /** * @brief Create a command that removes a node from its graph together * with its exclusive dependencies and disconnects its edges diff --git a/include/node/sequence.h b/include/node/sequence.h index fb7cf68ff..5e6a5d0cd 100644 --- a/include/node/sequence.h +++ b/include/node/sequence.h @@ -83,6 +83,12 @@ void oaknode_sequence_free(OakNodeSequence *sequence); * @param type One of OAKNODE_TRACK_TYPE_VIDEO / _AUDIO / _SUBTITLE. * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND (bad type). */ +/** + * @brief Apply the default video/audio parameters + * (ViewerOutput::set_default_parameters()). + */ +int oaknode_sequence_set_default_parameters(OakNodeSequence *sequence); + /** * @brief Borrowed cast from a sequence handle to its node handle. * NULL for NULL. diff --git a/include/task/project.h b/include/task/project.h index a370cf0bb..6347f87ff 100644 --- a/include/task/project.h +++ b/include/task/project.h @@ -70,6 +70,25 @@ int oaktask_import_invalid_count(OakTaskTask *t); int oaktask_import_invalid_at(OakTaskTask *t, int index, char *buf, int buf_size); +/** @brief olive::LoadOTIOTask. */ +OakTaskTask *oaktask_create_project_load_otio(const char *filename); + +/** @brief Take the loaded project (ownership transfer). */ +OakNodeProject *oaktask_load_otio_take_project(OakTaskTask *t); + +/** @brief olive::SaveOTIOTask. */ +OakTaskTask *oaktask_create_project_save_otio(OakNodeProject *project, + const char *filename); + +/** + * @brief OTIO import confirmation callback (facade concern; default + * accepts everything). Return non-zero to accept. + */ +typedef int (*oaktask_otio_import_confirm_fn)( + const char *const *sequence_names, int count, void *userdata); +void oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, + void *userdata); + /** @brief olive::PreCacheTask. */ OakTaskTask *oaktask_create_precache(OakNodeFootage *footage, int index, OakNodeSequence *sequence); diff --git a/src/node/c_api/node.cpp b/src/node/c_api/node.cpp index 176b5d956..7d17ed4ca 100644 --- a/src/node/c_api/node.cpp +++ b/src/node/c_api/node.cpp @@ -32,6 +32,8 @@ #include "output/viewer/viewer.h" +#include "project/footage/footage.h" + #include "valueconvert.h" namespace @@ -1332,3 +1334,23 @@ int oaknode_viewer_set_audio_params(OakNodeNode *viewer, return OAKNODE_E_FAILED; } } + +int oaknode_node_find_input_footage(const OakNodeNode *node, + OakNodeFootage **out) +{ + if (!node || !out) { + return OAKNODE_E_INVALID; + } + + *out = NULL; + try { + std::vector found = + to_node(node)->find_input_nodes(); + if (!found.empty()) { + *out = reinterpret_cast(found.front()); + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/sequence.cpp b/src/node/c_api/sequence.cpp index 88f147863..49392372d 100644 --- a/src/node/c_api/sequence.cpp +++ b/src/node/c_api/sequence.cpp @@ -323,3 +323,17 @@ OakNodeNode *oaknode_sequence_as_node(OakNodeSequence *sequence) { return reinterpret_cast(sequence); } + +int oaknode_sequence_set_default_parameters(OakNodeSequence *sequence) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + + try { + impl(sequence)->set_default_parameters(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/task/c_api/project.cpp b/src/task/c_api/project.cpp index 56862ef91..a58a63366 100644 --- a/src/task/c_api/project.cpp +++ b/src/task/c_api/project.cpp @@ -25,6 +25,8 @@ #include "../src/export/export.h" #include "../src/precache/precachetask.h" #include "../src/project/import/import.h" +#include "../src/project/loadotio/loadotio.h" +#include "../src/project/saveotio/saveotio.h" #include "../src/project/load/load.h" #include "../src/project/save/save.h" #include "taskhandle.h" @@ -207,3 +209,55 @@ OakTaskTask *oaktask_create_export(OakNodeNode *viewer, return NULL; } } + +OakTaskTask *oaktask_create_project_load_otio(const char *filename) +{ + if (!filename) { + return NULL; + } + try { + return wrap(new olive::LoadOTIOTask(filename)); + } catch (...) { + return NULL; + } +} + +OakNodeProject *oaktask_load_otio_take_project(OakTaskTask *t) +{ + olive::ProjectLoadBaseTask *task = load_impl(t); + if (!task) { + return NULL; + } + return task->take_project(); +} + +OakTaskTask *oaktask_create_project_save_otio(OakNodeProject *project, + const char *filename) +{ + if (!project || !filename) { + return NULL; + } + try { + return wrap(new olive::SaveOTIOTask(project, filename)); + } catch (...) { + return NULL; + } +} + +void oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, + void *userdata) +{ + if (!fn) { + olive::LoadOTIOTask::set_import_confirm_callback(nullptr); + return; + } + olive::LoadOTIOTask::set_import_confirm_callback( + [fn, userdata](const std::vector &names) { + std::vector ptrs; + ptrs.reserve(names.size()); + for (const std::string &name : names) { + ptrs.push_back(name.c_str()); + } + return fn(ptrs.data(), int(ptrs.size()), userdata) != 0; + }); +} diff --git a/src/task/src/CMakeLists.txt b/src/task/src/CMakeLists.txt index 66e3a6336..d670cbcfa 100644 --- a/src/task/src/CMakeLists.txt +++ b/src/task/src/CMakeLists.txt @@ -25,9 +25,16 @@ target_include_directories(oaktask PUBLIC ${OAK_REPO_ROOT}/src/common/src ${OAK_REPO_ROOT}/src/undo/src ${OAK_REPO_ROOT}/core/include + ${OAK_REPO_ROOT}/otio-install/include ) +target_compile_definitions(oaktask PUBLIC USE_OTIO) + +target_link_directories(oaktask PUBLIC ${OAK_REPO_ROOT}/otio-install/lib) + target_link_libraries(oaktask PUBLIC + opentimelineio + opentime oaknode oakrender oakcodec @@ -35,3 +42,11 @@ target_link_libraries(oaktask PUBLIC oakcommon olivecore ) + +# OTIO dylibs use @loader_path install names; they must sit next to +# liboaktask.dylib (the loading object). +add_custom_command(TARGET oaktask POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${OAK_REPO_ROOT}/otio-install/lib/libopentimelineio.dylib + ${OAK_REPO_ROOT}/otio-install/lib/libopentime.dylib + $) diff --git a/src/task/src/project/loadotio/loadotio.cpp b/src/task/src/project/loadotio/loadotio.cpp new file mode 100644 index 000000000..b5d4990a5 --- /dev/null +++ b/src/task/src/project/loadotio/loadotio.cpp @@ -0,0 +1,483 @@ +/*** + + 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 "loadotio.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "node/block.h" +#include "node/factory.h" +#include "node/folder.h" +#include "node/footage.h" +#include "node/node.h" +#include "node/project.h" +#include "node/sequence.h" +#include "node/track.h" +#include "timeline/edit.h" + +namespace OTIO = opentimelineio::OPENTIMELINEIO_VERSION; + +namespace olive +{ + +using core::Rational; + +namespace +{ + +const char *k_sequence_id = "org.olivevideoeditor.Olive.sequence"; +const char *k_transform_id = "org.olivevideoeditor.Olive.transform"; +const char *k_volume_id = "org.olivevideoeditor.Olive.volume"; + +void set_own_context_position(OakNodeNode *node, double x, double y) +{ + oaknode_node_set_context_position(node, node, x, y, 0); +} + +} // namespace + +LoadOTIOTask::ImportConfirmFn LoadOTIOTask::confirm_callback_; + +LoadOTIOTask::LoadOTIOTask(const std::string &filename) + : ProjectLoadBaseTask(filename) +{ +} + +bool LoadOTIOTask::run() +{ + OTIO::ErrorStatus es; + + auto root = OTIO::SerializableObjectWithMetadata::from_json_file( + get_filename(), &es); + + if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { + set_error("Failed to load OpenTimelineIO from file \"" + + get_filename() + "\" \n\nOpenTimelineIO Error:\n\n" + + es.full_description); + return false; + } + + project_ = oaknode_project_init(); + if (!project_) { + set_error("Failed to create project"); + return false; + } + oaknode_project_initialize(project_); + oaknode_project_set_modified(project_, 1); + + std::vector timelines; + + if (root->schema_name() == "SerializableCollection") { + // This is a number of timelines + std::vector> + &root_children = + static_cast(root)->children(); + + timelines.resize(root_children.size()); + for (size_t j = 0; j < root_children.size(); j++) { + timelines[j] = + static_cast(root_children[j].value); + } + } else if (root->schema_name() == "Timeline") { + // This is a single timeline + timelines.push_back(static_cast(root)); + } else { + // Unknown root, we don't know what to do with this + set_error("Unknown OpenTimelineIO root element"); + oaknode_project_free(project_); + project_ = nullptr; + return false; + } + + // Keep track of imported footage + std::map imported_footage; + std::map timeline_sequence_map; + + // Variables used for loading bar + float number_of_clips = 0; + float clips_done = 0; + + // Generate a list of sequences with the same names as the timelines. + // Assumes each timeline has a unique name. + int unnamed_sequence_count = 0; + for (auto timeline : timelines) { + OakNodeSequence *sequence = oaknode_sequence_create(); + if (!sequence) { + continue; + } + + std::string label; + if (!timeline->name().empty()) { + label = timeline->name(); + } else { + // If the otio timeline does not provide a name, create a default one here + unnamed_sequence_count++; + label = "Sequence " + std::to_string(unnamed_sequence_count); + } + oaknode_node_set_label(oaknode_sequence_as_node(sequence), + label.c_str()); + + // Set default params incase they aren't edited. + oaknode_sequence_set_default_parameters(sequence); + timeline_sequence_map.insert({ timeline, sequence }); + + // Get number of clips for loading bar + for (auto track : timeline->tracks()->children()) { + auto otio_track = static_cast(track.value); + number_of_clips += float(otio_track->children().size()); + } + } + if (number_of_clips <= 0) { + number_of_clips = 1; + } + + // Ask the user which sequences to import (facade callback; headless + // default accepts everything) + std::vector sequence_names; + sequence_names.reserve(timeline_sequence_map.size()); + for (const auto &pair : timeline_sequence_map) { + char buf[256]; + if (oaknode_node_get_label(oaknode_sequence_as_node(pair.second), + buf, sizeof(buf)) > 0) { + sequence_names.emplace_back(buf); + } else { + sequence_names.emplace_back(); + } + } + + bool accepted = + confirm_callback_ ? confirm_callback_(sequence_names) : true; + + if (!accepted) { + // Cancel to indicate to caller that this task did not complete and to simply dispose of it + cancel(); + for (const auto &pair : timeline_sequence_map) { + oaknode_sequence_free(pair.second); + } + return true; + } + + for (const auto &pair : timeline_sequence_map) { + OTIO::Timeline *timeline = pair.first; + OakNodeSequence *sequence = pair.second; + OakNodeNode *sequence_node = oaknode_sequence_as_node(sequence); + + oaknode_project_add_node(project_, sequence_node); + OakUndoCommand *add_seq = oaknode_command_create_folder_add_child( + oaknode_project_root(project_), sequence_node); + if (add_seq) { + oakundo_command_redo_now(add_seq); + oakundo_command_free(add_seq); + } + + // Create a folder for this sequence's footage + OakNodeFolder *sequence_footage = + oaknode_folder_create(project_); + if (sequence_footage) { + oaknode_node_set_label(oaknode_folder_as_node(sequence_footage), + timeline->name().c_str()); + OakUndoCommand *add_folder = + oaknode_command_create_folder_add_child( + oaknode_project_root(project_), + oaknode_folder_as_node(sequence_footage)); + if (add_folder) { + oakundo_command_redo_now(add_folder); + oakundo_command_free(add_folder); + } + } + + // Iterate through tracks + for (auto c : timeline->tracks()->children()) { + auto otio_track = static_cast(c.value); + + // Create a new track + OakNodeTrack *track = nullptr; + + // Determine what kind of track it is + int track_type = OAKNODE_TRACK_TYPE_NONE; + if (otio_track->kind() == "Video") { + track_type = OAKNODE_TRACK_TYPE_VIDEO; + } else if (otio_track->kind() == "Audio") { + track_type = OAKNODE_TRACK_TYPE_AUDIO; + } else { + fprintf(stderr, "Found unknown track type: %s\n", + otio_track->kind().c_str()); + continue; + } + + { + OakNodeTrackList *track_list = nullptr; + oaknode_sequence_get_track_list(sequence, track_type, + &track_list); + OakUndoCommand *add_track = + oaktimeline_add_track_command(track_list); + if (add_track) { + oakundo_command_redo_now(add_track); + oakundo_command_free(add_track); + } + + int count = 0; + oaknode_tracklist_get_track_count(track_list, &count); + if (count > 0) { + oaknode_tracklist_get_track_at(track_list, count - 1, + &track); + } + } + + if (!track) { + continue; + } + + // Get clips from track + auto clip_map = otio_track->children(); + + OakNodeBlock *previous_block = nullptr; + bool prev_block_transition = false; + + for (auto otio_block_retainer : clip_map) { + auto otio_block = otio_block_retainer.value; + + OakNodeBlock *block = nullptr; + + if (otio_block->schema_name() == "Clip") { + block = oaknode_block_clip_create(); + + } else if (otio_block->schema_name() == "Gap") { + block = oaknode_block_gap_create(); + + } else if (otio_block->schema_name() == "Transition") { + // Todo: Look into OTIO supported transitions and add them to Olive + block = oaknode_block_transition_create( + OAKNODE_TRANSITION_CROSS_DISSOLVE); + + } else { + // We don't know what this is yet, just create a gap for now so that *something* is there + fprintf(stderr, "Found unknown block type: %s\n", + otio_block->schema_name().c_str()); + block = oaknode_block_gap_create(); + } + + if (!block) { + continue; + } + + oaknode_project_add_node(project_, + oaknode_block_as_node(block)); + oaknode_node_set_label(oaknode_block_as_node(block), + otio_block->name().c_str()); + + oaknode_track_append_block(track, block); + + if (otio_block->schema_name() == "Clip" || + otio_block->schema_name() == "Gap") { + double start_seconds = + static_cast(otio_block) + ->source_range() + ->start_time() + .to_seconds(); + double duration_seconds = + static_cast(otio_block) + ->source_range() + ->duration() + .to_seconds(); + + Rational start_time = + Rational::from_double(start_seconds); + Rational duration = + Rational::from_double(duration_seconds); + + if (otio_block->schema_name() == "Clip") { + oaknode_clip_set_media_in( + block, start_time.numerator(), + start_time.denominator()); + } + oaknode_block_set_length_and_media_out( + block, duration.numerator(), + duration.denominator()); + } + + // If the previous block was a transition, connect the current block to it + if (prev_block_transition) { + oaknode_node_connect( + oaknode_block_as_node(block), + oaknode_block_as_node(previous_block), + OAKNODE_TRANSITION_IN_BLOCK_INPUT); + prev_block_transition = false; + } + + if (otio_block->schema_name() == "Transition") { + OTIO::Transition *otio_block_transition = + static_cast(otio_block); + + // Set how far the transition eats into the previous clip + Rational in_offset = Rational::fromRationalTime( + otio_block_transition->in_offset()); + Rational out_offset = Rational::fromRationalTime( + otio_block_transition->out_offset()); + oaknode_transition_set_offsets_and_length( + block, in_offset.numerator(), in_offset.denominator(), + out_offset.numerator(), out_offset.denominator()); + + if (previous_block) { + oaknode_node_connect( + oaknode_block_as_node(previous_block), + oaknode_block_as_node(block), + OAKNODE_TRANSITION_OUT_BLOCK_INPUT); + } + prev_block_transition = true; + + // Position transition in its own context + set_own_context_position(oaknode_block_as_node(block), + 0, 0); + } + + if (otio_block->schema_name() == "Gap") { + // Position gap in its own context + set_own_context_position(oaknode_block_as_node(block), + 0, 0); + } + + // Update this after it's used but before any continue statements + previous_block = block; + + if (otio_block->schema_name() == "Clip") { + auto otio_clip = static_cast(otio_block); + if (!otio_clip->media_reference()) { + continue; + } + if (otio_clip->media_reference()->schema_name() == + "ExternalReference") { + // Link footage + std::string footage_url = + static_cast( + otio_clip->media_reference()) + ->target_url(); + + OakNodeFootage *probed_item = nullptr; + + auto it = imported_footage.find(footage_url); + if (it != imported_footage.end()) { + probed_item = it->second; + } else { + probed_item = oaknode_footage_create( + project_, footage_url.c_str()); + if (probed_item) { + imported_footage.insert( + { footage_url, probed_item }); + + std::string label = + std::filesystem::path(footage_url) + .filename() + .string(); + oaknode_node_set_label( + oaknode_footage_as_node(probed_item), + label.c_str()); + + if (sequence_footage) { + OakUndoCommand *add_footage = + oaknode_command_create_folder_add_child( + sequence_footage, + oaknode_footage_as_node( + probed_item)); + if (add_footage) { + oakundo_command_redo_now(add_footage); + oakundo_command_free(add_footage); + } + } + } + } + + if (probed_item) { + // Position clip in its own context + set_own_context_position( + oaknode_block_as_node(block), 0, 0); + + // Position footage in its context + oaknode_node_set_context_position( + oaknode_block_as_node(block), + oaknode_footage_as_node(probed_item), -2, 0, + 0); + + if (track_type == OAKNODE_TRACK_TYPE_VIDEO) { + OakNodeNode *transform = + oaknode_factory_create_from_id( + k_transform_id); + if (transform) { + oaknode_project_add_node(project_, + transform); + + oaknode_node_connect( + oaknode_footage_as_node( + probed_item), + transform, "tex_in"); + oaknode_node_connect( + transform, + oaknode_block_as_node(block), + "buffer_in"); + oaknode_node_set_context_position( + oaknode_block_as_node(block), + transform, -1, 0, 0); + } + } else { + OakNodeNode *volume_node = + oaknode_factory_create_from_id( + k_volume_id); + if (volume_node) { + oaknode_project_add_node(project_, + volume_node); + + oaknode_node_connect( + oaknode_footage_as_node( + probed_item), + volume_node, "samples_in"); + oaknode_node_connect( + volume_node, + oaknode_block_as_node(block), + "buffer_in"); + oaknode_node_set_context_position( + oaknode_block_as_node(block), + volume_node, -1, 0, 0); + } + } + } + } + } + clips_done++; + emit_progress(clips_done / number_of_clips); + } + } + } + + return true; +} + +} diff --git a/src/task/src/project/loadotio/loadotio.cpp.pending b/src/task/src/project/loadotio/loadotio.cpp.pending deleted file mode 100644 index 4ea804d8d..000000000 --- a/src/task/src/project/loadotio/loadotio.cpp.pending +++ /dev/null @@ -1,377 +0,0 @@ -/*** - - 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 "loadotio.h" - -#ifdef USE_OTIO - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "coreengine.h" -#include "node/audio/volume/volume.h" -#include "node/block/clip/clip.h" -#include "node/block/gap/gap.h" -#include "node/block/transition/crossdissolve/crossdissolvetransition.h" -#include "node/distort/transform/transformdistortnode.h" -#include "node/generator/matrix/matrix.h" -#include "node/math/math/math.h" -#include "node/nodeundo.h" -#include "node/project/folder/folder.h" -#include "node/project/footage/footage.h" -#include "node/project/sequence/sequence.h" -#include "timeline/timelineundogeneral.h" - -namespace olive -{ - -LoadOTIOTask::LoadOTIOTask(const QString &s) - : ProjectLoadBaseTask(s) -{ -} - -bool LoadOTIOTask::run() -{ - OTIO::ErrorStatus es; - - auto root = OTIO::SerializableObjectWithMetadata::from_json_file( - get_filename().toStdString(), &es); - - if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - set_error( - tr("Failed to load OpenTimelineIO from file \"%1\" \n\nOpenTimelineIO Error:\n\n%2") - .arg(get_filename(), - QString::fromStdString(es.full_description))); - return false; - } - - project_ = new Project(); - project_->initialize(); - project_->set_modified(true); - - std::vector timelines; - - if (root->schema_name() == "SerializableCollection") { - // This is a number of timelines - std::vector> - &root_children = - static_cast(root)->children(); - - timelines.resize(root_children.size()); - for (size_t j = 0; j < root_children.size(); j++) { - timelines[j] = - static_cast(root_children[j].value); - } - } else if (root->schema_name() == "Timeline") { - // This is a single timeline - timelines.push_back(static_cast(root)); - } else { - // Unknown root, we don't know what to do with this - set_error(tr("Unknown OpenTimelineIO root element")); - delete project_; - project_ = nullptr; - return false; - } - - // Keep track of imported footage - QMap imported_footage; - QMap timeline_sequnce_map; - - // Variables used for loading bar - float number_of_clips = 0; - float clips_done = 0; - - // Generate a list of sequences with the same names as the timelines. - // Assumes each timeline has a unique name. - int unnamed_sequence_count = 0; - for (auto timeline : timelines) { - Sequence *sequence = new Sequence(); - if (!timeline->name().empty()) { - sequence->set_label(QString::fromStdString(timeline->name())); - } else { - // If the otio timeline does not provide a name, create a default one here - unnamed_sequence_count++; - QString label = tr("Sequence %1").arg(unnamed_sequence_count); - sequence->set_label(QString::fromStdString(label.toStdString())); - } - // Set default params incase they aren't edited. - sequence->set_default_parameters(); - timeline_sequnce_map.insert(timeline, sequence); - - // Get number of clips for loading bar - for (auto track : timeline->tracks()->children()) { - auto otio_track = static_cast(track.value); - number_of_clips += otio_track->children().size(); - } - } - - // Dialog has to be called from the main thread so we pass the list of sequences here. - bool accepted = false; - QMetaObject::invokeMethod( - EngineCore::instance(), "show_otio_import_dialog", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, accepted), - Q_ARG(QList, timeline_sequnce_map.values())); - - if (!accepted) { - // Cancel to indicate to caller that this task did not complete and to simply dispose of it - Cancel(); - qDeleteAll(timeline_sequnce_map); // Clear sequences - return true; - } - - foreach (auto timeline, timeline_sequnce_map.keys()) { - Sequence *sequence = timeline_sequnce_map.value(timeline); - sequence->setParent(project_); - FolderAddChild(project_->root(), sequence).redo_now(); - - // Create a folder for this sequence's footage - Folder *sequence_footage = new Folder(); - sequence_footage->set_label(QString::fromStdString(timeline->name())); - sequence_footage->setParent(project_); - FolderAddChild(project_->root(), sequence_footage).redo_now(); - - // Iterate through tracks - for (auto c : timeline->tracks()->children()) { - auto otio_track = static_cast(c.value); - - // Create a new track - Track *track = nullptr; - - // Determine what kind of track it is - if (otio_track->kind() == "Video" || - otio_track->kind() == "Audio") { - Track::Type type; - - if (otio_track->kind() == "Video") { - type = Track::k_video; - } else { - type = Track::k_audio; - } - - // Create track - TimelineAddTrackCommand t(sequence->track_list(type)); - t.redo_now(); - track = t.track(); - } else { - qWarning() << "Found unknown track type:" - << otio_track->kind().c_str(); - continue; - } - - // Get clips from track - auto clip_map = otio_track->children(); - if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - set_error(tr("Failed to load clip")); - return false; - } - - Block *previous_block = nullptr; - bool prev_block_transition = false; - - for (auto otio_block_retainer : clip_map) { - auto otio_block = otio_block_retainer.value; - - Block *block = nullptr; - - if (otio_block->schema_name() == "Clip") { - block = new ClipBlock(); - - } else if (otio_block->schema_name() == "Gap") { - block = new GapBlock(); - - } else if (otio_block->schema_name() == "Transition") { - // Todo: Look into OTIO supported transitions and add them to Olive - block = new CrossDissolveTransition(); - - } else { - // We don't know what this is yet, just create a gap for now so that *something* is there - qWarning() << "Found unknown block type:" - << otio_block->schema_name().c_str(); - block = new GapBlock(); - } - - block->setParent(project_); - block->set_label(QString::fromStdString(otio_block->name())); - - track->append_block(block); - - Rational start_time; - Rational duration; - - if (otio_block->schema_name() == "Clip" || - otio_block->schema_name() == "Gap") { - start_time = Rational::from_double( - static_cast(otio_block) - ->source_range() - ->start_time() - .to_seconds()); - duration = Rational::from_double( - static_cast(otio_block) - ->source_range() - ->duration() - .to_seconds()); - - if (otio_block->schema_name() == "Clip") { - static_cast(block)->set_media_in( - start_time); - } - block->set_length_and_media_out(duration); - } - - // If the previous block was a transition, connect the current block to it - if (prev_block_transition) { - TransitionBlock *previous_transition_block = - static_cast(previous_block); - Node::connect_edge( - block, NodeInput(previous_transition_block, - TransitionBlock::k_in_block_input)); - prev_block_transition = false; - } - - if (otio_block->schema_name() == "Transition") { - TransitionBlock *transition_block = - static_cast(block); - OTIO::Transition *otio_block_transition = - static_cast(otio_block); - - // Set how far the transition eats into the previous clip - transition_block->set_offsets_and_length( - Rational::fromRationalTime( - otio_block_transition->in_offset()), - Rational::fromRationalTime( - otio_block_transition->out_offset())); - - if (previous_block) { - Node::connect_edge( - previous_block, - NodeInput(transition_block, - TransitionBlock::k_out_block_input)); - } - prev_block_transition = true; - - // Add nodes to the graph and set up contexts - block->setParent(sequence->parent()); - - // Position transition in its own context - block->set_node_position_in_context(block, QPointF(0, 0)); - } - - if (otio_block->schema_name() == "Gap") { - // Add nodes to the graph and set up contexts - block->setParent(sequence->parent()); - - // Position transition in its own context - block->set_node_position_in_context(block, QPointF(0, 0)); - } - - // Update this after it's used but before any continue statements - previous_block = block; - - if (otio_block->schema_name() == "Clip") { - auto otio_clip = static_cast(otio_block); - if (!otio_clip->media_reference()) { - continue; - } - if (otio_clip->media_reference()->schema_name() == - "ExternalReference") { - // Link footage - QString footage_url = QString::fromStdString( - static_cast( - otio_clip->media_reference()) - ->target_url()); - - Footage *probed_item; - - if (imported_footage.contains(footage_url)) { - probed_item = imported_footage.value(footage_url); - } else { - probed_item = new Footage(footage_url); - imported_footage.insert(footage_url, probed_item); - probed_item->setParent(project_); - - QFileInfo info(probed_item->filename()); - probed_item->set_label(info.fileName()); - - FolderAddChild add(sequence_footage, probed_item); - add.redo_now(); - } - - // Add nodes to the graph and set up contexts - block->setParent(sequence->parent()); - - // Position clip in its own context - block->set_node_position_in_context(block, QPointF(0, 0)); - - // Position footage in its context - block->set_node_position_in_context(probed_item, - QPointF(-2, 0)); - - if (track->type() == Track::k_video) { - TransformDistortNode *transform = - new TransformDistortNode(); - transform->setParent(sequence->parent()); - - Node::connect_edge( - probed_item, - NodeInput(transform, - TransformDistortNode::k_texture_input)); - Node::connect_edge(transform, - NodeInput(block, - ClipBlock::k_buffer_in)); - block->set_node_position_in_context(transform, - QPointF(-1, 0)); - } else { - VolumeNode *volume_node = new VolumeNode(); - volume_node->setParent(sequence->parent()); - - Node::connect_edge( - probed_item, - NodeInput(volume_node, - VolumeNode::k_samples_input)); - Node::connect_edge(volume_node, - NodeInput(block, - ClipBlock::k_buffer_in)); - block->set_node_position_in_context(volume_node, - QPointF(-1, 0)); - } - } - } - clips_done++; - emit progress_changed(clips_done / number_of_clips); - } - } - } - - project_->moveToThread(qApp->thread()); - - return true; -} - -} - -#endif // USE_OTIO diff --git a/src/task/src/project/loadotio/loadotio.h.pending b/src/task/src/project/loadotio/loadotio.h similarity index 55% rename from src/task/src/project/loadotio/loadotio.h.pending rename to src/task/src/project/loadotio/loadotio.h index 59f94e655..f80b78e5c 100644 --- a/src/task/src/project/loadotio/loadotio.h.pending +++ b/src/task/src/project/loadotio/loadotio.h @@ -19,29 +19,43 @@ ***/ -#ifndef OAK_OTIODECODER_H -#define OAK_OTIODECODER_H +#ifndef OAK_LOADOTIOTASK_H +#define OAK_LOADOTIOTASK_H -#ifdef USE_OTIO +#include +#include +#include -#include "common/otioutils.h" -#include "node/project.h" -#include "task/project/load/loadbasetask.h" +#include "../load/load.h" namespace olive { class LoadOTIOTask : public ProjectLoadBaseTask { - Q_OBJECT public: - LoadOTIOTask(const QString &filename); + LoadOTIOTask(const std::string &filename); + + /** + * @brief Ask the user which sequences to import (facade/UI concern). + * + * Receives the sequence labels, returns true to accept the import. + * When no callback is installed, everything is imported (headless + * default). + */ + using ImportConfirmFn = std::function &sequence_names)>; + static void set_import_confirm_callback(ImportConfirmFn callback) + { + confirm_callback_ = std::move(callback); + } protected: virtual bool run() override; + +private: + static ImportConfirmFn confirm_callback_; }; } -#endif - -#endif // OAK_OTIODECODER_H +#endif // OAK_LOADOTIOTASK_H diff --git a/src/task/src/project/saveotio/saveotio.cpp b/src/task/src/project/saveotio/saveotio.cpp new file mode 100644 index 000000000..0e34928e5 --- /dev/null +++ b/src/task/src/project/saveotio/saveotio.cpp @@ -0,0 +1,404 @@ +/*** + + 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 "saveotio.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "node/block.h" +#include "node/folder.h" +#include "node/node.h" +#include "node/footage.h" +#include "common/videoparams.h" +#include "node/sequence.h" +#include "node/track.h" + +namespace olive +{ + +namespace +{ + +std::string node_label_of(OakNodeNode *node) +{ + char buf[256]; + if (oaknode_node_get_label(node, buf, sizeof(buf)) <= 0) { + return std::string(); + } + return buf; +} + +Rational block_in_of(OakNodeBlock *b) +{ + int n = 0, d = 1; + oaknode_block_get_in(b, &n, &d); + return Rational(n, d); +} + +Rational block_length_of(OakNodeBlock *b) +{ + int n = 0, d = 1; + oaknode_block_get_length(b, &n, &d); + return Rational(n, d); +} + +Rational track_length_of(OakNodeTrack *t) +{ + int n = 0, d = 1; + oaknode_track_get_length(t, &n, &d); + return Rational(n, d); +} + +} // namespace + +SaveOTIOTask::SaveOTIOTask(OakNodeProject *project, + const std::string &filename) + : project_(project) + , filename_(filename) +{ + set_title("Exporting project to OpenTimelineIO"); +} + +bool SaveOTIOTask::run() +{ + // Collect sequences from the root folder (non-recursive, matching the + // original list_children_of_type behavior closely enough for OTIO) + std::vector sequences; + + OakNodeFolder *root = oaknode_project_root(project_); + if (!root) { + set_error("Project contains no sequences to export."); + return false; + } + + int child_count = oaknode_folder_child_count(root); + for (int i = 0; i < child_count; i++) { + OakNodeNode *child = oaknode_folder_child_at(root, i); + if (!child) { + continue; + } + + char id[128]; + if (oaknode_node_get_id(child, id, sizeof(id)) <= 0) { + continue; + } + if (std::string(id) == "org.olivevideoeditor.Olive.sequence") { + sequences.push_back( + reinterpret_cast(child)); + } + } + + if (sequences.empty()) { + set_error("Project contains no sequences to export."); + return false; + } + + std::vector serialized; + + for (OakNodeSequence *seq : sequences) { + auto otio_timeline = serialize_timeline(seq); + + if (otio_timeline) { + // Append to list + serialized.push_back(otio_timeline); + } else { + // Delete all existing timelines + for (auto s : serialized) { + s->possibly_delete(); + } + + // Error out of function + set_error("Failed to serialize sequence \"" + + node_label_of(oaknode_sequence_as_node(seq)) + "\""); + + return false; + } + } + + OTIO::ErrorStatus es; + + if (serialized.size() == 1) { + // Serialize timeline on its own + auto t = serialized.front(); + t->to_json_file(filename_, &es); + t->possibly_delete(); + } else { + // Serialize all into a SerializableCollection + auto collection = + new OTIO::SerializableCollection("Sequences", serialized); + collection->to_json_file(filename_, &es); + collection->possibly_delete(); + + // Delete all existing timelines + for (auto s : serialized) { + s->possibly_delete(); + } + } + + return (es.outcome == OTIO::ErrorStatus::Outcome::OK); +} + +OTIO::Timeline *SaveOTIOTask::serialize_timeline(OakNodeSequence *sequence) +{ + auto otio_timeline = new OTIO::Timeline( + node_label_of(oaknode_sequence_as_node(sequence))); + // Retainers clean themselves up when the final user is removed + OTIO::Timeline::Retainer *timeline_retainer = + new OTIO::Timeline::Retainer(otio_timeline); + (void)timeline_retainer; + + double rate = 0; + { + int num = 0, den = 1; + OakVideoParams vp = {}; + if (oaknode_sequence_get_video_params(sequence, 0, &vp) == + OAKNODE_OK) { + oakcommon_videoparams_get_frame_rate(vp, &num, &den); + if (den != 0) { + rate = double(num) / den; + } + oakcommon_videoparams_free(&vp); + } + } + if (rate != rate /* NaN */ || rate <= 0) { + return nullptr; + } + + OakNodeTrackList *video_list = nullptr; + OakNodeTrackList *audio_list = nullptr; + oaknode_sequence_get_track_list(sequence, OAKNODE_TRACK_TYPE_VIDEO, + &video_list); + oaknode_sequence_get_track_list(sequence, OAKNODE_TRACK_TYPE_AUDIO, + &audio_list); + + if (!serialize_track_list(video_list, otio_timeline, rate) || + !serialize_track_list(audio_list, otio_timeline, rate)) { + otio_timeline->possibly_delete(); + return nullptr; + } + + return otio_timeline; +} + +OTIO::Track *SaveOTIOTask::serialize_track(OakNodeTrack *track, + double sequence_rate, + Rational max_track_length) +{ + auto otio_track = new OTIO::Track(); + + OTIO::ErrorStatus es; + + int track_type = OAKNODE_TRACK_TYPE_NONE; + oaknode_track_get_type(track, &track_type); + + switch (track_type) { + case OAKNODE_TRACK_TYPE_VIDEO: + otio_track->set_kind("Video"); + break; + case OAKNODE_TRACK_TYPE_AUDIO: + otio_track->set_kind("Audio"); + break; + default: + fprintf(stderr, "Don't know OTIO track kind for native type %d\n", + track_type); + goto fail; + } + + { + int block_count = 0; + oaknode_track_get_block_count(track, &block_count); + + for (int i = 0; i < block_count; i++) { + OakNodeBlock *block = nullptr; + oaknode_track_get_block_at(track, i, &block); + if (!block) { + continue; + } + + OTIO::Composable *otio_block = nullptr; + + int kind = OAKNODE_BLOCK_OTHER; + oaknode_block_get_kind(block, &kind); + + if (kind == OAKNODE_BLOCK_CLIP) { + auto otio_clip = new OTIO::Clip( + node_label_of(oaknode_block_as_node(block))); + + otio_clip->set_source_range(OTIO::TimeRange( + block_in_of(block).toRationalTime(sequence_rate), + block_length_of(block).toRationalTime(sequence_rate))); + + OakNodeFootage *media = nullptr; + oaknode_node_find_input_footage( + oaknode_block_as_node(block), &media); + if (media) { + OTIO::TimeRange available_range; + if (track_type == OAKNODE_TRACK_TYPE_VIDEO) { + // OTIO ExternalReference uses the source clips frame rate (or sample rate) as opposed to + // the sequences rate + double source_frame_rate = 0; + double duration = 0; + int num = 0, den = 1; + OakVideoParams vp = {}; + if (oaknode_footage_get_video_params(media, 0, + &vp) == + OAKNODE_OK) { + oakcommon_videoparams_get_frame_rate(vp, &num, + &den); + if (den != 0) { + source_frame_rate = double(num) / den; + } + int64_t dur = 0; + oakcommon_videoparams_get_duration(vp, &dur); + duration = double(dur); + oakcommon_videoparams_free(&vp); + } + + available_range = OTIO::TimeRange( + OTIO::RationalTime(0, source_frame_rate), + OTIO::RationalTime(duration, source_frame_rate)); + } else { + available_range = OTIO::TimeRange( + OTIO::RationalTime(0, 48000), + OTIO::RationalTime(0, 48000)); + } + char media_url[1024]; + if (oaknode_footage_filename(media, media_url, + sizeof(media_url)) > 0) { + auto media_ref = new OTIO::ExternalReference( + media_url, available_range); + otio_clip->set_media_reference(media_ref); + } + } + + otio_block = otio_clip; + } else if (kind == OAKNODE_BLOCK_GAP) { + otio_block = new OTIO::Gap( + OTIO::TimeRange(block_in_of(block).toRationalTime(), + block_length_of(block).toRationalTime()), + node_label_of(oaknode_block_as_node(block))); + } else if (kind == OAKNODE_BLOCK_TRANSITION) { + auto otio_transition = new OTIO::Transition( + node_label_of(oaknode_block_as_node(block))); + + int n = 0, d = 1; + oaknode_transition_get_in_offset(block, &n, &d); + otio_transition->set_in_offset( + Rational(n, d).toRationalTime()); + oaknode_transition_get_out_offset(block, &n, &d); + otio_transition->set_out_offset( + Rational(n, d).toRationalTime()); + + otio_block = otio_transition; + } + + if (!otio_block) { + // We shouldn't ever get here, but catch without crashing if we ever do + goto fail; + } + + otio_track->append_child(otio_block, &es); + + if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { + goto fail; + } + } + } + + // All OTIO tracks must have the same duration so we add a Gap to fill the remaining time + if (otio_track->duration(&es).to_seconds() < + max_track_length.to_double()) { + double time_left = max_track_length.to_double() - + otio_track->duration(&es).to_seconds(); + + OTIO::Gap *gap = new OTIO::Gap(OTIO::TimeRange( + otio_track->duration(&es), OTIO::RationalTime(time_left, 1.0))); + otio_track->append_child(gap, &es); + + if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { + goto fail; + } + } + + return otio_track; + +fail: + otio_track->possibly_delete(); + + return nullptr; +} + +bool SaveOTIOTask::serialize_track_list(OakNodeTrackList *list, + OTIO::Timeline *otio_timeline, + double sequence_rate) +{ + if (!list) { + return true; + } + + OTIO::ErrorStatus es; + + Rational max_track_length = RATIONAL_MIN; + + int track_count = 0; + oaknode_tracklist_get_track_count(list, &track_count); + + for (int i = 0; i < track_count; i++) { + OakNodeTrack *track = nullptr; + oaknode_tracklist_get_track_at(list, i, &track); + if (track && track_length_of(track) > max_track_length) { + max_track_length = track_length_of(track); + } + } + + for (int i = 0; i < track_count; i++) { + OakNodeTrack *track = nullptr; + oaknode_tracklist_get_track_at(list, i, &track); + if (!track) { + continue; + } + + auto otio_track = serialize_track(track, sequence_rate, + max_track_length); + + if (!otio_track) { + return false; + } + + otio_timeline->tracks()->append_child(otio_track, &es); + + if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { + otio_track->possibly_delete(); + return false; + } + } + + return true; +} + +} diff --git a/src/task/src/project/saveotio/saveotio.cpp.pending b/src/task/src/project/saveotio/saveotio.cpp.pending deleted file mode 100644 index 0eac31175..000000000 --- a/src/task/src/project/saveotio/saveotio.cpp.pending +++ /dev/null @@ -1,278 +0,0 @@ -/*** - - 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 "saveotio.h" - -#ifdef USE_OTIO - -#include -#include -#include -#include -#include -#include - -#include "node/block/clip/clip.h" -#include "node/block/gap/gap.h" -#include "node/block/transition/transition.h" -#include "node/project/footage/footage.h" - -namespace olive -{ - -SaveOTIOTask::SaveOTIOTask(Project *project) - : project_(project) -{ - set_title(tr("Exporting project to OpenTimelineIO")); -} - -bool SaveOTIOTask::run() -{ - QVector sequences = - project_->root()->list_children_of_type(); - - if (sequences.isEmpty()) { - set_error(tr("Project contains no sequences to export.")); - return false; - } - - std::vector serialized; - - foreach (Sequence *seq, sequences) { - auto otio_timeline = SerializeTimeline(seq); - - if (otio_timeline) { - // Append to list - serialized.push_back(otio_timeline); - } else { - // Delete all existing timelines - for (auto s : serialized) { - s->possibly_delete(); - } - - // Error out of function - set_error( - tr("Failed to serialize sequence \"%1\"").arg(seq->get_label())); - - return false; - } - } - - OTIO::ErrorStatus es; - - if (serialized.size() == 1) { - // Serialize timeline on its own - auto t = serialized.front(); - t->to_json_file(project_->filename().toStdString(), &es); - t->possibly_delete(); - } else { - // Serialize all into a SerializableCollection - auto collection = - new OTIO::SerializableCollection("Sequences", serialized); - collection->to_json_file(project_->filename().toStdString(), &es); - collection->possibly_delete(); - - // Delete all existing timelines - for (auto s : serialized) { - s->possibly_delete(); - } - } - - return (es.outcome == OTIO::ErrorStatus::Outcome::OK); -} - -OTIO::Timeline *SaveOTIOTask::SerializeTimeline(Sequence *sequence) -{ - auto otio_timeline = new OTIO::Timeline(sequence->get_label().toStdString()); - // Retainers clean themselves up when the final user is removed - OTIO::Timeline::Retainer *timeline_retainer = - new OTIO::Timeline::Retainer(otio_timeline); - // Suppress unused variable warning - Q_UNUSED(timeline_retainer); - - double rate = sequence->get_video_params().frame_rate().to_double(); - if (qIsNaN(rate)) { - return nullptr; - } - - if (!SerializeTrackList(sequence->track_list(Track::k_video), otio_timeline, - rate) || - !SerializeTrackList(sequence->track_list(Track::k_audio), otio_timeline, - rate)) { - otio_timeline->possibly_delete(); - return nullptr; - } - - return otio_timeline; -} - -OTIO::Track *SaveOTIOTask::SerializeTrack(Track *track, double sequence_rate, - Rational max_track_length) -{ - auto otio_track = new OTIO::Track(); - - OTIO::ErrorStatus es; - - switch (track->type()) { - case Track::k_video: - otio_track->set_kind("Video"); - break; - case Track::k_audio: - otio_track->set_kind("Audio"); - break; - default: - qWarning() - << "Don't know OTIO track kind for native type" << track->type(); - goto fail; - } - - foreach (Block *block, track->blocks()) { - OTIO::Composable *otio_block = nullptr; - - if (dynamic_cast(block)) { - auto otio_clip = new OTIO::Clip(block->get_label().toStdString()); - - otio_clip->set_source_range( - OTIO::TimeRange(block->in().toRationalTime(sequence_rate), - block->length().toRationalTime(sequence_rate))); - - QVector media_nodes = block->find_input_nodes(); - if (!media_nodes.isEmpty()) { - OTIO::TimeRange available_range; - if (otio_track->kind().compare("Video") == 0) { - // OTIO ExternalReference uses the source clips frame rate (or sample rate) as opposed to - // the sequences rate - double source_frame_rate = static_cast(block) - ->connected_viewer() - ->get_video_params() - .frame_rate() - .to_double(); - available_range = OTIO::TimeRange( - OTIO::RationalTime(0, source_frame_rate), - OTIO::RationalTime( - media_nodes.first()->get_video_params().duration(), - source_frame_rate)); - } else if (otio_track->kind().compare("Audio") == 0) { - available_range = OTIO::TimeRange( - OTIO::RationalTime( - 0, - media_nodes.first()->get_audio_params().sample_rate()), - OTIO::RationalTime( - media_nodes.first()->get_audio_params().duration(), - media_nodes.first()->get_audio_params().sample_rate())); - } - auto media_ref = new OTIO::ExternalReference( - media_nodes.first()->filename().toStdString(), - available_range); - otio_clip->set_media_reference(media_ref); - } - - otio_block = otio_clip; - } else if (dynamic_cast(block)) { - otio_block = - new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(), - block->length().toRationalTime()), - block->get_label().toStdString()); - } else if (dynamic_cast(block)) { - auto otio_transition = - new OTIO::Transition(block->get_label().toStdString()); - - TransitionBlock *our_transition = - static_cast(block); - - otio_transition->set_in_offset( - our_transition->in_offset().toRationalTime()); - otio_transition->set_out_offset( - our_transition->out_offset().toRationalTime()); - - otio_block = new OTIO::Transition(); - } - - if (!otio_block) { - // We shouldn't ever get here, but catch without crashing if we ever do - goto fail; - } - - otio_track->append_child(otio_block, &es); - - if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - goto fail; - } - } - - // All OTIO tracks must have the same duration so we add a Gap to fill the remaining time - if (otio_track->duration(&es).to_seconds() < max_track_length.to_double()) { - double time_left = max_track_length.to_double() - - otio_track->duration(&es).to_seconds(); - - OTIO::Gap *gap = new OTIO::Gap(OTIO::TimeRange( - otio_track->duration(&es), OTIO::RationalTime(time_left, 1.0))); - otio_track->append_child(gap, &es); - - if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - goto fail; - } - } - - return otio_track; - -fail: - otio_track->possibly_delete(); - - return nullptr; -} - -bool SaveOTIOTask::SerializeTrackList(TrackList *list, - OTIO::Timeline *otio_timeline, - double sequence_rate) -{ - OTIO::ErrorStatus es; - - Rational max_track_length = RATIONAL_MIN; - - foreach (Track *track, list->get_tracks()) { - if (track->track_length() > max_track_length) { - max_track_length = track->track_length(); - } - } - - foreach (Track *track, list->get_tracks()) { - auto otio_track = - SerializeTrack(track, sequence_rate, max_track_length); - - if (!otio_track) { - return false; - } - - otio_timeline->tracks()->append_child(otio_track, &es); - - if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - otio_track->possibly_delete(); - return false; - } - } - - return true; -} - -} - -#endif // USE_OTIO diff --git a/src/task/src/project/saveotio/saveotio.h.pending b/src/task/src/project/saveotio/saveotio.h similarity index 56% rename from src/task/src/project/saveotio/saveotio.h.pending rename to src/task/src/project/saveotio/saveotio.h index c51db6263..01ac8a582 100644 --- a/src/task/src/project/saveotio/saveotio.h.pending +++ b/src/task/src/project/saveotio/saveotio.h @@ -19,43 +19,50 @@ ***/ -#ifndef OAK_PROJECTSAVEASOTIOTASK_H -#define OAK_PROJECTSAVEASOTIOTASK_H +#ifndef OAK_SAVEOTIOTASK_H +#define OAK_SAVEOTIOTASK_H -#ifdef USE_OTIO +#include #include -#include +#include + +#include -#include "common/otioutils.h" #include "node/project.h" -#include "task/task.h" +#include "node/sequence.h" +#include "node/track.h" +#include "task.h" + +namespace OTIO = opentimelineio::OPENTIMELINEIO_VERSION; namespace olive { +using core::Rational; + class SaveOTIOTask : public Task { - Q_OBJECT public: - SaveOTIOTask(Project *project); + SaveOTIOTask(OakNodeProject *project, const std::string &filename); protected: virtual bool run() override; private: - OTIO::Timeline *SerializeTimeline(Sequence *sequence); + OTIO::Timeline *serialize_timeline(OakNodeSequence *sequence); - OTIO::Track *SerializeTrack(Track *track, double sequence_rate, - Rational max_track_length); + OTIO::Track *serialize_track(OakNodeTrack *track, double sequence_rate, + Rational max_track_length); - bool SerializeTrackList(TrackList *list, OTIO::Timeline *otio_timeline, - double sequence_rate); + bool serialize_track_list(OakNodeTrackList *list, + OTIO::Timeline *otio_timeline, + double sequence_rate); - Project *project_; + OakNodeProject *project_; + + std::string filename_; }; } -#endif - -#endif // OAK_PROJECTSAVEASOTIOTASK_H +#endif // OAK_SAVEOTIOTASK_H diff --git a/src/task/standalone/CMakeLists.txt b/src/task/standalone/CMakeLists.txt index cbcf76a8a..298921309 100644 --- a/src/task/standalone/CMakeLists.txt +++ b/src/task/standalone/CMakeLists.txt @@ -148,6 +148,7 @@ target_include_directories(oaknode-gtest PRIVATE ${OAK_REPO_ROOT}/engine/include ) + target_link_libraries(oaktask-gtest PRIVATE oakrender) target_sources(oaktask-gtest PRIVATE ${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp @@ -171,7 +172,6 @@ target_link_options(oaktask-gtest PRIVATE target_link_options(oaknode-gtest PRIVATE "-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}") -# oaknode's Sequence::add_default_nodes() constructs # TimelineAddTrackCommand, which lives in liboaktask; the two # libraries resolve each other at runtime (dynamic_lookup). -target_link_libraries(oaknode-gtest PRIVATE oaktask) +target_link_libraries(oaknode-gtest PRIVATE oaktimeline) diff --git a/src/task/tests/task_test.cpp b/src/task/tests/task_test.cpp index c9f5b3e73..5c52cd249 100644 --- a/src/task/tests/task_test.cpp +++ b/src/task/tests/task_test.cpp @@ -362,3 +362,63 @@ TEST(OakTaskRenderFamily, ExportTaskConstruction) EXPECT_EQ(oaktask_debug_alive_count(), 0); } + +// ---- OTIO round trip ------------------------------------------------------- + +TEST(OakTaskOTIO, SaveLoadRoundTrip) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + + // A sequence with a name so save has something to serialize + OakNodeSequence *sequence = oaknode_sequence_create(); + ASSERT_NE(sequence, nullptr); + ASSERT_EQ(oaknode_project_add_node(project, + oaknode_sequence_as_node(sequence)), + OAKNODE_OK); + ASSERT_EQ(oaknode_sequence_set_default_parameters(sequence), + OAKNODE_OK); + ASSERT_EQ(oaknode_node_set_label(oaknode_sequence_as_node(sequence), + "OTIO Test Sequence"), + OAKNODE_OK); + + OakNodeFolder *root = oaknode_project_root(project); + ASSERT_NE(root, nullptr); + OakUndoCommand *add_seq = oaknode_command_create_folder_add_child( + root, oaknode_sequence_as_node(sequence)); + ASSERT_NE(add_seq, nullptr); + oakundo_command_redo_now(add_seq); + oakundo_command_free(add_seq); + + std::string path = + (std::filesystem::temp_directory_path() / "oaktask_otio_test.otio") + .string(); + std::error_code ec; + std::filesystem::remove(path, ec); + + OakTaskTask *save = + oaktask_create_project_save_otio(project, path.c_str()); + ASSERT_NE(save, nullptr); + ASSERT_EQ(oaktask_task_start_sync(save), 1); + oaktask_task_free(save); + + ASSERT_TRUE(std::filesystem::exists(path)); + + OakTaskTask *load = oaktask_create_project_load_otio(path.c_str()); + ASSERT_NE(load, nullptr); + ASSERT_EQ(oaktask_task_start_sync(load), 1); + + OakNodeProject *loaded = oaktask_load_otio_take_project(load); + ASSERT_NE(loaded, nullptr); + oaknode_project_free(loaded); + oaktask_task_free(load); + + std::filesystem::remove(path, ec); + oaknode_project_free(project); + + EXPECT_EQ(oaktask_debug_alive_count(), 0); + + EXPECT_EQ(oaktask_create_project_load_otio(nullptr), nullptr); + EXPECT_EQ(oaktask_create_project_save_otio(nullptr, "x"), nullptr); +}