refactor(task): OTIO load/save tasks over oaknode/oaktimeline C ABIs

- LoadOTIOTask/SaveOTIOTask de-Qt'd: graph traversal and construction
  through oaknode C ABI (find_input_footage, sequence defaults, block
  factories, context positions), track creation through oaktimeline's
  add_track command; the OTIO import dialog becomes a facade callback
  (headless default: accept all)
- oaknode additions: find_input_footage, sequence_set_default_parameters
- USE_OTIO + otio-install libs wired into oaktask; OTIO dylibs copied
  next to liboaktask for @loader_path resolution
- OTIO round-trip test passes; build-oaktask 106/106 green
This commit is contained in:
2026-08-07 14:58:56 +08:00
parent 40a336276f
commit da2ab512c6
15 changed files with 1139 additions and 685 deletions
+11
View File
@@ -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<Footage>(), 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
+6
View File
@@ -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.
+19
View File
@@ -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);
+22
View File
@@ -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<olive::Footage *> found =
to_node(node)->find_input_nodes<olive::Footage>();
if (!found.empty()) {
*out = reinterpret_cast<OakNodeFootage *>(found.front());
}
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
+14
View File
@@ -323,3 +323,17 @@ OakNodeNode *oaknode_sequence_as_node(OakNodeSequence *sequence)
{
return reinterpret_cast<OakNodeNode *>(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;
}
}
+54
View File
@@ -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<std::string> &names) {
std::vector<const char *> 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;
});
}
+15
View File
@@ -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
$<TARGET_FILE_DIR:oaktask>)
+483
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "loadotio.h"
#include <filesystem>
#include <map>
#include <olive/core/util/rational.h>
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/timeline.h>
#include <opentimelineio/transition.h>
#include <opentimelineio/version.h>
#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<OTIO::Timeline *> timelines;
if (root->schema_name() == "SerializableCollection") {
// This is a number of timelines
std::vector<OTIO::SerializableObject::Retainer<OTIO::SerializableObject>>
&root_children =
static_cast<OTIO::SerializableCollection *>(root)->children();
timelines.resize(root_children.size());
for (size_t j = 0; j < root_children.size(); j++) {
timelines[j] =
static_cast<OTIO::Timeline *>(root_children[j].value);
}
} else if (root->schema_name() == "Timeline") {
// This is a single timeline
timelines.push_back(static_cast<OTIO::Timeline *>(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<std::string, OakNodeFootage *> imported_footage;
std::map<OTIO::Timeline *, OakNodeSequence *> 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<OTIO::Track *>(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<std::string> 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<OTIO::Track *>(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::Item *>(otio_block)
->source_range()
->start_time()
.to_seconds();
double duration_seconds =
static_cast<OTIO::Item *>(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::Transition *>(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::Clip *>(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::ExternalReference *>(
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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "loadotio.h"
#ifdef USE_OTIO
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/timeline.h>
#include <opentimelineio/transition.h>
#include <QApplication>
#include <QFileInfo>
#include <QThread>
#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<OTIO::Timeline *> timelines;
if (root->schema_name() == "SerializableCollection") {
// This is a number of timelines
std::vector<OTIO::SerializableObject::Retainer<OTIO::SerializableObject>>
&root_children =
static_cast<OTIO::SerializableCollection *>(root)->children();
timelines.resize(root_children.size());
for (size_t j = 0; j < root_children.size(); j++) {
timelines[j] =
static_cast<OTIO::Timeline *>(root_children[j].value);
}
} else if (root->schema_name() == "Timeline") {
// This is a single timeline
timelines.push_back(static_cast<OTIO::Timeline *>(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<QString, Footage *> imported_footage;
QMap<OTIO::Timeline *, Sequence *> 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<OTIO::Track *>(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<Sequence *>, 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<OTIO::Track *>(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::Item *>(otio_block)
->source_range()
->start_time()
.to_seconds());
duration = Rational::from_double(
static_cast<OTIO::Item *>(otio_block)
->source_range()
->duration()
.to_seconds());
if (otio_block->schema_name() == "Clip") {
static_cast<ClipBlock *>(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<TransitionBlock *>(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<TransitionBlock *>(block);
OTIO::Transition *otio_block_transition =
static_cast<OTIO::Transition *>(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::Clip *>(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::ExternalReference *>(
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
@@ -19,29 +19,43 @@
***/
#ifndef OAK_OTIODECODER_H
#define OAK_OTIODECODER_H
#ifndef OAK_LOADOTIOTASK_H
#define OAK_LOADOTIOTASK_H
#ifdef USE_OTIO
#include <functional>
#include <string>
#include <vector>
#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<bool(
const std::vector<std::string> &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
+404
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "saveotio.h"
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/serializableObject.h>
#include <opentimelineio/timeline.h>
#include <opentimelineio/transition.h>
#include <opentimelineio/version.h>
#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<OakNodeSequence *> 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<OakNodeSequence *>(child));
}
}
if (sequences.empty()) {
set_error("Project contains no sequences to export.");
return false;
}
std::vector<OTIO::SerializableObject *> 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<OTIO::Timeline> *timeline_retainer =
new OTIO::Timeline::Retainer<OTIO::Timeline>(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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "saveotio.h"
#ifdef USE_OTIO
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/serializableObject.h>
#include <opentimelineio/transition.h>
#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<Sequence *> sequences =
project_->root()->list_children_of_type<Sequence>();
if (sequences.isEmpty()) {
set_error(tr("Project contains no sequences to export."));
return false;
}
std::vector<OTIO::SerializableObject *> 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<OTIO::Timeline> *timeline_retainer =
new OTIO::Timeline::Retainer<OTIO::Timeline>(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<ClipBlock *>(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<Footage *> media_nodes = block->find_input_nodes<Footage>();
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<ClipBlock *>(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<GapBlock *>(block)) {
otio_block =
new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(),
block->length().toRationalTime()),
block->get_label().toStdString());
} else if (dynamic_cast<TransitionBlock *>(block)) {
auto otio_transition =
new OTIO::Transition(block->get_label().toStdString());
TransitionBlock *our_transition =
static_cast<TransitionBlock *>(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
@@ -19,43 +19,50 @@
***/
#ifndef OAK_PROJECTSAVEASOTIOTASK_H
#define OAK_PROJECTSAVEASOTIOTASK_H
#ifndef OAK_SAVEOTIOTASK_H
#define OAK_SAVEOTIOTASK_H
#ifdef USE_OTIO
#include <string>
#include <opentimelineio/timeline.h>
#include <opentimelineio/track.h>
#include <opentimelineio/version.h>
#include <olive/core/util/rational.h>
#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
+2 -2
View File
@@ -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)
+60
View File
@@ -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);
}