- 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)
842 lines
28 KiB
Rust
842 lines
28 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
|
// Copyright (C) 2026 Oak Team
|
|
//
|
|
// 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/>.
|
|
|
|
//! Smoke tests for the timeline family (`engine/include/oakengine/timeline.h`).
|
|
//!
|
|
//! The facade owns a process-wide undo stack, so every test that pushes
|
|
//! undoable commands (sequence creation, tracks, markers, workarea, clip
|
|
//! editing) is serialized inside the single `timeline_lifecycle` test; the
|
|
//! failure-path test only exercises non-mutating NULL-handle calls and runs
|
|
//! in parallel.
|
|
//!
|
|
//! Clips are placed through `oaknode::ffi` directly (the module clip has no
|
|
//! `buffer_in` input, so `oakengine_sequence_add_footage_clip` cannot
|
|
//! connect a footage node; that test is `#[ignore]`d with the reason).
|
|
|
|
#[path = "common/mod.rs"]
|
|
mod common;
|
|
|
|
use std::ffi::{c_char, c_int};
|
|
|
|
use oakengine::handle::{box_handle, OakEngineNode};
|
|
use oakengine::node::{
|
|
oakengine_footage_borrow, oakengine_project_create, oakengine_project_free,
|
|
oakengine_project_new,
|
|
};
|
|
use oakengine::timeline::{
|
|
oakengine_block_get_range, oakengine_block_get_track, oakengine_block_is_enabled,
|
|
oakengine_block_is_gap, oakengine_block_link_count, oakengine_block_next, oakengine_block_prev,
|
|
oakengine_block_set_enabled, oakengine_block_set_length_and_media_out,
|
|
oakengine_clip_are_linked, oakengine_clip_get_range, oakengine_clip_get_sequence,
|
|
oakengine_clip_is_enabled, oakengine_clip_toggle_enabled, oakengine_clip_trim,
|
|
oakengine_marker_commit_time, oakengine_marker_get_color, oakengine_marker_get_name,
|
|
oakengine_marker_get_time, oakengine_marker_has_sibling_at_time, oakengine_marker_list_add,
|
|
oakengine_marker_list_at, oakengine_marker_list_count, oakengine_marker_list_marker_at_time,
|
|
oakengine_marker_remove, oakengine_marker_set_time_command, oakengine_marker_set_time_live,
|
|
oakengine_multicam_switch_source, oakengine_node_is_block, oakengine_node_is_transition,
|
|
oakengine_sequence_add_default_nodes, oakengine_sequence_add_footage_clip,
|
|
oakengine_sequence_add_track, oakengine_sequence_clip_at, oakengine_sequence_clip_count,
|
|
oakengine_sequence_delete_empty_tracks, oakengine_sequence_get_audio_params,
|
|
oakengine_sequence_get_frame_rate, oakengine_sequence_get_length,
|
|
oakengine_sequence_get_length_rational, oakengine_sequence_get_playhead,
|
|
oakengine_sequence_get_playhead_seconds, oakengine_sequence_get_preview_divider,
|
|
oakengine_sequence_get_video_auto_cache, oakengine_sequence_get_workarea,
|
|
oakengine_sequence_last_error, oakengine_sequence_marker_add, oakengine_sequence_marker_add_ex,
|
|
oakengine_sequence_marker_at, oakengine_sequence_marker_count,
|
|
oakengine_sequence_marker_remove, oakengine_sequence_marker_remove_many,
|
|
oakengine_sequence_marker_rename, oakengine_sequence_move_clip, oakengine_sequence_name,
|
|
oakengine_sequence_new, oakengine_sequence_remove_track, oakengine_sequence_ripple_delete_clip,
|
|
oakengine_sequence_ripple_delete_range, oakengine_sequence_set_audio_params,
|
|
oakengine_sequence_set_playhead, oakengine_sequence_set_workarea,
|
|
oakengine_sequence_split_clip, oakengine_sequence_track_at, oakengine_sequence_track_count,
|
|
oakengine_sequence_track_list, oakengine_sequence_trim_clips_to,
|
|
oakengine_sequence_workarea_is_enabled, oakengine_track_block_at, oakengine_track_block_count,
|
|
oakengine_track_get_height, oakengine_track_get_length, oakengine_track_is_locked,
|
|
oakengine_track_is_muted, oakengine_track_is_range_free, oakengine_track_set_height,
|
|
oakengine_track_set_locked, oakengine_track_set_muted, oakengine_track_type,
|
|
oakengine_track_visible_block_at_time, oakengine_workarea_create, oakengine_workarea_free,
|
|
oakengine_workarea_get, oakengine_workarea_reset_in_out, oakengine_workarea_set_enabled,
|
|
oakengine_workarea_set_enabled_undoable, oakengine_workarea_set_range,
|
|
oakengine_workarea_set_range_undoable,
|
|
};
|
|
use oakengine::undo::{oakengine_undo_index, oakengine_undo_jump};
|
|
|
|
/// Read a NUL-terminated facade string into a Rust String.
|
|
unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
|
std::ffi::CStr::from_ptr(buf.as_ptr())
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|
|
|
|
/// Force the runtime-dlsym'd symbols into the link: the oaknode module
|
|
/// resolves `oakcommon_videoparams_*` and `oakundo_command_init` at runtime
|
|
/// with `dlsym(RTLD_DEFAULT)`, and nothing references those codegen units at
|
|
/// link time unless named here (the node.rs family force-links
|
|
/// `oakundo_command_init` for the same reason).
|
|
fn force_runtime_syms() -> usize {
|
|
let fns: [usize; 8] = [
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_get_width as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_get_height as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_get_channel_count as *const () as usize,
|
|
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate as *const () as usize,
|
|
oakundo::ffi::command::oakundo_command_init as *const () as usize,
|
|
];
|
|
fns.iter().sum()
|
|
}
|
|
|
|
/// Convert a facade `CHandle` to the layout-identical oaknode `CHandle`
|
|
/// (distinct Rust types over the same C ABI struct).
|
|
fn to_node_handle(h: oakengine::handle::CHandle) -> oaknode::handle::CHandle {
|
|
oaknode::handle::CHandle {
|
|
ctx: h.ctx,
|
|
addref: h.addref,
|
|
release: h.release,
|
|
abi_version: h.abi_version,
|
|
}
|
|
}
|
|
|
|
/// Convert an oaknode `CHandle` back to the facade `CHandle`.
|
|
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakengine::handle::CHandle {
|
|
oakengine::handle::CHandle {
|
|
ctx: h.ctx,
|
|
addref: h.addref,
|
|
release: h.release,
|
|
abi_version: h.abi_version,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Serialized stack-mutating test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Sequence creation, tracks, playhead, markers, workarea and clip editing —
|
|
/// all in ONE test because the facade's undo stack is process-wide (the
|
|
/// same serialization the undo/node families use).
|
|
#[test]
|
|
fn timeline_lifecycle() {
|
|
common::force_link();
|
|
let _ = force_runtime_syms();
|
|
|
|
// ---- project + sequence creation ----------------------------------
|
|
let project = oakengine_project_create();
|
|
assert!(!project.is_null());
|
|
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
|
|
|
// NULL project -> NULL.
|
|
assert!(unsafe { oakengine_sequence_new(std::ptr::null_mut(), c"x".as_ptr()) }.is_null());
|
|
|
|
let seq = unsafe { oakengine_sequence_new(project, c"Test Sequence".as_ptr()) };
|
|
assert!(!seq.is_null());
|
|
|
|
let mut buf = [0 as c_char; 256];
|
|
let len = unsafe { oakengine_sequence_name(seq, buf.as_mut_ptr(), 256) };
|
|
assert_eq!(len, 13);
|
|
assert_eq!(unsafe { read_buf(&mut buf) }, "Test Sequence");
|
|
|
|
// Fresh sequence: zero length, default frame rate.
|
|
let mut seconds = -1.0;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_length(seq, &mut seconds) },
|
|
0
|
|
);
|
|
assert_eq!(seconds, 0.0);
|
|
let mut num = -1;
|
|
let mut den = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_length_rational(seq, &mut num, &mut den) },
|
|
0
|
|
);
|
|
assert_eq!((num, den), (0, 1));
|
|
let mut fps_num = 0;
|
|
let mut fps_den = 0;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_frame_rate(seq, &mut fps_num, &mut fps_den) },
|
|
0
|
|
);
|
|
assert_eq!((fps_num, fps_den), (30, 1));
|
|
|
|
// ---- tracks ---------------------------------------------------------
|
|
let idx = unsafe { oakengine_sequence_add_track(seq, 0) };
|
|
assert_eq!(idx, 0);
|
|
let mut video = -1;
|
|
let mut audio = -1;
|
|
let mut subtitle = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
|
0
|
|
);
|
|
assert_eq!(video, 1);
|
|
assert_eq!(audio, 0);
|
|
assert_eq!(subtitle, 0);
|
|
|
|
let track = unsafe { oakengine_sequence_track_at(seq, 0, 0) };
|
|
assert!(!track.is_null());
|
|
assert_eq!(unsafe { oakengine_track_type(track) }, 0); // video
|
|
assert!(unsafe { oakengine_sequence_track_at(seq, 0, 5) }.is_null());
|
|
assert!(!unsafe { oakengine_sequence_track_list(seq, 0) }.is_null());
|
|
assert!(unsafe { oakengine_sequence_track_list(seq, 99) }.is_null());
|
|
|
|
// Track height / mute / lock (NOT undoable, straight setters).
|
|
let mut h = 0.0;
|
|
assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0);
|
|
assert!((h - 3.0).abs() < 1e-9);
|
|
assert_eq!(unsafe { oakengine_track_set_height(seq, 0, 0, 5.0) }, 0);
|
|
assert_eq!(unsafe { oakengine_track_get_height(seq, 0, 0, &mut h) }, 0);
|
|
assert!((h - 5.0).abs() < 1e-9);
|
|
assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 0);
|
|
assert_eq!(unsafe { oakengine_track_set_muted(seq, 0, 0, 1) }, 0);
|
|
assert_eq!(unsafe { oakengine_track_is_muted(seq, 0, 0) }, 1);
|
|
assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 0);
|
|
assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 1) }, 0);
|
|
assert_eq!(unsafe { oakengine_track_is_locked(seq, 0, 0) }, 1);
|
|
assert_eq!(unsafe { oakengine_track_set_locked(seq, 0, 0, 0) }, 0);
|
|
|
|
// Track length (empty -> 0) and free-range query.
|
|
let mut tlen = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_track_get_length(seq, 0, 0, &mut tlen) },
|
|
0
|
|
);
|
|
assert_eq!(tlen, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_track_is_range_free(seq, 0, 0, 0, 30) },
|
|
1
|
|
);
|
|
// Bad track index -> E_NOT_FOUND (-4).
|
|
assert_eq!(
|
|
unsafe { oakengine_track_get_length(seq, 0, 99, &mut tlen) },
|
|
-4
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_track_is_range_free(seq, 0, 99, 0, 30) },
|
|
-4
|
|
);
|
|
|
|
// ---- playhead ---------------------------------------------------------
|
|
assert_eq!(unsafe { oakengine_sequence_set_playhead(seq, 90) }, 0);
|
|
let mut ph = -1;
|
|
assert_eq!(unsafe { oakengine_sequence_get_playhead(seq, &mut ph) }, 0);
|
|
assert_eq!(ph, 90);
|
|
let mut phs = 0.0;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_playhead_seconds(seq, &mut phs) },
|
|
0
|
|
);
|
|
assert!((phs - 3.0).abs() < 1e-6);
|
|
|
|
// ---- audio params (round-trip through oakcore) -------------------------
|
|
let mut arate: c_int = 0;
|
|
let mut alayout: u64 = 0;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) },
|
|
0
|
|
);
|
|
assert!(arate > 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_audio_params(seq, &mut arate, &mut alayout) },
|
|
0
|
|
);
|
|
assert_eq!(arate, 48000);
|
|
assert_eq!(alayout, 0x3);
|
|
// A no-op change (same values) is a success without a new command.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_audio_params(seq, 48000, 0x3, 1) },
|
|
0
|
|
);
|
|
// NULL handle -> E_INVALID.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_audio_params(std::ptr::null_mut(), 48000, 0x3, 1) },
|
|
-1
|
|
);
|
|
|
|
// ---- markers -----------------------------------------------------------
|
|
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_add(seq, 30, c"One".as_ptr()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_add_ex(seq, 60, c"Two".as_ptr(), 2) },
|
|
0
|
|
);
|
|
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 2);
|
|
|
|
let mut mtime = -1;
|
|
let mut mcolor = -1;
|
|
let mut mname = [0 as c_char; 64];
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_sequence_marker_at(seq, 0, &mut mtime, mname.as_mut_ptr(), 64, &mut mcolor)
|
|
},
|
|
0
|
|
);
|
|
assert_eq!(mtime, 30);
|
|
assert_eq!(mcolor, 0);
|
|
assert_eq!(unsafe { read_buf(&mut mname) }, "One");
|
|
|
|
// A duplicate time is rejected with E_STATE.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_add(seq, 30, c"dup".as_ptr()) },
|
|
-2
|
|
);
|
|
|
|
// Rename, then remove many at once.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_rename(seq, 30, c"Renamed".as_ptr()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_remove_many(seq, [30i64, 60].as_ptr(), 2) },
|
|
2
|
|
);
|
|
assert_eq!(unsafe { oakengine_sequence_marker_count(seq) }, 0);
|
|
// Removing a nonexistent time -> E_NOT_FOUND (-4).
|
|
assert_eq!(unsafe { oakengine_sequence_marker_remove(seq, 999) }, -4);
|
|
|
|
// ---- workarea ------------------------------------------------------------
|
|
assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_workarea(seq, 1, 0, 300) },
|
|
0
|
|
);
|
|
assert_eq!(unsafe { oakengine_sequence_workarea_is_enabled(seq) }, 1);
|
|
let mut wa_in = -1;
|
|
let mut wa_out = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_workarea(seq, &mut wa_in, &mut wa_out) },
|
|
0
|
|
);
|
|
assert_eq!((wa_in, wa_out), (0, 300));
|
|
|
|
// Reset sentinels: in = 0/1, out = RATIONAL_MAX/1.
|
|
let mut rn = -1;
|
|
let mut rd = -1;
|
|
let mut ron = -1;
|
|
let mut rod = -1;
|
|
unsafe { oakengine_workarea_reset_in_out(&mut rn, &mut rd, &mut ron, &mut rod) };
|
|
assert_eq!((rn, rd), (0, 1));
|
|
assert_eq!(ron, i32::MAX as i64);
|
|
assert_eq!(rod, 1);
|
|
|
|
// Standalone workarea round-trip.
|
|
let wa = oakengine_workarea_create();
|
|
assert!(!wa.is_null());
|
|
assert_eq!(unsafe { oakengine_workarea_set_range(wa, 10, 1, 20, 1) }, 0);
|
|
assert_eq!(unsafe { oakengine_workarea_set_enabled(wa, 1) }, 0);
|
|
let mut wn0 = -1;
|
|
let mut wd0 = -1;
|
|
let mut wn1 = -1;
|
|
let mut wd1 = -1;
|
|
let mut wen = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_workarea_get(wa, &mut wn0, &mut wd0, &mut wn1, &mut wd1, &mut wen) },
|
|
0
|
|
);
|
|
assert_eq!((wn0, wd0, wn1, wd1, wen), (10, 1, 20, 1, 1));
|
|
// Undoable variant on the standalone handle (pushed, not added to a parent).
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_workarea_set_range_undoable(
|
|
wa,
|
|
30,
|
|
1,
|
|
40,
|
|
1,
|
|
10,
|
|
1,
|
|
20,
|
|
1,
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_workarea_set_enabled_undoable(wa, 0, std::ptr::null_mut()) },
|
|
0
|
|
);
|
|
unsafe { oakengine_workarea_free(wa) };
|
|
|
|
// ---- clip editing (blocks placed through oaknode::ffi directly) --------
|
|
// The module clip has no `buffer_in` input, so footage clips cannot be
|
|
// placed; a raw clip node is appended to the track instead.
|
|
let track_module = to_node_handle(unsafe { (*track).handle });
|
|
let clip = unsafe { oaknode::ffi::block::oaknode_block_clip_create() };
|
|
assert!(!clip.ctx.is_null());
|
|
unsafe { oaknode::ffi::block::oaknode_block_set_in(clip.clone(), 0, 1) };
|
|
unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip.clone(), 1, 1) };
|
|
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip.clone(), 0, 1) };
|
|
assert_eq!(
|
|
unsafe {
|
|
oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip.clone())
|
|
},
|
|
0
|
|
);
|
|
|
|
assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 1);
|
|
let mut clip_at = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
|
assert!(!clip_at.is_null());
|
|
let mut cin = -1;
|
|
let mut cout = -1;
|
|
let mut cmi = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_get_range(clip_at, &mut cin, &mut cout, &mut cmi) },
|
|
0
|
|
);
|
|
assert_eq!((cin, cout, cmi), (0, 30, 0));
|
|
|
|
// Generic block traversal.
|
|
assert_eq!(unsafe { oakengine_track_block_count(track) }, 1);
|
|
let blk = unsafe { oakengine_track_block_at(track, 0) };
|
|
assert!(!blk.is_null());
|
|
assert_eq!(unsafe { oakengine_block_is_gap(blk) }, 0);
|
|
let mut bin = -1;
|
|
let mut bout = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) },
|
|
0
|
|
);
|
|
assert_eq!((bin, bout), (0, 30));
|
|
assert!(!unsafe { oakengine_block_get_track(blk) }.is_null());
|
|
assert!(unsafe { oakengine_block_next(blk) }.is_null());
|
|
assert!(unsafe { oakengine_block_prev(blk) }.is_null());
|
|
assert_eq!(unsafe { oakengine_block_link_count(blk) }, 0);
|
|
|
|
// Clip enable toggling (undoable, one command).
|
|
assert_eq!(unsafe { oakengine_clip_toggle_enabled(&mut clip_at, 1) }, 1);
|
|
assert_eq!(unsafe { oakengine_clip_is_enabled(clip_at) }, 0);
|
|
assert_eq!(unsafe { oakengine_block_set_enabled(blk, 1) }, 0);
|
|
assert_eq!(unsafe { oakengine_block_is_enabled(blk) }, 1);
|
|
|
|
// Block resize (undoable): length 30 -> 40, in stays.
|
|
assert_eq!(
|
|
unsafe { oakengine_block_set_length_and_media_out(blk, 40) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_block_get_range(blk, &mut bin, &mut bout) },
|
|
0
|
|
);
|
|
assert_eq!((bin, bout), (0, 40));
|
|
|
|
// Split at 20 -> two clips (the module's split lists the right half
|
|
// first: clip0 = [20, 40)).
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 20) },
|
|
0
|
|
);
|
|
assert_eq!(unsafe { oakengine_sequence_clip_count(seq, 0, 0) }, 2);
|
|
// Split outside the clip -> E_INVALID and a last-error.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_split_clip(seq, 0, 0, 0, 100) },
|
|
-1
|
|
);
|
|
let mut err = [0 as c_char; 256];
|
|
let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) };
|
|
assert!(
|
|
elen > 0,
|
|
"last_error must be non-empty after a failed split"
|
|
);
|
|
|
|
// Trim the first clip [20, 40) to [25, 35).
|
|
let clip0 = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
|
assert!(!clip0.is_null());
|
|
assert_eq!(unsafe { oakengine_clip_trim(clip0, 25, 35) }, 0);
|
|
let mut cin2 = -1;
|
|
let mut cout2 = -1;
|
|
let mut cmi2 = -1;
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_get_range(clip0, &mut cin2, &mut cout2, &mut cmi2) },
|
|
0
|
|
);
|
|
assert_eq!((cin2, cout2), (25, 35));
|
|
|
|
// Trim clips to a point on every unlocked track (edge 0 = in).
|
|
assert!(unsafe { oakengine_sequence_trim_clips_to(seq, 0, 25) } >= 0);
|
|
|
|
// Move the clip: a time-only move within the addressed track (the capi's
|
|
// gap+place assembly, one undoable entry). clip0 = [25, 35) -> [50, 60)
|
|
// (length and media-in kept), fully reversible.
|
|
let base_move = unsafe { oakengine_undo_index() };
|
|
assert_eq!(unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, 50) }, 0);
|
|
let mut mv_in = -1i64;
|
|
let mut mv_out = -1i64;
|
|
let mut mv_mi = -1i64;
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
|
0
|
|
);
|
|
assert_eq!((mv_in, mv_out, mv_mi), (50, 60, 25));
|
|
assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
|
0
|
|
);
|
|
assert_eq!((mv_in, mv_out), (25, 35));
|
|
assert_eq!(unsafe { oakengine_undo_jump(base_move + 1) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_get_range(clip0, &mut mv_in, &mut mv_out, &mut mv_mi) },
|
|
0
|
|
);
|
|
assert_eq!((mv_in, mv_out), (50, 60));
|
|
// Back to the pre-move state (clip0 = [25, 35)) for the ripple deletes
|
|
// below.
|
|
assert_eq!(unsafe { oakengine_undo_jump(base_move) }, 0);
|
|
// Invalid arguments -> E_INVALID; nonexistent clip -> E_NOT_FOUND.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_move_clip(seq, 0, 0, 0, -1) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_move_clip(seq, 0, 9, 0, 50) },
|
|
-4
|
|
);
|
|
|
|
// Ripple delete the addressed clip.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_ripple_delete_clip(seq, 0, 0, 0) },
|
|
0
|
|
);
|
|
|
|
// Ripple delete a range on every track.
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_ripple_delete_range(seq, 0, 10) },
|
|
0
|
|
);
|
|
|
|
// Linked clips: two fresh clips linked then unlinked.
|
|
let clip_b = unsafe { oaknode::ffi::block::oaknode_block_clip_create() };
|
|
unsafe { oaknode::ffi::block::oaknode_block_set_in(clip_b.clone(), 0, 1) };
|
|
unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_in(clip_b.clone(), 1, 3) };
|
|
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip_b.clone(), 0, 1) };
|
|
assert_eq!(
|
|
unsafe {
|
|
oaknode::ffi::track::oaknode_track_append_block(track_module.clone(), clip_b.clone())
|
|
},
|
|
0
|
|
);
|
|
let clip_b_engine = unsafe { oakengine_sequence_clip_at(seq, 0, 0, 0) };
|
|
assert!(!clip_b_engine.is_null());
|
|
let mut clips = [clip_b_engine, clip_at];
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_toggle_enabled(clips.as_mut_ptr(), 2) },
|
|
2
|
|
);
|
|
assert_eq!(unsafe { oakengine_clip_are_linked(clips[0], clips[1]) }, 0);
|
|
|
|
// ---- default nodes + track removal ----------------------------------
|
|
// Add one video + one audio track as one command.
|
|
assert_eq!(unsafe { oakengine_sequence_add_default_nodes(seq) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
|
0
|
|
);
|
|
assert_eq!(video, 2);
|
|
assert_eq!(audio, 1);
|
|
|
|
// Remove the audio track.
|
|
assert_eq!(unsafe { oakengine_sequence_remove_track(seq, 1, 0) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_track_count(seq, &mut video, &mut audio, &mut subtitle) },
|
|
0
|
|
);
|
|
assert_eq!(audio, 0);
|
|
|
|
// Delete empty tracks (the extra video track is empty).
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_delete_empty_tracks(seq, -1) },
|
|
1
|
|
);
|
|
|
|
// ---- cleanup ----------------------------------------------------------
|
|
unsafe { oakengine_project_free(project) };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Non-mutating failure paths (no undo-stack access; run in parallel)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// NULL handles yield the header's documented values/codes.
|
|
#[test]
|
|
fn timeline_failure_paths() {
|
|
common::force_link();
|
|
|
|
// NULL sequence.
|
|
let mut buf = [0 as c_char; 64];
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_name(std::ptr::null(), buf.as_mut_ptr(), 64) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_add_track(std::ptr::null_mut(), 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_count(std::ptr::null()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_marker_add(std::ptr::null_mut(), 0, c"x".as_ptr()) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_workarea_is_enabled(std::ptr::null()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_preview_divider(std::ptr::null()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_get_video_auto_cache(std::ptr::null()) },
|
|
0
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_playhead(std::ptr::null_mut(), 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_set_workarea(std::ptr::null_mut(), 1, 0, 10) },
|
|
-1
|
|
);
|
|
assert!(unsafe { oakengine_sequence_track_at(std::ptr::null(), 0, 0) }.is_null());
|
|
assert!(unsafe { oakengine_sequence_track_list(std::ptr::null_mut(), 0) }.is_null());
|
|
assert!(unsafe { oakengine_sequence_clip_at(std::ptr::null_mut(), 0, 0, 0) }.is_null());
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_clip_count(std::ptr::null_mut(), 0, 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_ripple_delete_clip(std::ptr::null_mut(), 0, 0, 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_ripple_delete_range(std::ptr::null_mut(), 0, 10) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_remove_track(std::ptr::null_mut(), 0, 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_sequence_add_default_nodes(std::ptr::null_mut()) },
|
|
-1
|
|
);
|
|
|
|
// NULL clip / block handles.
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_clip_get_range(
|
|
std::ptr::null(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_trim(std::ptr::null_mut(), 0, 10) },
|
|
-1
|
|
);
|
|
assert_eq!(unsafe { oakengine_clip_is_enabled(std::ptr::null()) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_clip_are_linked(std::ptr::null(), std::ptr::null()) },
|
|
0
|
|
);
|
|
assert!(unsafe { oakengine_clip_get_sequence(std::ptr::null()) }.is_null());
|
|
assert_eq!(unsafe { oakengine_track_block_count(std::ptr::null()) }, -1);
|
|
assert!(unsafe { oakengine_track_block_at(std::ptr::null(), 0) }.is_null());
|
|
assert!(unsafe { oakengine_track_visible_block_at_time(std::ptr::null_mut(), 0) }.is_null());
|
|
assert!(unsafe { oakengine_block_get_track(std::ptr::null()) }.is_null());
|
|
assert!(unsafe { oakengine_block_next(std::ptr::null()) }.is_null());
|
|
assert!(unsafe { oakengine_block_prev(std::ptr::null()) }.is_null());
|
|
assert_eq!(unsafe { oakengine_block_is_gap(std::ptr::null()) }, 0);
|
|
assert_eq!(unsafe { oakengine_block_is_enabled(std::ptr::null()) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_block_set_enabled(std::ptr::null_mut(), 1) },
|
|
-1
|
|
);
|
|
assert_eq!(unsafe { oakengine_block_link_count(std::ptr::null()) }, 0);
|
|
assert_eq!(
|
|
unsafe { oakengine_block_set_length_and_media_out(std::ptr::null_mut(), 10) },
|
|
-1
|
|
);
|
|
assert_eq!(unsafe { oakengine_track_type(std::ptr::null()) }, -1);
|
|
assert_eq!(unsafe { oakengine_node_is_block(std::ptr::null()) }, 0);
|
|
assert_eq!(unsafe { oakengine_node_is_transition(std::ptr::null()) }, 0);
|
|
|
|
// Marker handle family NULL paths.
|
|
assert_eq!(unsafe { oakengine_marker_list_count(std::ptr::null()) }, 0);
|
|
assert!(unsafe { oakengine_marker_list_at(std::ptr::null(), 0) }.is_null());
|
|
assert!(unsafe { oakengine_marker_list_marker_at_time(std::ptr::null(), 1, 1) }.is_null());
|
|
assert_eq!(
|
|
unsafe { oakengine_marker_list_add(std::ptr::null_mut(), 0, 1, 0, 1, c"x".as_ptr(), 0) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_marker_get_time(
|
|
std::ptr::null(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_marker_get_name(std::ptr::null(), std::ptr::null_mut(), 0) },
|
|
-1
|
|
);
|
|
assert_eq!(unsafe { oakengine_marker_get_color(std::ptr::null()) }, -1);
|
|
assert_eq!(
|
|
unsafe { oakengine_marker_has_sibling_at_time(std::ptr::null(), 1, 1) },
|
|
0
|
|
);
|
|
assert_eq!(unsafe { oakengine_marker_remove(std::ptr::null_mut()) }, -1);
|
|
assert!(unsafe { oakengine_marker_set_time_command(std::ptr::null_mut(), 1, 1) }.is_null());
|
|
assert_eq!(
|
|
unsafe { oakengine_marker_set_time_live(std::ptr::null_mut(), 0, 1, 0, 1) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_marker_commit_time(
|
|
std::ptr::null_mut(),
|
|
0,
|
|
1,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
|
|
// Workarea handle NULL paths.
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_workarea_get(
|
|
std::ptr::null(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_workarea_set_range(std::ptr::null_mut(), 0, 1, 1, 1) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe { oakengine_workarea_set_enabled(std::ptr::null_mut(), 1) },
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_workarea_set_range_undoable(
|
|
std::ptr::null_mut(),
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
0,
|
|
1,
|
|
0,
|
|
1,
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_workarea_set_enabled_undoable(std::ptr::null_mut(), 1, std::ptr::null_mut())
|
|
},
|
|
-1
|
|
);
|
|
|
|
// Multicam NULL path.
|
|
assert_eq!(
|
|
unsafe {
|
|
oakengine_multicam_switch_source(
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
0,
|
|
0,
|
|
0.0,
|
|
std::ptr::null_mut(),
|
|
)
|
|
},
|
|
-1
|
|
);
|
|
}
|
|
|
|
/// Footage clips need the footage node connected to the clip's `buffer_in`
|
|
/// input, which module clips do not declare; the placement is expected to
|
|
/// fail with a non-empty last error.
|
|
#[test]
|
|
#[ignore = "module clips have no buffer input; footage clips cannot be placed"]
|
|
fn footage_clip_placement() {
|
|
common::force_link();
|
|
|
|
let project = oakengine_project_create();
|
|
assert!(!project.is_null());
|
|
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
|
let seq = unsafe { oakengine_sequence_new(project, c"FC".as_ptr()) };
|
|
assert!(!seq.is_null());
|
|
assert_eq!(unsafe { oakengine_sequence_add_track(seq, 0) }, 0);
|
|
|
|
// A footage node created through oaknode::ffi directly (no media needed).
|
|
let footage = unsafe {
|
|
oaknode::ffi::footage::oaknode_footage_create(
|
|
to_node_handle((*project).handle),
|
|
c"/no/such/media.mp4".as_ptr(),
|
|
)
|
|
};
|
|
assert!(!footage.ctx.is_null());
|
|
let node_box = unsafe { box_handle::<OakEngineNode>(to_facade_handle(footage)) };
|
|
let footage_handle = unsafe { oakengine_footage_borrow(node_box) };
|
|
assert!(!footage_handle.is_null());
|
|
|
|
let clip = unsafe { oakengine_sequence_add_footage_clip(seq, footage_handle, 0, 0, 0, 30, 0) };
|
|
assert!(clip.is_null());
|
|
let mut err = [0 as c_char; 256];
|
|
let elen = unsafe { oakengine_sequence_last_error(err.as_mut_ptr(), 256) };
|
|
assert!(
|
|
elen > 0,
|
|
"last_error must explain the failed footage placement"
|
|
);
|
|
|
|
unsafe { oakengine_project_free(project) };
|
|
}
|