feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple trim bugs the stub was hiding); same-track via the frozen C ABI, cross-track supported by the module command - oaknode clip blocks now declare a tex_in texture input and set effect_input to it, so timeline clips can host effect chains; facade test covers effect insert/remove on a real clip - oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a Homebrew upgrade left the system ffmpeg .pc pointing at a deleted dav1d Cellar path, breaking links); reads a git-ignored workspace .env for IDEs that cannot inject env vars (RustRover); links the C++ stdlib for C++ codec libs (svt-av1) - oakengine re-exports oaknode so tests share one crate instance; it_node uses the direct instance's value type where it calls the module FFI (the --workspace dev-dependency feature split builds oaknode twice)
This commit is contained in:
@@ -156,7 +156,14 @@ impl ProjectImportTask {
|
||||
&self.invalid_files[index]
|
||||
}
|
||||
|
||||
fn import(&mut self, task: &mut Task, folder: CHandle, entries: &mut Vec<String>, counter: &mut usize, parent_command: CHandle) {
|
||||
fn import(
|
||||
&mut self,
|
||||
task: &mut Task,
|
||||
folder: CHandle,
|
||||
entries: &mut Vec<String>,
|
||||
counter: &mut usize,
|
||||
parent_command: CHandle,
|
||||
) {
|
||||
let mut i = 0;
|
||||
while i < entries.len() {
|
||||
if task.is_cancelled() {
|
||||
@@ -176,7 +183,8 @@ impl ProjectImportTask {
|
||||
};
|
||||
|
||||
if !entry_list.is_empty() {
|
||||
let folder_handle = unsafe { bridge::node::oaknode_folder_create(self.project) };
|
||||
let folder_handle =
|
||||
unsafe { bridge::node::oaknode_folder_create(self.project) };
|
||||
if !folder_handle.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::node::oaknode_node_set_label(
|
||||
@@ -184,13 +192,24 @@ impl ProjectImportTask {
|
||||
cstr(&basename_of(file_path)),
|
||||
);
|
||||
}
|
||||
self.add_item_to_folder(folder, unsafe { bridge::node::oaknode_folder_as_node(folder_handle) }, parent_command);
|
||||
self.add_item_to_folder(
|
||||
folder,
|
||||
unsafe { bridge::node::oaknode_folder_as_node(folder_handle) },
|
||||
parent_command,
|
||||
);
|
||||
let mut sub_entries = entry_list;
|
||||
self.import(task, folder_handle, &mut sub_entries, counter, parent_command);
|
||||
self.import(
|
||||
task,
|
||||
folder_handle,
|
||||
&mut sub_entries,
|
||||
counter,
|
||||
parent_command,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let footage = unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };
|
||||
let footage =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };
|
||||
if footage.ctx.is_null() {
|
||||
i += 1;
|
||||
continue;
|
||||
@@ -199,21 +218,33 @@ impl ProjectImportTask {
|
||||
unsafe {
|
||||
bridge::node::oaknode_footage_set_cancel_atom(footage, task.get_cancel_atom());
|
||||
}
|
||||
let ok = unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) } == 0;
|
||||
let ok =
|
||||
unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) }
|
||||
== 0;
|
||||
unsafe {
|
||||
bridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::null());
|
||||
bridge::node::oaknode_footage_set_cancel_atom(
|
||||
footage,
|
||||
bridge::render::OakCancelAtom::null(),
|
||||
);
|
||||
}
|
||||
|
||||
if ok && unsafe { bridge::node::oaknode_footage_is_valid(footage) } != 0 {
|
||||
unsafe {
|
||||
bridge::node::oaknode_node_set_label(bridge::node::oaknode_footage_as_node(footage), cstr(&basename_of(file_path)));
|
||||
bridge::node::oaknode_node_set_label(
|
||||
bridge::node::oaknode_footage_as_node(footage),
|
||||
cstr(&basename_of(file_path)),
|
||||
);
|
||||
}
|
||||
|
||||
// See if this footage is an image sequence.
|
||||
self.validate_image_sequence(task, footage, entries, i);
|
||||
|
||||
// Create the undoable command that adds the item.
|
||||
self.add_item_to_folder(folder, unsafe { bridge::node::oaknode_footage_as_node(footage) }, parent_command);
|
||||
self.add_item_to_folder(
|
||||
folder,
|
||||
unsafe { bridge::node::oaknode_footage_as_node(footage) },
|
||||
parent_command,
|
||||
);
|
||||
|
||||
self.imported_footage.push(footage);
|
||||
} else {
|
||||
@@ -222,7 +253,11 @@ impl ProjectImportTask {
|
||||
// Remove the invalid footage from the graph; the remove
|
||||
// command takes ownership on redo and deletes the node
|
||||
// when the command is destroyed.
|
||||
let mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(footage)) };
|
||||
let mut remove = unsafe {
|
||||
bridge::node::oaknode_command_create_remove_node(
|
||||
bridge::node::oaknode_footage_as_node(footage),
|
||||
)
|
||||
};
|
||||
if !remove.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::undo::oakundo_command_redo_now(remove);
|
||||
@@ -247,18 +282,30 @@ impl ProjectImportTask {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_image_sequence(&mut self, task: &mut Task, footage: CHandle, info_list: &mut Vec<String>, index: usize) {
|
||||
fn validate_image_sequence(
|
||||
&mut self,
|
||||
task: &mut Task,
|
||||
footage: CHandle,
|
||||
info_list: &mut Vec<String>,
|
||||
index: usize,
|
||||
) {
|
||||
let filename = footage_filename(footage);
|
||||
if filename.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let digit_count = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(&filename)) };
|
||||
let digit_count = unsafe {
|
||||
bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(&filename))
|
||||
};
|
||||
if digit_count <= 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.image_sequence_ignore_files.iter().any(|f| f == &filename) {
|
||||
if self
|
||||
.image_sequence_ignore_files
|
||||
.iter()
|
||||
.any(|f| f == &filename)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +314,9 @@ impl ProjectImportTask {
|
||||
}
|
||||
|
||||
let mut video_stream = CHandle::null();
|
||||
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut video_stream) } != 0 {
|
||||
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut video_stream) }
|
||||
!= 0
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,13 +327,16 @@ impl ProjectImportTask {
|
||||
bridge::common::oakcommon_videoparams_get_height(video_stream, &mut height);
|
||||
}
|
||||
|
||||
let seq_index = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(&filename)) };
|
||||
let seq_index =
|
||||
unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(&filename)) };
|
||||
|
||||
let prev_fn = transform_sequence_filename(&filename, seq_index - 1, digit_count);
|
||||
let next_fn = transform_sequence_filename(&filename, seq_index + 1, digit_count);
|
||||
|
||||
let previous_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&prev_fn)) };
|
||||
let next_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&next_fn)) };
|
||||
let previous_file =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&prev_fn)) };
|
||||
let next_file =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&next_fn)) };
|
||||
|
||||
let prev_matches = !previous_file.ctx.is_null()
|
||||
&& unsafe { bridge::node::oaknode_footage_is_valid(previous_file) } != 0
|
||||
@@ -325,22 +377,41 @@ impl ProjectImportTask {
|
||||
|
||||
if is_sequence {
|
||||
unsafe {
|
||||
bridge::common::oakcommon_videoparams_set_video_type(video_stream, bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE);
|
||||
bridge::common::oakcommon_videoparams_set_video_type(
|
||||
video_stream,
|
||||
bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE,
|
||||
);
|
||||
|
||||
let mut rate_buf = [0i8; 64];
|
||||
let needed = bridge::common::oakcommon_config_get(std::ptr::null(), cstr("DefaultSequenceFrameRate"), rate_buf.as_mut_ptr(), rate_buf.len() as i32);
|
||||
let needed = bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
cstr("DefaultSequenceFrameRate"),
|
||||
rate_buf.as_mut_ptr(),
|
||||
rate_buf.len() as i32,
|
||||
);
|
||||
if needed > 0 {
|
||||
let rate = crate::project::load::buf_to_string(&rate_buf);
|
||||
if let Some((num, den)) = parse_rational(&rate) {
|
||||
if den != 0 {
|
||||
bridge::common::oakcommon_videoparams_set_time_base(video_stream, num, den);
|
||||
bridge::common::oakcommon_videoparams_set_frame_rate(video_stream, den, num);
|
||||
bridge::common::oakcommon_videoparams_set_time_base(
|
||||
video_stream,
|
||||
num,
|
||||
den,
|
||||
);
|
||||
bridge::common::oakcommon_videoparams_set_frame_rate(
|
||||
video_stream,
|
||||
den,
|
||||
num,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bridge::common::oakcommon_videoparams_set_start_time(video_stream, start_index);
|
||||
bridge::common::oakcommon_videoparams_set_duration(video_stream, end_index - start_index + 1);
|
||||
bridge::common::oakcommon_videoparams_set_duration(
|
||||
video_stream,
|
||||
end_index - start_index + 1,
|
||||
);
|
||||
bridge::node::oaknode_footage_set_video_params(footage, 0, &video_stream);
|
||||
}
|
||||
}
|
||||
@@ -355,7 +426,11 @@ impl ProjectImportTask {
|
||||
// The probe footage was only created for comparison; remove it.
|
||||
for probe in [previous_file, next_file] {
|
||||
if !probe.ctx.is_null() {
|
||||
let mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(probe)) };
|
||||
let mut remove = unsafe {
|
||||
bridge::node::oaknode_command_create_remove_node(
|
||||
bridge::node::oaknode_footage_as_node(probe),
|
||||
)
|
||||
};
|
||||
if !remove.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::undo::oakundo_command_redo_now(remove);
|
||||
@@ -483,7 +558,8 @@ fn basename_of(path: &str) -> String {
|
||||
|
||||
/// Two-stage read of the footage filename.
|
||||
fn footage_filename(footage: CHandle) -> String {
|
||||
let needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
|
||||
let needed =
|
||||
unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
|
||||
if needed <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
@@ -504,7 +580,12 @@ fn transform_sequence_filename(filename: &str, number: i64, digit_count: i32) ->
|
||||
// extension; they are replaced by `number`.
|
||||
let digits_start = dot.saturating_sub(width);
|
||||
let prefix = &filename[..digits_start];
|
||||
format!("{prefix}{:0width$}{}", number, &filename[dot..], width = width)
|
||||
format!(
|
||||
"{prefix}{:0width$}{}",
|
||||
number,
|
||||
&filename[dot..],
|
||||
width = width
|
||||
)
|
||||
}
|
||||
None => format!("{filename}{:0width$}", number, width = width),
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ impl TaskBehavior for ProjectLoadBaseTask {
|
||||
task.set_error("Failed to determine project version.");
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_FILE_ERROR => {
|
||||
task.set_error(&format!("Failed to read file \"{}\" for reading.", self.filename));
|
||||
task.set_error(&format!(
|
||||
"Failed to read file \"{}\" for reading.",
|
||||
self.filename
|
||||
));
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_XML_ERROR => {
|
||||
task.set_error(&format!(
|
||||
@@ -166,5 +169,8 @@ impl TaskBehavior for ProjectLoadTask {
|
||||
/// Read a NUL-terminated char buffer into a String (lossy).
|
||||
pub(crate) fn buf_to_string(buf: &[i8]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
|
||||
unsafe {
|
||||
String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len))
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,10 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
format!("Sequence {unnamed_sequence_count}")
|
||||
};
|
||||
unsafe {
|
||||
bridge::node::oaknode_node_set_label(bridge::node::oaknode_sequence_as_node(sequence), cstr(&label));
|
||||
bridge::node::oaknode_node_set_label(
|
||||
bridge::node::oaknode_sequence_as_node(sequence),
|
||||
cstr(&label),
|
||||
);
|
||||
}
|
||||
|
||||
// Set default params incase they aren't edited.
|
||||
@@ -189,7 +192,9 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
unsafe {
|
||||
bridge::node::oaknode_project_add_node(project, sequence_node);
|
||||
}
|
||||
let mut add_seq = unsafe { bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node) };
|
||||
let mut add_seq = unsafe {
|
||||
bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node)
|
||||
};
|
||||
if !add_seq.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::undo::oakundo_command_redo_now(add_seq);
|
||||
@@ -239,9 +244,14 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// Create a new track
|
||||
let mut track_list = CHandle::null();
|
||||
unsafe {
|
||||
bridge::node::oaknode_sequence_get_track_list(*sequence, track_type, &mut track_list);
|
||||
bridge::node::oaknode_sequence_get_track_list(
|
||||
*sequence,
|
||||
track_type,
|
||||
&mut track_list,
|
||||
);
|
||||
}
|
||||
let mut add_track = unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) };
|
||||
let mut add_track =
|
||||
unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) };
|
||||
if !add_track.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::undo::oakundo_command_redo_now(add_track);
|
||||
@@ -256,7 +266,11 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
}
|
||||
if count > 0 {
|
||||
unsafe {
|
||||
bridge::node::oaknode_tracklist_get_track_at(track_list, count - 1, &mut track);
|
||||
bridge::node::oaknode_tracklist_get_track_at(
|
||||
track_list,
|
||||
count - 1,
|
||||
&mut track,
|
||||
);
|
||||
}
|
||||
}
|
||||
if track.ctx.is_null() {
|
||||
@@ -279,7 +293,9 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// Todo: Look into OTIO supported transitions and add
|
||||
// them to Oak.
|
||||
unsafe {
|
||||
bridge::node::oaknode_block_transition_create(bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE)
|
||||
bridge::node::oaknode_block_transition_create(
|
||||
bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE,
|
||||
)
|
||||
}
|
||||
}
|
||||
other => {
|
||||
@@ -394,10 +410,17 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// Link footage
|
||||
let footage_url = external.target_url().to_string();
|
||||
|
||||
let probed_item = if let Some(existing) = imported_footage.get(&footage_url) {
|
||||
let probed_item = if let Some(existing) =
|
||||
imported_footage.get(&footage_url)
|
||||
{
|
||||
*existing
|
||||
} else {
|
||||
let created = unsafe { bridge::node::oaknode_footage_create(project, cstr(&footage_url)) };
|
||||
let created = unsafe {
|
||||
bridge::node::oaknode_footage_create(
|
||||
project,
|
||||
cstr(&footage_url),
|
||||
)
|
||||
};
|
||||
if !created.ctx.is_null() {
|
||||
imported_footage.insert(footage_url.clone(), created);
|
||||
|
||||
@@ -422,7 +445,9 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
if !add_footage.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::undo::oakundo_command_redo_now(add_footage);
|
||||
bridge::undo::oakundo_command_free(&mut add_footage);
|
||||
bridge::undo::oakundo_command_free(
|
||||
&mut add_footage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,23 +472,36 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
|
||||
if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {
|
||||
let transform = unsafe {
|
||||
bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_TRANSFORM))
|
||||
bridge::node::oaknode_factory_create_from_id(cstr(
|
||||
bridge::node::OAKNODE_TYPE_TRANSFORM,
|
||||
))
|
||||
};
|
||||
if !transform.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::node::oaknode_project_add_node(project, transform);
|
||||
bridge::node::oaknode_project_add_node(
|
||||
project, transform,
|
||||
);
|
||||
bridge::node::oaknode_node_connect(
|
||||
bridge::node::oaknode_footage_as_node(probed_item),
|
||||
transform,
|
||||
cstr("tex_in"),
|
||||
);
|
||||
bridge::node::oaknode_node_connect(transform, block_node, cstr("buffer_in"));
|
||||
bridge::node::oaknode_node_set_context_position(block_node, transform, -1.0, 0.0, 0);
|
||||
bridge::node::oaknode_node_connect(
|
||||
transform,
|
||||
block_node,
|
||||
cstr("buffer_in"),
|
||||
);
|
||||
bridge::node::oaknode_node_set_context_position(
|
||||
block_node, transform, -1.0, 0.0, 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let volume =
|
||||
unsafe { bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_VOLUME)) };
|
||||
let volume = unsafe {
|
||||
bridge::node::oaknode_factory_create_from_id(cstr(
|
||||
bridge::node::OAKNODE_TYPE_VOLUME,
|
||||
))
|
||||
};
|
||||
if !volume.ctx.is_null() {
|
||||
unsafe {
|
||||
bridge::node::oaknode_project_add_node(project, volume);
|
||||
@@ -472,8 +510,14 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
volume,
|
||||
cstr("samples_in"),
|
||||
);
|
||||
bridge::node::oaknode_node_connect(volume, block_node, cstr("buffer_in"));
|
||||
bridge::node::oaknode_node_set_context_position(block_node, volume, -1.0, 0.0, 0);
|
||||
bridge::node::oaknode_node_connect(
|
||||
volume,
|
||||
block_node,
|
||||
cstr("buffer_in"),
|
||||
);
|
||||
bridge::node::oaknode_node_set_context_position(
|
||||
block_node, volume, -1.0, 0.0, 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,7 +555,11 @@ unsafe fn set_own_context_position(node: CHandle) {
|
||||
/// Format handling ends here — the caller builds the project from the
|
||||
/// returned timelines regardless of the source format (C++ parity), so the
|
||||
/// track/clip/footage code never forks.
|
||||
fn parse_timelines(task: &mut Task, filename: &str, format: InterchangeFormat) -> Result<Vec<oakotio::Timeline>> {
|
||||
fn parse_timelines(
|
||||
task: &mut Task,
|
||||
filename: &str,
|
||||
format: InterchangeFormat,
|
||||
) -> Result<Vec<oakotio::Timeline>> {
|
||||
match format {
|
||||
InterchangeFormat::OtioJson => {
|
||||
let root = oakotio::from_json_file(filename).map_err(|e| {
|
||||
@@ -530,7 +578,9 @@ fn parse_timelines(task: &mut Task, filename: &str, format: InterchangeFormat) -
|
||||
Serializable::Timeline(timeline) => Ok(vec![timeline.clone()]),
|
||||
_ => {
|
||||
task.set_error("Unknown OpenTimelineIO root element");
|
||||
Err(Error::Failed("Unknown OpenTimelineIO root element".to_string()))
|
||||
Err(Error::Failed(
|
||||
"Unknown OpenTimelineIO root element".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,9 @@ impl TaskBehavior for ProjectSaveTask {
|
||||
|
||||
if using_filename.is_empty() {
|
||||
task.set_error("Project has no filename to save to.");
|
||||
return Err(Error::Failed("Project has no filename to save to.".to_string()));
|
||||
return Err(Error::Failed(
|
||||
"Project has no filename to save to.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;
|
||||
@@ -89,7 +91,10 @@ impl TaskBehavior for ProjectSaveTask {
|
||||
task.set_error("Failed to write XML data.");
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_FILE_ERROR => {
|
||||
task.set_error(&format!("Failed to open file for writing: {}", buf_to_string(&details)));
|
||||
task.set_error(&format!(
|
||||
"Failed to open file for writing: {}",
|
||||
buf_to_string(&details)
|
||||
));
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR => {
|
||||
task.set_error(&format!(
|
||||
@@ -112,7 +117,8 @@ impl TaskBehavior for ProjectSaveTask {
|
||||
|
||||
/// Two-stage read of the project's own filename (empty when unset).
|
||||
pub(crate) fn project_filename(project: CHandle) -> String {
|
||||
let needed = unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) };
|
||||
let needed =
|
||||
unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) };
|
||||
if needed <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
use oakotio::{
|
||||
Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, SerializableCollection, TimeRange,
|
||||
Timeline, Track, Transition,
|
||||
Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable,
|
||||
SerializableCollection, TimeRange, Timeline, Track, Transition,
|
||||
};
|
||||
|
||||
use crate::bridge;
|
||||
@@ -62,14 +62,17 @@ impl SaveOTIOTask {
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline)
|
||||
fn serialize_timeline(sequence: CHandle) -> Option<Timeline> {
|
||||
let mut otio_timeline = Timeline::new(node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(sequence) }));
|
||||
let mut otio_timeline = Timeline::new(node_label_of(unsafe {
|
||||
bridge::node::oaknode_sequence_as_node(sequence)
|
||||
}));
|
||||
|
||||
let mut rate = 0.0f64;
|
||||
{
|
||||
let mut num = 0;
|
||||
let mut den = 1;
|
||||
let mut vp = CHandle::null();
|
||||
if unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut vp) } == 0 {
|
||||
if unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut vp) } == 0
|
||||
{
|
||||
unsafe {
|
||||
bridge::common::oakcommon_videoparams_get_frame_rate(vp, &mut num, &mut den);
|
||||
}
|
||||
@@ -90,8 +93,16 @@ impl SaveOTIOTask {
|
||||
let mut video_list = CHandle::null();
|
||||
let mut audio_list = CHandle::null();
|
||||
unsafe {
|
||||
bridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_VIDEO, &mut video_list);
|
||||
bridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_AUDIO, &mut audio_list);
|
||||
bridge::node::oaknode_sequence_get_track_list(
|
||||
sequence,
|
||||
bridge::node::OAKNODE_TRACK_TYPE_VIDEO,
|
||||
&mut video_list,
|
||||
);
|
||||
bridge::node::oaknode_sequence_get_track_list(
|
||||
sequence,
|
||||
bridge::node::OAKNODE_TRACK_TYPE_AUDIO,
|
||||
&mut audio_list,
|
||||
);
|
||||
}
|
||||
|
||||
if !Self::serialize_track_list(video_list, &mut otio_timeline, rate)
|
||||
@@ -107,7 +118,11 @@ impl SaveOTIOTask {
|
||||
/// timeline. Each track is padded to the list's maximum track length.
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)
|
||||
fn serialize_track_list(list: CHandle, otio_timeline: &mut Timeline, sequence_rate: f64) -> bool {
|
||||
fn serialize_track_list(
|
||||
list: CHandle,
|
||||
otio_timeline: &mut Timeline,
|
||||
sequence_rate: f64,
|
||||
) -> bool {
|
||||
if list.ctx.is_null() {
|
||||
return true;
|
||||
}
|
||||
@@ -141,11 +156,14 @@ impl SaveOTIOTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length) else {
|
||||
let Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
otio_timeline.tracks_mut().append_child(Composable::Track(otio_track));
|
||||
otio_timeline
|
||||
.tracks_mut()
|
||||
.append_child(Composable::Track(otio_track));
|
||||
}
|
||||
|
||||
true
|
||||
@@ -156,7 +174,11 @@ impl SaveOTIOTask {
|
||||
/// track is shorter than `max_track_length`.
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)
|
||||
fn serialize_track(track: CHandle, sequence_rate: f64, max_track_length: Rational) -> Option<Track> {
|
||||
fn serialize_track(
|
||||
track: CHandle,
|
||||
sequence_rate: f64,
|
||||
max_track_length: Rational,
|
||||
) -> Option<Track> {
|
||||
let mut track_type = bridge::node::OAKNODE_TRACK_TYPE_NONE;
|
||||
unsafe {
|
||||
bridge::node::oaknode_track_get_type(track, &mut track_type);
|
||||
@@ -193,7 +215,9 @@ impl SaveOTIOTask {
|
||||
|
||||
let otio_block: Option<Composable> = match kind {
|
||||
bridge::node::OAKNODE_BLOCK_CLIP => {
|
||||
let mut otio_clip = Clip::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));
|
||||
let mut otio_clip = Clip::new(node_label_of(unsafe {
|
||||
bridge::node::oaknode_block_as_node(block)
|
||||
}));
|
||||
|
||||
otio_clip.set_source_range(TimeRange::new(
|
||||
RationalTime::from_rational(block_in_of(block), sequence_rate),
|
||||
@@ -202,10 +226,15 @@ impl SaveOTIOTask {
|
||||
|
||||
let mut media = CHandle::null();
|
||||
unsafe {
|
||||
bridge::node::oaknode_node_find_input_footage(bridge::node::oaknode_block_as_node(block), &mut media);
|
||||
bridge::node::oaknode_node_find_input_footage(
|
||||
bridge::node::oaknode_block_as_node(block),
|
||||
&mut media,
|
||||
);
|
||||
}
|
||||
if !media.ctx.is_null() {
|
||||
let available_range = if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {
|
||||
let available_range = if track_type
|
||||
== bridge::node::OAKNODE_TRACK_TYPE_VIDEO
|
||||
{
|
||||
// OTIO ExternalReference uses the source clips
|
||||
// frame rate (or sample rate) as opposed to the
|
||||
// sequences rate.
|
||||
@@ -214,16 +243,23 @@ impl SaveOTIOTask {
|
||||
let mut num = 0;
|
||||
let mut den = 1;
|
||||
let mut vp = CHandle::null();
|
||||
if unsafe { bridge::node::oaknode_footage_get_video_params(media, 0, &mut vp) } == 0 {
|
||||
if unsafe {
|
||||
bridge::node::oaknode_footage_get_video_params(media, 0, &mut vp)
|
||||
} == 0
|
||||
{
|
||||
unsafe {
|
||||
bridge::common::oakcommon_videoparams_get_frame_rate(vp, &mut num, &mut den);
|
||||
bridge::common::oakcommon_videoparams_get_frame_rate(
|
||||
vp, &mut num, &mut den,
|
||||
);
|
||||
}
|
||||
if den != 0 {
|
||||
source_frame_rate = num as f64 / den as f64;
|
||||
}
|
||||
let mut dur = 0i64;
|
||||
unsafe {
|
||||
bridge::common::oakcommon_videoparams_get_duration(vp, &mut dur);
|
||||
bridge::common::oakcommon_videoparams_get_duration(
|
||||
vp, &mut dur,
|
||||
);
|
||||
}
|
||||
duration = dur as f64;
|
||||
if !vp.ctx.is_null() {
|
||||
@@ -237,15 +273,17 @@ impl SaveOTIOTask {
|
||||
RationalTime::new(duration, source_frame_rate),
|
||||
)
|
||||
} else {
|
||||
TimeRange::new(RationalTime::new(0.0, 48000.0), RationalTime::new(0.0, 48000.0))
|
||||
TimeRange::new(
|
||||
RationalTime::new(0.0, 48000.0),
|
||||
RationalTime::new(0.0, 48000.0),
|
||||
)
|
||||
};
|
||||
|
||||
let media_url = footage_filename(media);
|
||||
if !media_url.is_empty() {
|
||||
otio_clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(
|
||||
media_url,
|
||||
Some(available_range),
|
||||
)));
|
||||
otio_clip.set_media_reference(MediaReference::ExternalReference(
|
||||
ExternalReference::new(media_url, Some(available_range)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,12 +297,20 @@ impl SaveOTIOTask {
|
||||
node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }),
|
||||
))),
|
||||
bridge::node::OAKNODE_BLOCK_TRANSITION => {
|
||||
let mut otio_transition = Transition::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));
|
||||
let mut otio_transition = Transition::new(node_label_of(unsafe {
|
||||
bridge::node::oaknode_block_as_node(block)
|
||||
}));
|
||||
|
||||
let (n, d) = transition_offset_of(block, true);
|
||||
otio_transition.set_in_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));
|
||||
otio_transition.set_in_offset(RationalTime::from_rational(
|
||||
Rational::new(n as i64, d as i64),
|
||||
24.0,
|
||||
));
|
||||
let (n, d) = transition_offset_of(block, false);
|
||||
otio_transition.set_out_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));
|
||||
otio_transition.set_out_offset(RationalTime::from_rational(
|
||||
Rational::new(n as i64, d as i64),
|
||||
24.0,
|
||||
));
|
||||
|
||||
Some(Composable::Transition(otio_transition))
|
||||
}
|
||||
@@ -287,7 +333,10 @@ impl SaveOTIOTask {
|
||||
if duration_seconds < max_track_length.to_f64() {
|
||||
let time_left = max_track_length.to_f64() - duration_seconds;
|
||||
|
||||
let gap = Gap::new(TimeRange::new(duration, RationalTime::new(time_left, 1.0)), "");
|
||||
let gap = Gap::new(
|
||||
TimeRange::new(duration, RationalTime::new(time_left, 1.0)),
|
||||
"",
|
||||
);
|
||||
otio_track.append_child(Composable::Gap(gap));
|
||||
}
|
||||
|
||||
@@ -318,7 +367,9 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
let root = unsafe { bridge::node::oaknode_project_root(self.project) };
|
||||
if root.ctx.is_null() {
|
||||
task.set_error("Project contains no sequences to export.");
|
||||
return Err(Error::Failed("Project contains no sequences to export.".to_string()));
|
||||
return Err(Error::Failed(
|
||||
"Project contains no sequences to export.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut sequences: Vec<CHandle> = Vec::new();
|
||||
@@ -337,7 +388,9 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
|
||||
if sequences.is_empty() {
|
||||
task.set_error("Project contains no sequences to export.");
|
||||
return Err(Error::Failed("Project contains no sequences to export.".to_string()));
|
||||
return Err(Error::Failed(
|
||||
"Project contains no sequences to export.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut serialized: Vec<Timeline> = Vec::with_capacity(sequences.len());
|
||||
@@ -361,7 +414,9 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
let result: std::result::Result<(), String> = match format {
|
||||
InterchangeFormat::OtioJson => {
|
||||
if serialized.len() == 1 {
|
||||
serialized[0].to_json_file(&self.filename).map_err(|e| e.to_string())
|
||||
serialized[0]
|
||||
.to_json_file(&self.filename)
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
// Serialize all into a SerializableCollection.
|
||||
let collection = SerializableCollection::new(
|
||||
@@ -373,13 +428,20 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
InterchangeFormat::Fcpxml => oakotio::to_fcpxml_file(&serialized, &self.filename).map_err(|e| e.to_string()),
|
||||
InterchangeFormat::Fcpxml => {
|
||||
oakotio::to_fcpxml_file(&serialized, &self.filename).map_err(|e| e.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
result.map_err(|e| {
|
||||
let (label, what) = match format {
|
||||
InterchangeFormat::OtioJson => ("Failed to save OpenTimelineIO to file", "Failed to save OpenTimelineIO"),
|
||||
InterchangeFormat::Fcpxml => ("Failed to save FCPXML to file", "Failed to save FCPXML"),
|
||||
InterchangeFormat::OtioJson => (
|
||||
"Failed to save OpenTimelineIO to file",
|
||||
"Failed to save OpenTimelineIO",
|
||||
),
|
||||
InterchangeFormat::Fcpxml => {
|
||||
("Failed to save FCPXML to file", "Failed to save FCPXML")
|
||||
}
|
||||
};
|
||||
task.set_error(&format!("{label} \"{}\": {e}", self.filename));
|
||||
Error::Failed(what.to_string())
|
||||
@@ -461,7 +523,8 @@ fn transition_offset_of(block: CHandle, in_offset: bool) -> (i32, i32) {
|
||||
|
||||
/// Two-stage read of a footage's filename (empty when unset).
|
||||
fn footage_filename(footage: CHandle) -> String {
|
||||
let needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
|
||||
let needed =
|
||||
unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
|
||||
if needed <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
@@ -478,9 +541,16 @@ fn footage_filename(footage: CHandle) -> String {
|
||||
fn track_duration(track: &Track) -> RationalTime {
|
||||
let mut acc: Option<RationalTime> = None;
|
||||
for child in track.children() {
|
||||
let duration = match child.as_clip().and_then(|c| c.source_range()).map(|r| r.duration())
|
||||
.or_else(|| child.as_gap().and_then(|g| g.source_range()).map(|r| r.duration()))
|
||||
{
|
||||
let duration = match child
|
||||
.as_clip()
|
||||
.and_then(|c| c.source_range())
|
||||
.map(|r| r.duration())
|
||||
.or_else(|| {
|
||||
child
|
||||
.as_gap()
|
||||
.and_then(|g| g.source_range())
|
||||
.map(|r| r.duration())
|
||||
}) {
|
||||
Some(d) => d,
|
||||
// Transitions occupy no time.
|
||||
None => RationalTime::new(0.0, 1.0),
|
||||
|
||||
Reference in New Issue
Block a user