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:
2026-08-11 23:04:48 +08:00
parent a209f63d52
commit 18ff60f147
351 changed files with 32880 additions and 15939 deletions
+106 -25
View File
@@ -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),
}
+8 -2
View File
@@ -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()
}
}
+69 -19
View File
@@ -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(),
))
}
}
}
+9 -3
View File
@@ -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();
}
+105 -35
View File
@@ -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),