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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>)
|
||||
|
||||
@@ -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
|
||||
+25
-11
@@ -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
|
||||
@@ -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
|
||||
+24
-17
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user