Files
oak-editor/crates/oakengine/src/timeline.rs
T
Mike-Solar 92daff1a83 feat(engine,app): timeline markers, work area, cross-track move (M12 P4)
- facade: oakengine_sequence_set_workarea_undoable (enable+range as one
  undo record); marker add/remove/list already existed and are now
  covered by it_timeline (10 new tests: markers, workarea, cross-track)
- fixes: stubs workarea_get honored NULL out-params (is_enabled always
  failed); export tasks honor custom ranges (export_params_pod dropped
  them; oaktask EncodingParams carries has_custom_range)
- app: markers on the ruler (gpui diamond markers), menu
  sequence-add/remove marker, set/clear work area (selected-clip bounds
  or playhead+1), ruler drag previews live and commits one undoable
  record (C++ ruler semantics); export uses the work area when enabled
- cross-track clip moves were already wired; now covered end to end
2026-08-16 16:06:19 +08:00

6467 lines
183 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/>.
//! `engine/include/oakengine/timeline.h` — sequences, tracks, clips,
//! markers and the workarea over the oaknode + oaktimeline modules.
//!
//! The engine's frame timestamps (frame numbers in the sequence's
//! frame-rate timebase) convert to the modules' rational seconds through
//! the sequence's video parameters, mirroring the C++ capi
//! (`engine/src/capi/timeline.cpp`): the time base is the frame rate
//! flipped. Undoable mutations assemble the module's command creators and
//! push them through [`crate::undo::push_or_run`].
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
use oakundo::undocommand::OakUndoCommandVtable;
use crate::stubs::common as c;
use crate::stubs::node as n;
use crate::stubs::timeline as tl;
use crate::stubs::audio as a;
use oakundo::undocommand as u;
use crate::error::{Error, Result};
use crate::handle::{
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, read_cstr, string_result, unbox,
write_string, CHandle, OakEngineBlock, OakEngineClip, OakEngineClipboard, OakEngineFootage,
OakEngineMarker, OakEngineMarkerList, OakEngineNode, OakEngineProject, OakEngineSequence,
OakEngineTrack, OakEngineTrackList, OakEngineWorkarea,
};
use crate::undo::push_or_run;
/// `engine/include/oakengine/timeline.h` — POD mirror of
/// `oakengine_ripple_info` (one TrackListRippleToolCommand hash entry).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakEngineRippleInfo {
/// The track to ripple (borrowed engine handle).
pub track: *mut OakEngineTrack,
/// The block being moved (borrowed engine handle).
pub block: *mut OakEngineBlock,
/// Whether a gap should be appended after it (1/0).
pub append_gap: c_int,
}
// ---- helpers ---------------------------------------------------------------
/// Track types (`OAKENGINE_TRACK_TYPE_*`).
const TRACK_TYPE_VIDEO: c_int = 0;
const TRACK_TYPE_AUDIO: c_int = 1;
const TRACK_TYPE_SUBTITLE: c_int = 2;
/// Movement modes (`OAKENGINE_MOVEMENT_MODE_*`).
const MOVEMENT_MODE_TRIM_IN: c_int = 2;
const MOVEMENT_MODE_TRIM_OUT: c_int = 3;
/// `oaknode/block.h` block kinds.
const BLOCK_KIND_CLIP: c_int = 1;
const BLOCK_KIND_GAP: c_int = 2;
/// Node type id of transition blocks.
const TYPE_ID_TRANSITION: &str = "org.olivevideoeditor.Olive.transitionblock";
/// Track height constants (C++ `Track::k_track_height_*`).
const TRACK_HEIGHT_DEFAULT: f64 = 3.0;
const TRACK_HEIGHT_MINIMUM: f64 = 1.5;
const TRACK_HEIGHT_INTERVAL: f64 = 0.5;
/// Default font height in pixels (C++ `Track::default_font_height`).
const TRACK_FONT_HEIGHT: f64 = 13.0;
// The last editing error for this thread (mirrors the capi's
// `thread_local QString g_seq_last_error`, timeline.cpp:126).
thread_local! {
static SEQ_LAST_ERROR: RefCell<String> = RefCell::new(String::new());
}
/// Record `msg` as the last editing error of this thread.
fn set_seq_error(msg: &str) {
SEQ_LAST_ERROR.with(|e| *e.borrow_mut() = msg.to_string());
}
/// Release a module handle reference (the handle's own `release`). Every
/// module "borrowed" handle returned by an out-parameter or
/// `*_as_node`/`*_of` creator is an owned copy with refcount 1, so the
/// facade releases it after temporary use.
fn release_handle(h: CHandle) {
if let Some(release) = h.release {
unsafe { release(h.ctx) };
}
}
/// Greatest common divisor (1 when both are zero).
fn gcd(a: i64, b: i64) -> i64 {
let (mut a, mut b) = (a.abs(), b.abs());
while b != 0 {
let t = b;
b = a % b;
a = t;
}
if a == 0 {
1
} else {
a
}
}
/// The sequence's frame duration (frame rate flipped) as a (num, den)
/// pair. Mirrors the capi's `time_base_of`; `E_STATE` when the sequence
/// has no valid frame rate.
///
/// # Safety
/// `seq` must be a live module sequence handle.
unsafe fn seq_time_base(seq: CHandle) -> Result<(i64, i64)> {
unsafe {
let mut params = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut params);
if rc != 0 || params.is_null() {
return Err(Error::State);
}
let mut num: c_int = 0;
let mut den: c_int = 0;
let fr = c::oakcommon_videoparams_get_frame_rate(params, &mut num, &mut den);
let mut h = params;
c::oakcommon_videoparams_free(&mut h);
if fr != 0 || num <= 0 || den <= 0 {
return Err(Error::State);
}
Ok((den as i64, num as i64))
}
}
/// Rational seconds -> timestamp in timebase units, rounding half away
/// from zero (mirrors `Timecode::k_round`).
fn rational_to_ts(num: i64, den: i64, tb: (i64, i64)) -> i64 {
if den == 0 || tb.0 == 0 || tb.1 == 0 {
return 0;
}
let n = num as i128 * tb.1 as i128;
let d = den as i128 * tb.0 as i128;
let q = n / d;
let r = n % d;
let rr = if r < 0 { -r } else { r };
let dd = if d < 0 { -d } else { d };
if rr * 2 >= dd {
(q + if n < 0 { -1 } else { 1 }) as i64
} else {
q as i64
}
}
/// Timestamp -> reduced rational seconds (`time = ts * tb`).
fn ts_to_rational(ts: i64, tb: (i64, i64)) -> (i64, i64) {
let num = ts as i128 * tb.0 as i128;
let den = tb.1 as i128;
let g = gcd((num % den) as i64, den as i64);
((num / g as i128) as i64, (den / g as i128) as i64)
}
/// Reduced rational sum.
fn rat_add(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> (i64, i64) {
let num = a_num * b_den + b_num * a_den;
let den = a_den * b_den;
let g = gcd(num, den);
(num / g, den / g)
}
/// Reduced rational difference (`a - b`).
fn rat_sub(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> (i64, i64) {
let num = a_num * b_den - b_num * a_den;
let den = a_den * b_den;
let g = gcd(num, den);
(num / g, den / g)
}
/// Box a module command handle into the engine command shell.
fn command_box(cmd: CHandle) -> Result<*mut OakEngineClipboard> {
if cmd.ctx.is_null() {
return Err(Error::Failed("command creation failed".into()));
}
Ok(box_handle::<OakEngineClipboard>(cmd).cast())
}
/// Push a single module command onto the undo stack (or run it directly).
///
/// # Safety
/// `cmd` must be a live module command handle.
unsafe fn push_command(cmd: CHandle, name: &str) -> Result<()> {
unsafe {
let boxed = command_box(cmd)?;
let name_c =
std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid undo name".into()))?;
push_or_run(boxed, name_c.as_ptr())
}
}
/// Assemble several module command handles into ONE multi command and push
/// it.
///
/// # Safety
/// `children` must hold live module command handles (each is consumed by
/// the multi).
unsafe fn push_multi_commands(children: &[CHandle], name: &str) -> Result<()> {
unsafe {
if children.is_empty() {
return Ok(());
}
let multi = u::command_init_multi();
if multi.is_null() {
return Err(Error::Failed("multi command allocation failed".into()));
}
let multi_box = box_handle::<OakEngineClipboard>(multi);
for child in children {
let rc = u::command_multi_add_child(multi, *child);
if rc != 0 {
free_box(multi_box);
return Err(Error::Module(rc));
}
}
let name_c =
std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid undo name".into()))?;
push_or_run(multi_box, name_c.as_ptr())
}
}
// ---- Facade-owned undo commands -------------------------------------------
//
// The module has no undo commands for sequence video params, block
// enable/disable, clip media-in or block resizing; the facade carries
// read-modify-write equivalents backed by `oakundo_command_init` vtables
// (the same pattern as the capi's facade-level `SequenceVideoParamsCommand`).
/// Generic vtable command factory: box `data` behind an oakundo command.
///
/// # Safety
/// `data` must be a box that `free_fn` can reclaim; `redo`/`undo` must be
/// safe to call with it.
unsafe fn vtable_command(
redo: unsafe extern "C" fn(*mut c_void),
undo: unsafe extern "C" fn(*mut c_void),
free_fn: unsafe extern "C" fn(*mut c_void),
data: *mut c_void,
) -> Result<CHandle> {
unsafe {
let vtable = OakUndoCommandVtable {
redo: Some(redo),
undo: Some(undo),
free_fn: Some(free_fn),
};
let cmd = u::command_init(&vtable, data);
if cmd.is_null() {
free_fn(data);
return Err(Error::Failed("undo command allocation failed".into()));
}
Ok(cmd)
}
}
/// Sequence video-parameter read-modify-write (the capi's
/// `SequenceVideoParamsCommand`).
struct VideoParamsCmdData {
/// Sequence node (addref'd).
seq: CHandle,
/// Old oakcommon videoparams handle.
old_params: CHandle,
/// New oakcommon videoparams handle.
new_params: CHandle,
}
unsafe extern "C" fn video_params_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const VideoParamsCmdData);
n::oaknode_sequence_set_video_params(d.seq, 0, d.new_params);
}
}
unsafe extern "C" fn video_params_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const VideoParamsCmdData);
n::oaknode_sequence_set_video_params(d.seq, 0, d.old_params);
}
}
unsafe extern "C" fn video_params_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut VideoParamsCmdData);
release_handle(d.seq);
release_handle(d.old_params);
release_handle(d.new_params);
}
}
/// Sequence audio-parameter read-modify-write (the capi's
/// `SequenceAudioParamsCommand`). The params are borrowed oakcore
/// `OakAudioParams` handles (raw pointers; release with
/// `oakcore_audioparams_free`).
struct AudioParamsCmdData {
/// Sequence node (addref'd).
seq: CHandle,
/// Old oakcore audio-params handle.
old_params: *mut c_void,
/// New oakcore audio-params handle.
new_params: *mut c_void,
}
unsafe extern "C" fn audio_params_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const AudioParamsCmdData);
n::oaknode_sequence_set_audio_params(d.seq, 0, d.new_params);
}
}
unsafe extern "C" fn audio_params_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const AudioParamsCmdData);
n::oaknode_sequence_set_audio_params(d.seq, 0, d.old_params);
}
}
unsafe extern "C" fn audio_params_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut AudioParamsCmdData);
release_handle(d.seq);
a::oakcore_audioparams_free(d.old_params);
a::oakcore_audioparams_free(d.new_params);
}
}
/// Block enable/disable (the capi's `BlockEnableDisableCommand`).
struct BlockEnabledCmdData {
/// Block (addref'd).
block: CHandle,
/// Enabled value captured at construction.
old_enabled: c_int,
/// New enabled value.
new_enabled: c_int,
}
unsafe extern "C" fn block_enabled_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockEnabledCmdData);
n::oaknode_block_set_enabled(d.block, d.new_enabled);
}
}
unsafe extern "C" fn block_enabled_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockEnabledCmdData);
n::oaknode_block_set_enabled(d.block, d.old_enabled);
}
}
unsafe extern "C" fn block_enabled_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut BlockEnabledCmdData);
release_handle(d.block);
}
}
/// Clip media-in change (the capi's `BlockSetMediaInCommand`).
struct ClipMediaInCmdData {
/// Clip (addref'd).
clip: CHandle,
/// Media-in captured at construction (rational seconds).
old_num: c_int,
old_den: c_int,
/// New media-in (rational seconds).
new_num: c_int,
new_den: c_int,
}
unsafe extern "C" fn clip_media_in_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const ClipMediaInCmdData);
n::oaknode_clip_set_media_in(d.clip, d.new_num, d.new_den);
}
}
unsafe extern "C" fn clip_media_in_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const ClipMediaInCmdData);
n::oaknode_clip_set_media_in(d.clip, d.old_num, d.old_den);
}
}
unsafe extern "C" fn clip_media_in_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut ClipMediaInCmdData);
release_handle(d.clip);
}
}
/// Block length change keeping the in point (the capi's
/// `BlockResizeCommand`).
struct BlockResizeCmdData {
/// Block (addref'd).
block: CHandle,
/// Length captured at construction (rational seconds).
old_num: c_int,
old_den: c_int,
/// New length (rational seconds).
new_num: c_int,
new_den: c_int,
}
// The engine's `set_length_and_media_out` keeps the in-point and extends
// the out (the C++ in/out points derive from the track position); that maps
// to the module's in-anchored `set_length_and_media_in`, despite the name.
unsafe extern "C" fn block_resize_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockResizeCmdData);
n::oaknode_block_set_length_and_media_in(d.block, d.new_num, d.new_den);
}
}
unsafe extern "C" fn block_resize_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockResizeCmdData);
n::oaknode_block_set_length_and_media_in(d.block, d.old_num, d.old_den);
}
}
unsafe extern "C" fn block_resize_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut BlockResizeCmdData);
release_handle(d.block);
}
}
/// Block trim (the capi's `BlockTrimCommand`). The module's own
/// `BlockTrimCommand` applies the length setters with inverted semantics
/// (its trim-in anchors the in-point), so the facade carries the correct
/// engine mapping: a trim-in anchors the OUT (the in moves), a trim-out
/// anchors the IN (the out moves).
struct BlockTrimCmdData {
/// Block (addref'd).
block: CHandle,
/// Trim mode: `MOVEMENT_MODE_TRIM_IN` (out anchored) or
/// `MOVEMENT_MODE_TRIM_OUT` (in anchored).
mode: c_int,
/// Length captured at construction (rational seconds).
old_num: c_int,
old_den: c_int,
/// New length (rational seconds).
new_num: c_int,
new_den: c_int,
}
unsafe extern "C" fn block_trim_redo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockTrimCmdData);
if d.mode == MOVEMENT_MODE_TRIM_IN {
n::oaknode_block_set_length_and_media_out(d.block, d.new_num, d.new_den);
} else {
n::oaknode_block_set_length_and_media_in(d.block, d.new_num, d.new_den);
}
}
}
unsafe extern "C" fn block_trim_undo(ud: *mut c_void) {
unsafe {
let d = &*(ud as *const BlockTrimCmdData);
if d.mode == MOVEMENT_MODE_TRIM_IN {
n::oaknode_block_set_length_and_media_out(d.block, d.old_num, d.old_den);
} else {
n::oaknode_block_set_length_and_media_in(d.block, d.old_num, d.old_den);
}
}
}
unsafe extern "C" fn block_trim_free(ud: *mut c_void) {
unsafe {
let d = Box::from_raw(ud as *mut BlockTrimCmdData);
release_handle(d.block);
}
}
/// Build a block-trim vtable command.
///
/// # Safety
/// `block` must be a live module block handle.
unsafe fn trim_cmd(
block: CHandle,
mode: c_int,
old_num: c_int,
old_den: c_int,
new_num: c_int,
new_den: c_int,
) -> Result<CHandle> {
unsafe {
let data = Box::into_raw(Box::new(BlockTrimCmdData {
block: block.addref(),
mode,
old_num,
old_den,
new_num,
new_den,
})) as *mut c_void;
vtable_command(block_trim_redo, block_trim_undo, block_trim_free, data)
}
}
// ---- Per-sequence marker list / workarea cache -----------------------------
//
// The module's sequences never initialize their own `markers`/`workarea`
// handles (`SequenceBehavior` defaults both to empty), so
// `oaktimeline_marker_list_of`/`workarea_of` return empty handles for
// them. The facade materializes one of each per sequence on first use and
// caches them by the sequence node's stable `ctx` token (facade state,
// like the process-wide undo stack). The cached entries live for the
// process; a sequence reused at the same heap address after a project free
// would find its old list (accepted: the application creates one project).
static SEQ_MARKER_LISTS: Mutex<Option<HashMap<usize, CHandle>>> = Mutex::new(None);
static SEQ_WORKAREAS: Mutex<Option<HashMap<usize, CHandle>>> = Mutex::new(None);
/// The per-sequence marker-list / workarea maps (created lazily).
fn seq_marker_map() -> std::sync::MutexGuard<'static, Option<HashMap<usize, CHandle>>> {
SEQ_MARKER_LISTS.lock().unwrap_or_else(|e| e.into_inner())
}
fn seq_workarea_map() -> std::sync::MutexGuard<'static, Option<HashMap<usize, CHandle>>> {
SEQ_WORKAREAS.lock().unwrap_or_else(|e| e.into_inner())
}
/// The sequence's marker list (addref'd; caller releases).
///
/// # Safety
/// `seq` must be a live module sequence handle.
unsafe fn seq_marker_list(seq: CHandle) -> Result<CHandle> {
unsafe {
let node = n::oaknode_sequence_as_node(seq);
let mut list = tl::oaktimeline_marker_list_of(node);
release_handle(node);
if !list.is_null() {
return Ok(list);
}
let key = seq.ctx as usize;
let mut map = seq_marker_map();
let map = map.get_or_insert_with(HashMap::new);
if let Some(h) = map.get(&key) {
return Ok(h.addref());
}
list = tl::oaktimeline_marker_list_create();
if list.is_null() {
return Err(Error::Failed("marker list allocation failed".into()));
}
let stored = list.addref();
map.insert(key, stored);
Ok(list)
}
}
/// The sequence's workarea (addref'd; caller releases).
///
/// # Safety
/// `seq` must be a live module sequence handle.
unsafe fn seq_workarea(seq: CHandle) -> Result<CHandle> {
unsafe {
let node = n::oaknode_sequence_as_node(seq);
let mut wa = tl::oaktimeline_workarea_of(node);
release_handle(node);
if !wa.is_null() {
return Ok(wa);
}
let key = seq.ctx as usize;
let mut map = seq_workarea_map();
let map = map.get_or_insert_with(HashMap::new);
if let Some(h) = map.get(&key) {
return Ok(h.addref());
}
wa = tl::oaktimeline_workarea_create();
if wa.is_null() {
return Err(Error::Failed("workarea allocation failed".into()));
}
let stored = wa.addref();
map.insert(key, stored);
Ok(wa)
}
}
// ---- Marker handle boxes ---------------------------------------------------
//
// The module exposes markers only through list operations
// (`oaktimeline_marker_at`, `*_command`, ...); there is no standalone
// marker handle. The facade represents an `OakEngineMarker` as a boxed
// (addref'd list, index) pair so the marker-handle family can route back
// into the list.
/// Marker-box ABI magic stamped into the handle's `abi_version` (marker
/// boxes are facade-created; the field distinguishes them from module
/// handles without relying on function-pointer equality).
const MARKER_ABI_MAGIC: u32 = 0x4D41524B; // "MARK"
/// Facade-owned marker reference: the owning list plus the marker index.
struct MarkerBox {
refs: AtomicU32,
list: CHandle,
index: c_int,
}
unsafe extern "C" fn marker_box_addref(ptr: *mut c_void) {
unsafe {
if ptr.is_null() {
return;
}
let rb = &*(ptr as *const MarkerBox);
rb.refs.fetch_add(1, Ordering::SeqCst);
}
}
unsafe extern "C" fn marker_box_release(ptr: *mut c_void) {
unsafe {
if ptr.is_null() {
return;
}
let rb = ptr as *mut MarkerBox;
let prev = (*rb).refs.fetch_sub(1, Ordering::SeqCst);
if prev == 1 {
let list = (*rb).list;
release_handle(list);
drop(Box::from_raw(rb));
}
}
}
/// Box an (addref'd `list`, `index`) pair as a borrowed marker handle.
fn box_marker(list: CHandle, index: c_int) -> *mut OakEngineMarker {
let boxed = Box::new(MarkerBox {
refs: AtomicU32::new(1),
list,
index,
});
let ch = CHandle {
ctx: Box::into_raw(boxed) as *mut c_void,
addref: Some(marker_box_addref),
release: Some(marker_box_release),
abi_version: MARKER_ABI_MAGIC,
};
box_handle::<OakEngineMarker>(ch)
}
/// Unpack a marker handle into its (list, index).
///
/// # Safety
/// `m` must be a marker handle created by [`box_marker`] (or NULL).
unsafe fn marker_unbox(m: *const OakEngineMarker) -> Result<(CHandle, c_int)> {
unsafe {
let ch = unbox::<OakEngineMarker>(m)?;
if ch.abi_version != MARKER_ABI_MAGIC {
return Err(Error::Invalid);
}
let rb = &*(ch.ctx as *const MarkerBox);
Ok((rb.list, rb.index))
}
}
/// The clip at (track_index, clip_index) within the track list, skipping
/// gap blocks; empty handle when out of range (the capi's
/// `clip_at_index`).
///
/// # Safety
/// `list` must be a live module track-list handle.
unsafe fn clip_at_index(list: CHandle, track_index: c_int, clip_index: c_int) -> CHandle {
unsafe {
let mut track_count: c_int = 0;
if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0
|| track_index < 0
|| track_index >= track_count
{
return CHandle::null();
}
let mut track = CHandle::null();
if n::oaknode_tracklist_get_track_at(list, track_index, &mut track) != 0 || track.is_null()
{
return CHandle::null();
}
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(track, &mut block_count) != 0 {
release_handle(track);
return CHandle::null();
}
let mut seen: c_int = 0;
for i in 0..block_count {
let mut block = CHandle::null();
if n::oaknode_track_get_block_at(track, i, &mut block) != 0 || block.is_null() {
continue;
}
let mut kind: c_int = 0;
let rc = n::oaknode_block_get_kind(block, &mut kind);
if rc != 0 {
release_handle(block);
continue;
}
if kind == BLOCK_KIND_CLIP {
if seen == clip_index {
release_handle(track);
return block;
}
seen += 1;
}
release_handle(block);
}
release_handle(track);
CHandle::null()
}
}
/// Index of the first marker whose in-point equals `(num, den)`, or -1.
///
/// # Safety
/// `list` must be a live module marker-list handle.
unsafe fn marker_index_at(list: CHandle, num: i64, den: i64) -> c_int {
unsafe {
let mut count: c_int = 0;
if tl::oaktimeline_marker_count(list, &mut count) != 0 {
return -1;
}
for i in 0..count {
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
i,
&mut in_num,
&mut in_den,
&mut out_num,
&mut out_den,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
continue;
}
if in_num as i64 == num && in_den as i64 == den {
return i;
}
}
-1
}
}
/// Nearest block whose out-point is strictly before `(num, den)` (the
/// capi's `Track::nearest_block_before`; the module exposes only the
/// before-or-at variant, so the last block ordered before the time is
/// located by iteration).
///
/// # Safety
/// `track` must be a live module track handle.
unsafe fn nearest_block_before(track: CHandle, num: i64, den: i64) -> CHandle {
unsafe {
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(track, &mut block_count) != 0 {
return CHandle::null();
}
let mut best = CHandle::null();
for i in 0..block_count {
let mut b = CHandle::null();
if n::oaknode_track_get_block_at(track, i, &mut b) != 0 || b.is_null() {
continue;
}
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
if n::oaknode_block_get_out(b, &mut out_num, &mut out_den) == 0
&& rat_cmp(out_num as i64, out_den as i64, num, den) == std::cmp::Ordering::Less
{
if !best.is_null() {
release_handle(best);
}
best = b;
} else {
release_handle(b);
}
}
best
}
}
/// The module type id of a node (empty when the query fails).
///
/// # Safety
/// `node` must be a live module node handle.
unsafe fn node_type_id(node: CHandle) -> String {
unsafe {
let mut buf = [0 as c_char; 256];
let rc = n::oaknode_node_get_id(node, buf.as_mut_ptr(), buf.len() as c_int);
if rc < 0 {
String::new()
} else {
read_cstr(buf.as_ptr())
}
}
}
// ---- Sequence creation and inspection --------------------------------------
/// `oakengine_sequence_new` — create a sequence named `name` in `project`
/// and return its borrowed handle (NULL on failure; NULL project -> NULL).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_new(
project: *mut OakEngineProject,
name: *const c_char,
) -> *mut OakEngineSequence {
guard_ptr(|| unsafe {
if project.is_null() {
return Ok(std::ptr::null_mut());
}
let ph = unbox(project)?;
let root = n::oaknode_project_root(ph);
if root.is_null() {
release_handle(root);
return Ok(std::ptr::null_mut());
}
release_handle(root);
let seq = n::oaknode_sequence_create();
if seq.is_null() {
return Ok(std::ptr::null_mut());
}
let rc = n::oaknode_sequence_set_default_parameters(seq);
if rc != 0 {
return Err(Error::Module(rc));
}
let label = read_cstr(name);
let label_c =
std::ffi::CString::new(label).map_err(|_| Error::Failed("invalid name".into()))?;
let seq_node = n::oaknode_sequence_as_node(seq);
let rc = n::oaknode_node_set_label(seq_node, label_c.as_ptr());
release_handle(seq_node);
if rc != 0 {
return Err(Error::Module(rc));
}
// NOTE (documented deviation): the module's whole-subgraph transfer
// (`oaknode_project_add_node` -> `graph::transfer_all`) moves the node
// entries but does NOT remap the node ids held inside behaviors, so a
// sequence moved into a project keeps track lists that point at the
// wrong ids and a track-list `sequence` back-reference that points at
// the root folder. The sequence is therefore kept in its own scratch
// project (all track/marker/workarea queries stay functional there);
// project membership and the undoable creation are not established in
// the module world.
Ok(box_handle::<OakEngineSequence>(seq))
})
}
/// `oakengine_sequence_name` — sequence label (buf/size convention).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_name(
self_: *const OakEngineSequence,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
let h = unbox(self_)?;
let node = n::oaknode_sequence_as_node(h);
let rc = n::oaknode_node_get_label(node, buf, buf_size);
release_handle(node);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(string_result(rc))
}
})
}
/// `oakengine_sequence_get_length` — content length in seconds (0 for an
/// empty sequence).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_length(
self_: *const OakEngineSequence,
seconds: *mut f64,
) -> c_int {
guard(|| unsafe {
if seconds.is_null() {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let mut num: c_int = 0;
let mut den: c_int = 0;
Error::from_module(n::oaknode_sequence_get_length(h, &mut num, &mut den))?;
*seconds = num as f64 / den as f64;
Ok(())
})
}
/// `oakengine_sequence_get_length_rational` — content length as rational
/// seconds.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_length_rational(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut n: c_int = 0;
let mut d: c_int = 0;
Error::from_module(n::oaknode_sequence_get_length(h, &mut n, &mut d))?;
if !num.is_null() {
*num = n;
}
if !den.is_null() {
*den = d;
}
Ok(())
})
}
/// `oakengine_sequence_get_frame_rate` — frame rate num/den pair.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_frame_rate(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut params = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params);
if rc != 0 || params.is_null() {
return Err(Error::State);
}
let mut n: c_int = 0;
let mut d: c_int = 0;
let fr = c::oakcommon_videoparams_get_frame_rate(params, &mut n, &mut d);
let mut hh = params;
c::oakcommon_videoparams_free(&mut hh);
if fr != 0 || n <= 0 || d <= 0 {
return Err(Error::State);
}
if !num.is_null() {
*num = n;
}
if !den.is_null() {
*den = d;
}
Ok(())
})
}
/// `oakengine_sequence_get_video_params` — dimensions and pixel aspect
/// ratio (any output pointer may be NULL).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_video_params(
self_: *const OakEngineSequence,
width: *mut c_int,
height: *mut c_int,
par_num: *mut c_int,
par_den: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut params = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params);
if rc != 0 || params.is_null() {
return Err(Error::Failed("sequence has no video params".into()));
}
if !width.is_null() {
Error::from_module(c::oakcommon_videoparams_get_width(params, width))?;
}
if !height.is_null() {
Error::from_module(c::oakcommon_videoparams_get_height(params, height))?;
}
if !par_num.is_null() || !par_den.is_null() {
let mut pn: c_int = 0;
let mut pd: c_int = 0;
Error::from_module(c::oakcommon_videoparams_get_pixel_aspect_ratio(
params, &mut pn, &mut pd,
))?;
if !par_num.is_null() {
*par_num = pn;
}
if !par_den.is_null() {
*par_den = pd;
}
}
let mut hh = params;
c::oakcommon_videoparams_free(&mut hh);
Ok(())
})
}
/// `oakengine_sequence_get_video_params_ex` — full read of the video
/// parameters (any output pointer may be NULL).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_video_params_ex(
self_: *const OakEngineSequence,
width: *mut c_int,
height: *mut c_int,
fps_num: *mut c_int,
fps_den: *mut c_int,
par_num: *mut c_int,
par_den: *mut c_int,
interlacing: *mut c_int,
format: *mut c_int,
divider: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut params = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params);
if rc != 0 || params.is_null() {
return Err(Error::Failed("sequence has no video params".into()));
}
if !width.is_null() {
Error::from_module(c::oakcommon_videoparams_get_width(params, width))?;
}
if !height.is_null() {
Error::from_module(c::oakcommon_videoparams_get_height(params, height))?;
}
if !fps_num.is_null() || !fps_den.is_null() {
let mut n: c_int = 0;
let mut d: c_int = 0;
Error::from_module(c::oakcommon_videoparams_get_frame_rate(
params, &mut n, &mut d,
))?;
if !fps_num.is_null() {
*fps_num = n;
}
if !fps_den.is_null() {
*fps_den = d;
}
}
if !par_num.is_null() || !par_den.is_null() {
let mut pn: c_int = 0;
let mut pd: c_int = 0;
Error::from_module(c::oakcommon_videoparams_get_pixel_aspect_ratio(
params, &mut pn, &mut pd,
))?;
if !par_num.is_null() {
*par_num = pn;
}
if !par_den.is_null() {
*par_den = pd;
}
}
if !interlacing.is_null() {
Error::from_module(c::oakcommon_videoparams_get_interlacing(
params,
interlacing,
))?;
}
if !format.is_null() {
Error::from_module(c::oakcommon_videoparams_get_format(params, format))?;
}
if !divider.is_null() {
Error::from_module(c::oakcommon_videoparams_get_divider(params, divider))?;
}
let mut hh = params;
c::oakcommon_videoparams_free(&mut hh);
Ok(())
})
}
/// `oakengine_sequence_set_video_params` — write the video parameters
/// (`undoable` flag; -1 leaves a field unchanged).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_video_params(
self_: *mut OakEngineSequence,
width: c_int,
height: c_int,
fps_num: c_int,
fps_den: c_int,
par_num: c_int,
par_den: c_int,
interlacing: c_int,
format: c_int,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if self_.is_null() {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
if width < -1 || width == 0 || height < -1 || height == 0 {
set_seq_error(&format!("invalid video size {}x{}", width, height));
return Err(Error::Invalid);
}
if (fps_num == -1) != (fps_den == -1)
|| fps_num < -1
|| fps_num == 0
|| fps_den < -1
|| fps_den == 0
{
set_seq_error(&format!("invalid frame rate {}/{}", fps_num, fps_den));
return Err(Error::Invalid);
}
if (par_num == -1) != (par_den == -1)
|| par_num < -1
|| par_num == 0
|| par_den < -1
|| par_den == 0
{
set_seq_error(&format!("invalid pixel aspect {}/{}", par_num, par_den));
return Err(Error::Invalid);
}
if interlacing < -1 || interlacing > 2 {
set_seq_error(&format!("invalid interlacing {}", interlacing));
return Err(Error::Invalid);
}
if format < -1 || format >= 32 {
set_seq_error(&format!("invalid pixel format {}", format));
return Err(Error::Invalid);
}
let seq = unbox(self_)?;
let mut current = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut current);
if rc != 0 || current.is_null() {
return Err(Error::State);
}
let mut cur_w: c_int = 0;
let mut cur_h: c_int = 0;
let mut cur_fn: c_int = 0;
let mut cur_fd: c_int = 0;
let mut cur_fmt: c_int = 0;
c::oakcommon_videoparams_get_width(current, &mut cur_w);
c::oakcommon_videoparams_get_height(current, &mut cur_h);
c::oakcommon_videoparams_get_frame_rate(current, &mut cur_fn, &mut cur_fd);
c::oakcommon_videoparams_get_format(current, &mut cur_fmt);
// The frame rate is stored directly in the module's model (the capi's
// flipped time base lives inside the C++ VideoParams constructor).
let new = c::oakcommon_videoparams_init();
if new.is_null() {
let mut h = current;
c::oakcommon_videoparams_free(&mut h);
return Err(Error::Failed("video params allocation failed".into()));
}
c::oakcommon_videoparams_set_width(new, if width >= 0 { width } else { cur_w });
c::oakcommon_videoparams_set_height(new, if height >= 0 { height } else { cur_h });
c::oakcommon_videoparams_set_format(new, if format >= 0 { format } else { cur_fmt });
c::oakcommon_videoparams_set_frame_rate(
new,
if fps_num >= 0 { fps_num } else { cur_fn },
if fps_den >= 0 { fps_den } else { cur_fd },
);
let mut equal: c_int = 0;
c::oakcommon_videoparams_equals(new, current, &mut equal);
if equal != 0 {
let mut h1 = current;
let mut h2 = new;
c::oakcommon_videoparams_free(&mut h1);
c::oakcommon_videoparams_free(&mut h2);
return Ok(());
}
if undoable != 0 {
let data = Box::into_raw(Box::new(VideoParamsCmdData {
seq: seq.addref(),
old_params: current,
new_params: new,
}));
let cmd = vtable_command(
video_params_redo,
video_params_undo,
video_params_free,
data as *mut c_void,
)?;
push_command(cmd, "Set Sequence Video Parameters")
} else {
let rc = n::oaknode_sequence_set_video_params(seq, 0, new);
let mut h1 = current;
let mut h2 = new;
c::oakcommon_videoparams_free(&mut h1);
c::oakcommon_videoparams_free(&mut h2);
Error::from_module(rc)
}
})
}
/// `oakengine_sequence_get_audio_params` — sample rate and channel layout.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_audio_params(
self_: *const OakEngineSequence,
sample_rate: *mut c_int,
channel_layout: *mut u64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut out: *mut c_void = std::ptr::null_mut();
Error::from_module(n::oaknode_sequence_get_audio_params(h, 0, &mut out))?;
if out.is_null() {
return Err(Error::Failed("sequence has no audio params".into()));
}
let rate = a::oakcore_audioparams_sample_rate(out);
let layout = a::oakcore_audioparams_channel_layout(out);
// The module created the oakcore handle; release it after reading.
a::oakcore_audioparams_free(out);
if !sample_rate.is_null() {
*sample_rate = rate;
}
if !channel_layout.is_null() {
*channel_layout = layout;
}
Ok(())
})
}
/// `oakengine_sequence_set_audio_params` — write the audio parameters.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_audio_params(
self_: *mut OakEngineSequence,
sample_rate: c_int,
channel_layout: u64,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if self_.is_null() {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
let seq = unbox(self_)?;
// Read the current params; `sample_rate <= 0` / `channel_layout == 0`
// leave the field unchanged (the capi's semantics).
let mut current: *mut c_void = std::ptr::null_mut();
let rc = n::oaknode_sequence_get_audio_params(seq, 0, &mut current);
if rc != 0 || current.is_null() {
return Err(Error::State);
}
let cur_rate = a::oakcore_audioparams_sample_rate(current);
let cur_layout = a::oakcore_audioparams_channel_layout(current);
let cur_format = a::oakcore_audioparams_format(current);
a::oakcore_audioparams_free(current);
let new_rate = if sample_rate <= 0 {
cur_rate
} else {
sample_rate
};
let new_layout = if channel_layout == 0 {
cur_layout
} else {
channel_layout
};
if new_rate == cur_rate && new_layout == cur_layout {
return Ok(());
}
let new = a::oakcore_audioparams_create(new_rate, new_layout, cur_format);
if new.is_null() {
return Err(Error::Failed("audio params allocation failed".into()));
}
if undoable != 0 {
let old = a::oakcore_audioparams_create(cur_rate, cur_layout, cur_format);
if old.is_null() {
a::oakcore_audioparams_free(new);
return Err(Error::Failed("audio params allocation failed".into()));
}
let data = Box::into_raw(Box::new(AudioParamsCmdData {
seq: seq.addref(),
old_params: old,
new_params: new,
}));
let cmd = vtable_command(
audio_params_redo,
audio_params_undo,
audio_params_free,
data as *mut c_void,
)?;
push_command(cmd, "Set Sequence Audio Parameters")
} else {
let r = n::oaknode_sequence_set_audio_params(seq, 0, new);
a::oakcore_audioparams_free(new);
Error::from_module(r)
}
})
}
/// `oakengine_sequence_get_preview_divider` — preview resolution divider
/// (0 on a NULL handle).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_preview_divider(
self_: *const OakEngineSequence,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let mut params = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(h, 0, &mut params);
if rc != 0 || params.is_null() {
return Ok(0);
}
let mut divider: c_int = 0;
c::oakcommon_videoparams_get_divider(params, &mut divider);
let mut hh = params;
c::oakcommon_videoparams_free(&mut hh);
Ok(divider)
})
}
/// `oakengine_sequence_set_preview_divider` — set the preview divider.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_preview_divider(
self_: *mut OakEngineSequence,
divider: c_int,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if self_.is_null() {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
if divider < 1 {
set_seq_error(&format!("invalid preview divider {}", divider));
return Err(Error::Invalid);
}
// The module's `VideoParams` model has no divider field
// (`videoparams_from_handle` drops it), so the write is a no-op for
// the module; the getter always reports 1. The setter still validates
// and mirrors the capi's command/apply shape.
let seq = unbox(self_)?;
let mut current = CHandle::null();
let rc = n::oaknode_sequence_get_video_params(seq, 0, &mut current);
if rc != 0 || current.is_null() {
return Err(Error::State);
}
let mut cur_div: c_int = 0;
c::oakcommon_videoparams_get_divider(current, &mut cur_div);
if cur_div == divider {
let mut h = current;
c::oakcommon_videoparams_free(&mut h);
return Ok(());
}
let mut h = current;
c::oakcommon_videoparams_free(&mut h);
let _ = undoable;
Ok(())
})
}
/// `oakengine_sequence_get_video_auto_cache` — 1 when video auto-cache is
/// enabled (the engine accessor is a stub that reports 0).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_video_auto_cache(
self_: *const OakEngineSequence,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let _ = unbox(self_)?;
Ok(0)
})
}
/// `oakengine_sequence_set_video_auto_cache` — forward to the engine's
/// stub accessor (no undo command; `undoable` accepted and ignored).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_video_auto_cache(
self_: *mut OakEngineSequence,
enabled: c_int,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if self_.is_null() {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
// Stub: the module has no video auto-cache accessor
// (`SequenceBehavior.autocache_video` is not exposed over the C
// ABI). Mirrors the capi forwarding to the engine's stub setter.
let _ = (enabled, undoable);
let _ = unbox(self_)?;
Ok(())
})
}
/// `oakengine_sequence_track_count` — tracks per type (any pointer may be
/// NULL).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_track_count(
self_: *const OakEngineSequence,
video: *mut c_int,
audio: *mut c_int,
subtitle: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
if !video.is_null() {
Error::from_module(n::oaknode_sequence_get_track_count(
h,
TRACK_TYPE_VIDEO,
video,
))?;
}
if !audio.is_null() {
Error::from_module(n::oaknode_sequence_get_track_count(
h,
TRACK_TYPE_AUDIO,
audio,
))?;
}
if !subtitle.is_null() {
Error::from_module(n::oaknode_sequence_get_track_count(
h,
TRACK_TYPE_SUBTITLE,
subtitle,
))?;
}
Ok(())
})
}
/// `oakengine_sequence_get_playhead` — playhead as a frame timestamp.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_playhead(
self_: *const OakEngineSequence,
timestamp: *mut i64,
) -> c_int {
guard(|| unsafe {
if timestamp.is_null() {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let tb = seq_time_base(h)?;
let mut num: c_int = 0;
let mut den: c_int = 0;
Error::from_module(n::oaknode_sequence_get_playhead(h, &mut num, &mut den))?;
*timestamp = rational_to_ts(num as i64, den as i64, tb);
Ok(())
})
}
/// `oakengine_sequence_set_playhead` — move the playhead to `timestamp`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_playhead(
self_: *mut OakEngineSequence,
timestamp: i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let tb = seq_time_base(h)?;
let (num, den) = ts_to_rational(timestamp, tb);
// The module's playhead setter takes c_int rationals; timestamps that
// overflow are truncated (module limitation).
Error::from_module(n::oaknode_sequence_set_playhead(
h,
num as c_int,
den as c_int,
))
})
}
/// `oakengine_sequence_get_playhead_seconds` — playhead in seconds.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_playhead_seconds(
self_: *const OakEngineSequence,
seconds: *mut f64,
) -> c_int {
guard(|| unsafe {
if seconds.is_null() {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let mut num: c_int = 0;
let mut den: c_int = 0;
Error::from_module(n::oaknode_sequence_get_playhead(h, &mut num, &mut den))?;
*seconds = num as f64 / den as f64;
Ok(())
})
}
/// `oakengine_sequence_workarea_is_enabled` — 1 when the workarea is
/// enabled.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_workarea_is_enabled(
self_: *const OakEngineSequence,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let wa = seq_workarea(h)?;
let mut enabled: c_int = 0;
let rc = tl::oaktimeline_workarea_get(
wa,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut enabled,
);
release_handle(wa);
Error::from_module(rc)?;
Ok(enabled)
})
}
/// `oakengine_sequence_get_workarea` — workarea in/out as frame timestamps.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_get_workarea(
self_: *const OakEngineSequence,
in_: *mut i64,
out: *mut i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let tb = seq_time_base(h)?;
let wa = seq_workarea(h)?;
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let rc = tl::oaktimeline_workarea_get(
wa,
&mut in_num,
&mut in_den,
&mut out_num,
&mut out_den,
std::ptr::null_mut(),
);
release_handle(wa);
Error::from_module(rc)?;
if !in_.is_null() {
*in_ = rational_to_ts(in_num as i64, in_den as i64, tb);
}
if !out.is_null() {
*out = rational_to_ts(out_num as i64, out_den as i64, tb);
}
Ok(())
})
}
/// `oakengine_sequence_set_workarea` — enable flag plus in/out timestamps.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_workarea(
self_: *mut OakEngineSequence,
enabled: c_int,
in_: i64,
out: i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let tb = seq_time_base(h)?;
let wa = seq_workarea(h)?;
let rc = tl::oaktimeline_workarea_set_enabled(wa, enabled);
if rc != 0 {
release_handle(wa);
return Err(Error::Module(rc));
}
let (in_num, in_den) = ts_to_rational(in_, tb);
let (out_num, out_den) = ts_to_rational(out, tb);
let rc = tl::oaktimeline_workarea_set_range(
wa,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
);
release_handle(wa);
Error::from_module(rc)
})
}
/// `oakengine_sequence_set_workarea_undoable` — set the workarea's enabled
/// flag and in/out range as ONE undoable entry ("Set Workarea").
///
/// The old range must be supplied by the caller (the same convention as
/// `oakengine_workarea_set_range_undoable`): pass the range read before the
/// change — e.g. the drag-start range of a ruler work-area drag. The enabled
/// flag's previous value is captured by the module command itself.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_set_workarea_undoable(
self_: *mut OakEngineSequence,
enabled: c_int,
in_: i64,
out: i64,
old_in: i64,
old_out: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(self_) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
};
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
if in_ < 0 || out < 0 || old_in < 0 || old_out < 0 {
set_seq_error("invalid workarea range");
return Err(Error::Invalid);
}
let wa = seq_workarea(sequence)?;
let (new_in_num, new_in_den) = ts_to_rational(in_, tb);
let (new_out_num, new_out_den) = ts_to_rational(out, tb);
let (old_in_num, old_in_den) = ts_to_rational(old_in, tb);
let (old_out_num, old_out_den) = ts_to_rational(old_out, tb);
let enabled_cmd = tl::oaktimeline_workarea_set_enabled_command(wa, enabled);
if enabled_cmd.is_null() {
release_handle(wa);
set_seq_error("workarea enabled command failed");
return Err(Error::Failed("workarea enabled command failed".into()));
}
let range_cmd = tl::oaktimeline_workarea_set_range_command(
wa,
new_in_num as c_int,
new_in_den as c_int,
new_out_num as c_int,
new_out_den as c_int,
old_in_num as c_int,
old_in_den as c_int,
old_out_num as c_int,
old_out_den as c_int,
);
release_handle(wa);
if range_cmd.is_null() {
set_seq_error("workarea range command failed");
return Err(Error::Failed("workarea range command failed".into()));
}
// The commands hold borrowed clones of `wa` (no addref); the workarea
// lives with the sequence, so it outlives the undo entry.
push_multi_commands(&[enabled_cmd, range_cmd], "Set Workarea")
})
}
/// `oakengine_sequence_marker_count` — number of timeline markers.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_count(self_: *const OakEngineSequence) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let list = seq_marker_list(h)?;
let mut count: c_int = 0;
let rc = tl::oaktimeline_marker_count(list, &mut count);
release_handle(list);
Error::from_module(rc)?;
Ok(count)
})
}
/// `oakengine_sequence_marker_at` — marker at `index` (time as a frame
/// timestamp, name via the buf/size convention, color index).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_at(
self_: *const OakEngineSequence,
index: c_int,
time: *mut i64,
name: *mut c_char,
name_size: c_int,
color: *mut c_int,
) -> c_int {
guard(|| unsafe {
if self_.is_null() || index < 0 {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let tb = seq_time_base(h)?;
let list = seq_marker_list(h)?;
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let mut marker_color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
index,
&mut in_num,
&mut in_den,
&mut out_num,
&mut out_den,
&mut marker_color,
name,
name_size,
);
release_handle(list);
if rc < 0 {
return Err(Error::Module(rc));
}
if !time.is_null() {
*time = rational_to_ts(in_num as i64, in_den as i64, tb);
}
if !color.is_null() {
*color = marker_color;
}
Ok(())
})
}
/* ---- Timeline editing primitives ----------------------------------------- */
/// `oakengine_sequence_last_error` — last editing error for this thread.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
SEQ_LAST_ERROR.with(|e| unsafe { write_string(&e.borrow(), buf, buf_size) })
}
/// `oakengine_sequence_add_track` — append a track and return its index.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_track(
self_: *mut OakEngineSequence,
track_type: c_int,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
let seq = unbox(self_)?;
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
seq, track_type, &mut list,
))?;
// The module's TimelineAddTrackCommand redo appends the created
// track to the list (with its back-reference and index), so the
// count/at queries observe it directly — no separate registration
// needed (the old module redo only grew the sequence's track-input
// array element, which is why the previous facade registered a
// second live track as compensation).
let cmd = tl::oaktimeline_add_track_command(list);
if cmd.is_null() {
release_handle(list);
return Err(Error::Failed("add track command failed".into()));
}
push_command(cmd, "Add Track")?;
let mut count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_track_count(
seq, track_type, &mut count,
))?;
release_handle(list);
Ok(count - 1)
})
}
/// `oakengine_sequence_add_track_command` — create a TimelineAddTrackCommand
/// without pushing it.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_track_command(
self_: *mut OakEngineSequence,
track_type: c_int,
auto_merge: c_int,
out_track: *mut *mut OakEngineTrack,
) -> *mut c_void {
guard_ptr(|| unsafe {
if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Ok(std::ptr::null_mut());
}
let seq = unbox(self_)?;
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
seq, track_type, &mut list,
))?;
let cmd = tl::oaktimeline_add_track_command(list);
if cmd.is_null() {
release_handle(list);
return Ok(std::ptr::null_mut());
}
// The module command carries its own internal track (inaccessible
// over the C ABI); `out_track` receives a live-created track the
// caller can hand to the list, mirroring the capi's
// `command->track()`. `auto_merge` is accepted and ignored (the
// module command hardcodes automerge off).
let _ = auto_merge;
if !out_track.is_null() {
let track = n::oaknode_track_create(track_type);
if track.is_null() {
release_handle(list);
return Ok(std::ptr::null_mut());
}
Error::from_module(n::oaknode_tracklist_add_track(list, track))?;
*out_track = box_handle::<OakEngineTrack>(track);
}
release_handle(list);
Ok(command_box(cmd)?.cast())
})
}
/// `oakengine_sequence_ripple_tracks_command` — create a
/// TrackListRippleToolCommand.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_ripple_tracks_command(
self_: *mut OakEngineSequence,
track_type: c_int,
infos: *const OakEngineRippleInfo,
info_count: c_int,
movement_num: i64,
movement_den: i64,
movement_mode: c_int,
) -> *mut c_void {
guard_ptr(|| {
if self_.is_null()
|| infos.is_null()
|| info_count <= 0
|| movement_den == 0
|| track_type < TRACK_TYPE_VIDEO
|| track_type > TRACK_TYPE_SUBTITLE
|| movement_mode < 0
|| movement_mode > MOVEMENT_MODE_TRIM_OUT
{
return Ok(std::ptr::null_mut());
}
// Stub: the oaktimeline module implements TrackListRippleToolCommand
// internally (undoripple.rs) but exposes no C creator for it; the
// facade bridge accordingly has no `oaktimeline_ripple_tracks_command`.
let _ = (
self_,
track_type,
infos,
info_count,
movement_num,
movement_den,
movement_mode,
);
Ok(std::ptr::null_mut())
})
}
/// `oakengine_sequence_add_footage_clip` — place a clip of `footage` on a
/// track (undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_footage_clip(
seq: *mut OakEngineSequence,
footage: *mut OakEngineFootage,
track_type: c_int,
track_index: c_int,
in_: i64,
out: i64,
media_in: i64,
) -> *mut OakEngineClip {
guard_ptr(|| unsafe {
add_footage_clip_impl(seq, footage, track_type, track_index, in_, out, media_in, true)
})
}
/// `oakengine_sequence_add_footage_clip_ex` — like
/// `oakengine_sequence_add_footage_clip`, but skips the same-project
/// check (M12 P0).
///
/// Sequences created through `oakengine_sequence_new` live in their own
/// scratch project (documented deviation: the module's whole-subgraph
/// transfer cannot remap the ids held inside behaviors, so project
/// membership is not established in the module world). The plain export
/// therefore rejects every footage clip as "different projects". The
/// `_ex` variant accepts the module reality: the check is unenforceable
/// and only the handle validity is required.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_footage_clip_ex(
seq: *mut OakEngineSequence,
footage: *mut OakEngineFootage,
track_type: c_int,
track_index: c_int,
in_: i64,
out: i64,
media_in: i64,
) -> *mut OakEngineClip {
guard_ptr(|| unsafe {
add_footage_clip_impl(seq, footage, track_type, track_index, in_, out, media_in, false)
})
}
/// Shared implementation of the footage-clip placement.
unsafe fn add_footage_clip_impl(
seq: *mut OakEngineSequence,
footage: *mut OakEngineFootage,
track_type: c_int,
track_index: c_int,
in_: i64,
out: i64,
media_in: i64,
strict_project: bool,
) -> Result<*mut OakEngineClip> {
unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence handle");
return Ok(std::ptr::null_mut());
}
};
let footage_h = match unbox(footage) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid footage handle");
return Ok(std::ptr::null_mut());
}
};
if track_type != TRACK_TYPE_VIDEO && track_type != TRACK_TYPE_AUDIO {
set_seq_error("clips are only supported on video and audio tracks");
return Ok(std::ptr::null_mut());
}
// Same-project check. The `_ex` variant skips it: sequences
// created through `oakengine_sequence_new` live in their own
// scratch project (documented deviation), so the strict check
// can never pass and only handle validity is enforceable.
let mut seq_project = CHandle::null();
let seq_node = n::oaknode_sequence_as_node(sequence);
let rc = n::oaknode_node_get_project(seq_node, &mut seq_project);
let mut foot_project = CHandle::null();
let rc2 = n::oaknode_node_get_project(footage_h, &mut foot_project);
release_handle(seq_node);
if strict_project
&& (rc != 0
|| rc2 != 0
|| seq_project.is_null()
|| foot_project.is_null()
|| seq_project.ctx != foot_project.ctx)
{
release_handle(seq_project);
release_handle(foot_project);
set_seq_error("footage and sequence belong to different projects");
return Ok(std::ptr::null_mut());
}
release_handle(seq_project);
release_handle(foot_project);
if in_ < 0 || out <= in_ || media_in < 0 {
set_seq_error("invalid clip range (need 0 <= in < out and media_in >= 0)");
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Ok(std::ptr::null_mut());
}
};
let mut list = CHandle::null();
if n::oaknode_sequence_get_track_list(sequence, track_type, &mut list) != 0
|| list.is_null()
{
set_seq_error("sequence has no track list for this type");
return Ok(std::ptr::null_mut());
}
let mut track_count: c_int = 0;
if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0
|| track_index < 0
|| track_index >= track_count
{
release_handle(list);
set_seq_error(&format!(
"track index {} out of range ({} tracks)",
track_index, track_count
));
return Ok(std::ptr::null_mut());
}
let (in_num, in_den) = ts_to_rational(in_, tb);
let (out_num, out_den) = ts_to_rational(out, tb);
let (media_num, media_den) = ts_to_rational(media_in, tb);
// The application's drop-import chain reduced to its editing core.
let clip = n::oaknode_block_clip_create();
if clip.is_null() {
release_handle(list);
set_seq_error("clip creation failed");
return Ok(std::ptr::null_mut());
}
let (len_num, len_den) = rat_sub(out_num, out_den, in_num, in_den);
Error::from_module(n::oaknode_clip_set_media_in(
clip,
media_num as c_int,
media_den as c_int,
))?;
Error::from_module(n::oaknode_block_set_length_and_media_in(
clip,
len_num as c_int,
len_den as c_int,
))?;
// The `_ex` variant connects a footage node created directly in
// the sequence's scratch project (graph edges cannot cross
// projects; the real-project footage node stays untouched for
// the project browser). The module clip declares its texture
// input as `tex_in` (the C++-era `buffer_in` name does not exist
// here); the graph's connect validates the input side only, so
// the footage edge builds even though the footage node has no
// declared output.
//
// Order matters: the place command ADOPTS the block into the
// sequence's project (remapping the block handle's node id), so
// the footage edge is created only after the block is placed —
// the edge needs the clip's final (project, id).
let scratch_footage = if !strict_project {
let scratch = seq_project_of(sequence);
let mut out = CHandle::null();
if !scratch.is_null() {
let mut fbuf = [0 as c_char; 4096];
if n::oaknode_footage_filename(
footage_h,
fbuf.as_mut_ptr(),
fbuf.len() as c_int,
) >= 0
{
let fname = crate::handle::read_cstr(fbuf.as_ptr());
let fc = std::ffi::CString::new(fname).unwrap_or_default();
out = n::oaknode_footage_create(scratch, fc.as_ptr());
}
release_handle(scratch);
}
out
} else {
CHandle::null()
};
let mut children: Vec<CHandle> = Vec::new();
// The `_ex` variant skips the graph add-node command: it would
// move the clip entry into the sequence's project under a fresh
// id while leaving the caller's clip handle stale, which then
// breaks the place command's adoption (the entry is already
// gone). The place command's `adopt_block` performs the move
// itself and remaps the block handle; on undo the block is
// simply left as an orphan node in the sequence's project.
if strict_project {
let add_cmd = n::oaknode_command_create_add_node(seq_project_of(sequence), clip);
if add_cmd.is_null() {
release_handle(list);
if !scratch_footage.is_null() {
release_handle(scratch_footage);
}
set_seq_error("node add command failed");
return Ok(std::ptr::null_mut());
}
children.push(add_cmd);
}
let place_cmd =
tl::oaktimeline_place_block_command(list, track_index, clip, in_num, in_den);
if place_cmd.is_null() {
for child in &children {
release_handle(*child);
}
release_handle(list);
if !scratch_footage.is_null() {
release_handle(scratch_footage);
}
set_seq_error("place block command failed");
return Ok(std::ptr::null_mut());
}
children.push(place_cmd);
// The commands hold borrowed clones of `list` (no addref), so the
// handle must stay alive until the group push has executed the
// redos; released right after.
if let Err(e) = push_multi_commands(&children, "Add Clip") {
release_handle(list);
set_seq_error(&format!("failed to push add-clip command: {:?}", e));
return Err(e);
}
release_handle(list);
if !scratch_footage.is_null() {
let clip_node = n::oaknode_block_as_node(clip);
let mut edge = CHandle::null();
let rc = n::oaknode_node_connect_undoable(
scratch_footage,
clip_node,
c"tex_in".as_ptr(),
&mut edge,
);
release_handle(clip_node);
release_handle(scratch_footage);
if rc != 0 || edge.is_null() {
set_seq_error("footage connection failed");
return Ok(std::ptr::null_mut());
}
let edge_box = box_handle::<OakEngineClipboard>(edge);
push_or_run(edge_box, c"Add Clip".as_ptr())?;
}
Ok(box_handle::<OakEngineClip>(clip))
}
}
/// Borrowed project handle of a sequence (temporary; caller releases).
///
/// # Safety
/// `seq` must be a live module sequence handle.
unsafe fn seq_project_of(seq: CHandle) -> CHandle {
unsafe {
let node = n::oaknode_sequence_as_node(seq);
let mut project = CHandle::null();
let rc = n::oaknode_node_get_project(node, &mut project);
release_handle(node);
if rc != 0 {
return CHandle::null();
}
project
}
}
/// `oakengine_sequence_clip_count` — clips on a track (gaps skipped).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_clip_count(
self_: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(h, track_type, &mut list))?;
let mut track_count: c_int = 0;
Error::from_module(n::oaknode_tracklist_get_track_count(list, &mut track_count))?;
if track_index < 0 || track_index >= track_count {
release_handle(list);
return Err(Error::NotFound);
}
let mut track = CHandle::null();
Error::from_module(n::oaknode_tracklist_get_track_at(
list,
track_index,
&mut track,
))?;
let mut block_count: c_int = 0;
Error::from_module(n::oaknode_track_get_block_count(track, &mut block_count))?;
let mut count: c_int = 0;
for i in 0..block_count {
let mut block = CHandle::null();
if n::oaknode_track_get_block_at(track, i, &mut block) != 0 || block.is_null() {
continue;
}
let mut kind: c_int = 0;
let rc = n::oaknode_block_get_kind(block, &mut kind);
release_handle(block);
if rc == 0 && kind == BLOCK_KIND_CLIP {
count += 1;
}
}
release_handle(track);
release_handle(list);
Ok(count)
})
}
/// `oakengine_sequence_clip_at` — borrowed clip at (track, clip) index.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_clip_at(
self_: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
clip_index: c_int,
) -> *mut OakEngineClip {
guard_ptr(|| unsafe {
if self_.is_null()
|| track_type < TRACK_TYPE_VIDEO
|| track_type > TRACK_TYPE_SUBTITLE
|| clip_index < 0
{
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(h, track_type, &mut list))?;
let clip = clip_at_index(list, track_index, clip_index);
release_handle(list);
if clip.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineClip>(clip))
})
}
/// `oakengine_clip_get_range` — clip timeline range and media in-point as
/// frame timestamps.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_range(
self_: *const OakEngineClip,
in_: *mut i64,
out: *mut i64,
media_in: *mut i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
// The clip's sequence (clip -> track -> sequence) provides the
// timebase.
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
return Err(Error::State);
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
release_handle(track);
if rc != 0 || sequence.is_null() {
return Err(Error::State);
}
let tb = seq_time_base(sequence)?;
release_handle(sequence);
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?;
let mut mi_num: c_int = 0;
let mut mi_den: c_int = 0;
Error::from_module(n::oaknode_clip_get_media_in(h, &mut mi_num, &mut mi_den))?;
if !in_.is_null() {
*in_ = rational_to_ts(in_num as i64, in_den as i64, tb);
}
if !out.is_null() {
*out = rational_to_ts(out_num as i64, out_den as i64, tb);
}
if !media_in.is_null() {
*media_in = rational_to_ts(mi_num as i64, mi_den as i64, tb);
}
Ok(())
})
}
/// `oakengine_clip_get_sequence` — the clip's owning sequence.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_sequence(
self_: *const OakEngineClip,
) -> *mut OakEngineSequence {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
return Ok(std::ptr::null_mut());
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
release_handle(track);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineSequence>(sequence))
})
}
/// `oakengine_clip_as_node` — the clip's node view (borrowed; freed with
/// `oakengine_node_free`). The effect-stack surface (chain enumeration and
/// edits) is node-based, so the app converts its clip handle before
/// walking the chain.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_as_node(self_: *const OakEngineClip) -> *mut OakEngineNode {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let node = n::oaknode_block_as_node(h);
if node.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineNode>(node))
})
}
// ---- Sequence node-graph enumeration ----------------------------------------
//
// The node-editor surface (M12 P2): the app displays the CURRENT sequence's
// node graph — the sequence node (the output) plus the blocks, effects and
// scratch footage of its timeline. The module keeps the sequence in its own
// scratch project (documented deviation, see `oakengine_sequence_new`), so
// the graph is exactly that project's node list; positions live in the
// sequence node's context position map (`oakengine_node_get_context_position`).
/// `oakengine_sequence_as_node` — the sequence's node view (borrowed;
/// freed with `oakengine_node_free`). The graph surface addresses the
/// sequence through this handle: the sequence node is the graph's output
/// node AND the context for its position map.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_as_node(
self_: *const OakEngineSequence,
) -> *mut OakEngineNode {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(self_)?;
let node = n::oaknode_sequence_as_node(h);
if node.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineNode>(node))
})
}
/// `oakengine_sequence_node_count` — nodes in the sequence's owning
/// project (the sequence's graph; 0 for NULL/invalid).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_node_count(
self_: *const OakEngineSequence,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let project = seq_project_of(unbox(self_)?);
if project.is_null() {
return Ok(0);
}
let count = n::oaknode_project_node_count(project);
release_handle(project);
Ok(count)
})
}
/// `oakengine_sequence_node_at` — boxed node at `index` (freed with
/// `oakengine_node_free`); NULL for an invalid index or sequence.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_node_at(
self_: *const OakEngineSequence,
index: c_int,
) -> *mut OakEngineNode {
guard_ptr(|| unsafe {
if self_.is_null() || index < 0 {
return Ok(std::ptr::null_mut());
}
let project = seq_project_of(unbox(self_)?);
if project.is_null() {
return Ok(std::ptr::null_mut());
}
let node = n::oaknode_project_node_at(project, index);
release_handle(project);
if node.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineNode>(node))
})
}
/// The uuid of a project handle (empty on failure). Project handles are
/// per-call boxes around the same `Arc`, so ctx pointers cannot be compared;
/// the uuid is the stable identity.
///
/// # Safety
/// `project` must be a live module project handle.
unsafe fn project_uuid(project: CHandle) -> String {
unsafe {
let mut buf = [0 as c_char; 256];
let len = n::oaknode_project_get_uuid(project, buf.as_mut_ptr(), buf.len() as c_int);
if len < 0 {
String::new()
} else {
read_cstr(buf.as_ptr())
}
}
}
/// `oakengine_sequence_remove_node` — undoable removal of `node` from the
/// sequence's graph (its owning project; the module's remove command drops
/// incident edges). The sequence node itself (the graph's output) cannot be
/// removed.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_remove_node(
self_: *mut OakEngineSequence,
node: *mut OakEngineNode,
) -> c_int {
guard(|| unsafe {
if self_.is_null() || node.is_null() {
set_seq_error("invalid sequence or node");
return Err(Error::Invalid);
}
let sh = unbox(self_)?;
let nh = unbox(node)?;
let project = seq_project_of(sh);
if project.is_null() {
return Err(Error::Invalid);
}
// The node must live in the sequence's OWNING project. Node
// identities are per-project arena slots (generation + index), so
// two projects produce colliding identities; compare the projects'
// uuids instead.
let mut node_project = CHandle::null();
let rc = n::oaknode_node_get_project(nh, &mut node_project);
let node_uuid = if rc == 0 && !node_project.is_null() {
project_uuid(node_project)
} else {
String::new()
};
release_handle(node_project);
let seq_uuid = project_uuid(project);
if node_uuid.is_empty() || node_uuid != seq_uuid {
release_handle(project);
set_seq_error("node does not belong to this sequence's graph");
return Err(Error::Invalid);
}
// The sequence node is the graph's context/output; removing it would
// orphan every position map entry and the sequence itself.
let seq_node = n::oaknode_sequence_as_node(sh);
let is_self = !seq_node.is_null()
&& n::oaknode_node_identity(seq_node) == n::oaknode_node_identity(nh);
release_handle(seq_node);
release_handle(project);
if is_self {
set_seq_error("the sequence node cannot be removed");
return Err(Error::Invalid);
}
let cmd = n::oaknode_command_create_remove_node(nh);
if cmd.ctx.is_null() {
return Err(Error::Failed("remove node command failed".into()));
}
push_command(cmd, "Remove Node")
})
}
/// `oakengine_clip_get_media_filename` — the clip's upstream footage
/// filename (two-stage buf/size; M12 P4 — the waveform decorator needs
/// the media file). Negative error when the clip has no media.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_media_filename(
self_: *const OakEngineClip,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let Some((filename, _)) = timeline_clip_media(h) else {
return Err(Error::NotFound);
};
let rc = crate::handle::write_string(&filename, buf, buf_size);
Ok(string_result(rc))
})
}
/// Resolve a clip's media `(filename, stream)` through its graph node's
/// upstream footage (the same walk `crates::render` performs for the
/// montage).
unsafe fn timeline_clip_media(clip: CHandle) -> Option<(String, c_int)> {
unsafe {
let node = n::oaknode_block_as_node(clip);
if node.is_null() {
return None;
}
let mut footage = CHandle::null();
if n::oaknode_node_find_input_footage(node, &mut footage) != 0 || footage.is_null() {
return None;
}
let mut buf = [0 as c_char; 4096];
if n::oaknode_footage_filename(footage, buf.as_mut_ptr(), buf.len() as c_int) < 0 {
return None;
}
let filename = crate::handle::read_cstr(buf.as_ptr());
Some((filename, 1))
}
}
/* ---- Editing primitives, round 2: split / ripple delete / trim / move ---- */
/// `oakengine_sequence_split_clip` — split the addressed clip at `time`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_split_clip(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
clip_index: c_int,
time: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let clip = clip_at_index(list, track_index, clip_index);
release_handle(list);
if clip.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (point_num, point_den) = ts_to_rational(time, tb);
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(clip, &mut in_num, &mut in_den))?;
Error::from_module(n::oaknode_block_get_out(clip, &mut out_num, &mut out_den))?;
let cmp_in = rat_cmp(point_num, point_den, in_num as i64, in_den as i64);
let cmp_out = rat_cmp(point_num, point_den, out_num as i64, out_den as i64);
if cmp_in != std::cmp::Ordering::Greater || cmp_out != std::cmp::Ordering::Less {
set_seq_error(&format!(
"split time {} is not strictly inside the clip",
time
));
return Err(Error::Invalid);
}
let cmd = tl::oaktimeline_split_command(&clip, 1, point_num, point_den);
if cmd.is_null() {
set_seq_error("split command failed");
return Err(Error::Failed("split command failed".into()));
}
push_command(cmd, "Split Clip")
})
}
/// Compare two rationals (a_num/a_den vs b_num/b_den).
fn rat_cmp(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> std::cmp::Ordering {
let lhs = a_num * b_den;
let rhs = b_num * a_den;
lhs.cmp(&rhs)
}
/// `oakengine_sequence_ripple_delete_clip` — delete the clip and ripple the
/// following content left.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_ripple_delete_clip(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
clip_index: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let clip = clip_at_index(list, track_index, clip_index);
release_handle(list);
if clip.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(clip, &mut track);
if rc != 0 || track.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(clip, &mut in_num, &mut in_den))?;
Error::from_module(n::oaknode_block_get_out(clip, &mut out_num, &mut out_den))?;
let cmd = tl::oaktimeline_ripple_remove_area_command(
track,
in_num as i64,
in_den as i64,
out_num as i64,
out_den as i64,
);
// NOTE: `track` is intentionally NOT released — the module command
// stores the borrowed handle for its whole lifetime (its `redo`/
// `undo` re-resolve the track), and the module model keeps such
// handles alive for the command's lifetime (same as
// `oakengine_sequence_delete_clips`).
if cmd.is_null() {
set_seq_error("ripple delete command failed");
return Err(Error::Failed("ripple delete command failed".into()));
}
push_command(cmd, "Ripple Delete Clip")
})
}
/// `oakengine_clip_trim` — change the clip's timeline range.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_trim(
clip: *mut OakEngineClip,
new_in: i64,
new_out: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let h = match unbox(clip) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid clip handle");
return Err(Error::Invalid);
}
};
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
set_seq_error("clip is not on a track");
return Err(Error::State);
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
if rc != 0 || sequence.is_null() {
release_handle(track);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
release_handle(track);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
release_handle(sequence);
if new_in < 0 || new_out <= new_in {
release_handle(track);
set_seq_error("invalid trim range (need 0 <= new_in < new_out)");
return Err(Error::Invalid);
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?;
Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?;
let old_in = rational_to_ts(in_num as i64, in_den as i64, tb);
let old_out = rational_to_ts(out_num as i64, out_den as i64, tb);
if new_in == old_in && new_out == old_out {
release_handle(track);
return Ok(());
}
// The application's trim command: one end at a time, adjacent gaps
// absorb the difference; both ends are one undoable command. The
// module's own `BlockTrimCommand` applies its length setters with
// inverted semantics, so the facade carries the correct engine
// mapping (trim-in anchors the out, trim-out anchors the in).
let mut children: Vec<CHandle> = Vec::new();
let (old_len_num, old_len_den) = {
let mut ln: c_int = 0;
let mut ld: c_int = 0;
Error::from_module(n::oaknode_block_get_length(h, &mut ln, &mut ld))?;
(ln, ld)
};
if new_in != old_in {
// in-trim: length = block out - new in (out anchored).
let (new_in_num, new_in_den) = ts_to_rational(new_in, tb);
let (new_num, new_den) =
rat_sub(out_num as i64, out_den as i64, new_in_num, new_in_den);
let cmd = trim_cmd(
h,
MOVEMENT_MODE_TRIM_IN,
old_len_num,
old_len_den,
new_num as c_int,
new_den as c_int,
)?;
children.push(cmd);
}
if new_out != old_out {
// out-trim: length = new out - new in (in anchored); the old
// length is the post-in-trim length (out - new in) when both
// ends move.
let (new_in_num, new_in_den) = ts_to_rational(new_in, tb);
let (new_out_num, new_out_den) = ts_to_rational(new_out, tb);
let (new_num, new_den) = rat_sub(new_out_num, new_out_den, new_in_num, new_in_den);
let (post_in_num, post_in_den) =
rat_sub(out_num as i64, out_den as i64, new_in_num, new_in_den);
let cmd = trim_cmd(
h,
MOVEMENT_MODE_TRIM_OUT,
post_in_num as c_int,
post_in_den as c_int,
new_num as c_int,
new_den as c_int,
)?;
children.push(cmd);
}
release_handle(track);
push_multi_commands(&children, "Trim Clip")
})
}
/// `oakengine_sequence_move_clip` — move the addressed clip so its in
/// point becomes `new_in` (frame units in the sequence's timebase).
///
/// Mirrors the capi: the clip's old spot is replaced with a gap and the
/// clip is placed at the destination, both as ONE undoable entry ("Move
/// Clip"). The destination track is the addressed track itself (the
/// capi passes `track_index` straight through to the place command), so
/// this is a time-only move within the same track; length and media-in
/// point are preserved.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_move_clip(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
clip_index: c_int,
new_in: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE || new_in < 0 {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let clip = clip_at_index(list, track_index, clip_index);
if clip.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(clip, &mut track);
if rc != 0 || track.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let tb = seq_time_base(sequence)?;
let (new_in_num, new_in_den) = ts_to_rational(new_in, tb);
// NOTE: `list`/`clip`/`track` are borrowed module handles; the
// move command keeps copies of `list` and `clip` for the whole undo
// entry, so their shells must not be released here (the same
// convention as the other command-creating family functions). The
// destination is the addressed track itself.
let cmd =
tl::oaktimeline_move_block_command(list, track_index, clip, new_in_num, new_in_den);
push_command(cmd, "Move Clip")
})
}
/// `oakengine_sequence_move_clip_to_track` — move a clip to a different
/// track at `new_in` (frame units in the sequence's timebase) as ONE
/// undoable entry (M12 P4, cross-track move): the source spot becomes a
/// gap and the clip is placed on the destination track.
///
/// The destination track must already exist and hold clips of the same
/// type (video ↔ video, audio ↔ audio).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_move_clip_to_track(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
clip_index: c_int,
dest_track_index: c_int,
new_in: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO
|| track_type > TRACK_TYPE_SUBTITLE
|| new_in < 0
|| dest_track_index < 0
{
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let clip = clip_at_index(list, track_index, clip_index);
if clip.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(clip, &mut track);
if rc != 0 || track.is_null() {
set_seq_error(&format!(
"no clip at track {} index {}",
track_index, clip_index
));
return Err(Error::NotFound);
}
let mut track_count: c_int = 0;
Error::from_module(n::oaknode_tracklist_get_track_count(
list, &mut track_count,
))?;
if dest_track_index >= track_count {
set_seq_error(&format!(
"destination track {} out of range ({} tracks)",
dest_track_index, track_count
));
return Err(Error::NotFound);
}
let tb = seq_time_base(sequence)?;
let (new_in_num, new_in_den) = ts_to_rational(new_in, tb);
// One undoable entry: gap the source, re-home the block's in
// point, place on the destination. (The module stores a block's
// position on the block itself, so the place command alone would
// keep the old in point.)
let mut children: Vec<CHandle> = Vec::new();
let gap_cmd = tl::oaktimeline_replace_block_with_gap_command(track, clip);
if gap_cmd.is_null() {
set_seq_error("replace-with-gap command failed");
return Err(Error::Failed("gap command failed".into()));
}
children.push(gap_cmd);
let mut old_num: c_int = 0;
let mut old_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(clip, &mut old_num, &mut old_den))?;
let data = Box::into_raw(Box::new(BlockInCmdData {
block: clip,
old_num,
old_den,
new_num: new_in_num as c_int,
new_den: new_in_den as c_int,
}));
let in_cmd = vtable_command(block_in_redo, block_in_undo, block_in_free, data as *mut c_void)?;
children.push(in_cmd);
let place_cmd = tl::oaktimeline_place_block_command(
list,
dest_track_index,
clip,
new_in_num,
new_in_den,
);
if place_cmd.is_null() {
set_seq_error("place-block command failed");
return Err(Error::Failed("place command failed".into()));
}
children.push(place_cmd);
// The commands hold borrowed clones of `list`/`track`/`clip` (no
// addref); keep them alive until the redos run.
push_multi_commands(&children, "Move Clip to Track")
})
}
/// Vtable data for the block-in re-home step of a cross-track move.
struct BlockInCmdData {
/// The block (borrowed; alive with the sequence).
block: CHandle,
/// Original in point.
old_num: c_int,
/// Original in point denominator.
old_den: c_int,
/// Placement in point.
new_num: c_int,
/// Placement in point denominator.
new_den: c_int,
}
/// `BlockInCmdData` redo: re-home the block's in point.
unsafe extern "C" fn block_in_redo(data: *mut c_void) {
unsafe {
let d = &*(data as *const BlockInCmdData);
n::oaknode_block_set_in(d.block, d.new_num, d.new_den);
}
}
/// `BlockInCmdData` undo: restore the original in point.
unsafe extern "C" fn block_in_undo(data: *mut c_void) {
unsafe {
let d = &*(data as *const BlockInCmdData);
n::oaknode_block_set_in(d.block, d.old_num, d.old_den);
}
}
/// `BlockInCmdData` free.
unsafe extern "C" fn block_in_free(data: *mut c_void) {
unsafe { drop(Box::from_raw(data as *mut BlockInCmdData)) };
}
/* ---- Batch editing (timeline panel) -------------------------------------- */
/// `oakengine_sequence_split_clips` — split every given clip at `time_ts`,
/// preserving links.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_split_clips(
seq: *mut OakEngineSequence,
clips: *mut *mut OakEngineClip,
clip_count: c_int,
time_ts: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if clips.is_null() || clip_count <= 0 {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (time_num, time_den) = ts_to_rational(time_ts, tb);
// Collect the blocks, deduplicating, and check that at least one
// spans the time (same as the engine command).
let mut blocks: Vec<CHandle> = Vec::new();
let mut any_spanning = false;
let slice = std::slice::from_raw_parts(clips, clip_count as usize);
for (i, clip) in slice.iter().enumerate() {
let c = match unbox(*clip) {
Ok(h) => h,
Err(_) => {
set_seq_error(&format!("invalid clip at index {}", i));
return Err(Error::Invalid);
}
};
if blocks.iter().any(|b| b.ctx == c.ctx) {
continue;
}
blocks.push(c);
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
if n::oaknode_block_get_in(c, &mut in_num, &mut in_den) == 0
&& n::oaknode_block_get_out(c, &mut out_num, &mut out_den) == 0
&& rat_cmp(in_num as i64, in_den as i64, time_num, time_den)
== std::cmp::Ordering::Less
&& rat_cmp(out_num as i64, out_den as i64, time_num, time_den)
== std::cmp::Ordering::Greater
{
any_spanning = true;
}
}
if !any_spanning {
set_seq_error(&format!("no clip spans time {}", time_ts));
return Err(Error::NotFound);
}
let cmd = tl::oaktimeline_split_preserving_links_command(
blocks.as_ptr(),
blocks.len() as c_int,
&time_num,
&time_den,
1,
);
if cmd.is_null() {
set_seq_error("split command failed");
return Err(Error::Failed("split command failed".into()));
}
push_command(cmd, "Split Clips")
})
}
/// `oakengine_sequence_delete_clips` — delete clips leaving gaps,
/// optionally rippling regions closed.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_delete_clips(
seq: *mut OakEngineSequence,
clips: *mut *mut OakEngineClip,
clip_count: c_int,
ripple: c_int,
ripple_ranges_ts: *const i64,
ripple_range_count: c_int,
rippled: *mut c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if !rippled.is_null() {
*rippled = 0;
}
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if clip_count < 0
|| (clip_count > 0 && clips.is_null())
|| ripple_range_count < 0
|| (ripple_range_count > 0 && ripple_ranges_ts.is_null())
{
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
if clip_count == 0 && (ripple == 0 || ripple_range_count == 0) {
return Ok(());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let mut children: Vec<CHandle> = Vec::new();
// (track, in, out) rationals of the deleted clips, for the default
// ripple regions.
let mut clip_ranges: Vec<(CHandle, i64, i64, i64, i64)> = Vec::new();
// NULL with a zero count is a legal empty set; the slice must not be
// constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)`
// is UB), so it is only built for a positive count.
let slice: &[*mut OakEngineClip] = if clip_count > 0 {
std::slice::from_raw_parts(clips, clip_count as usize)
} else {
&[]
};
for (i, clip) in slice.iter().enumerate() {
let c = match unbox(*clip) {
Ok(h) => h,
Err(_) => {
set_seq_error(&format!("invalid clip at index {}", i));
return Err(Error::Invalid);
}
};
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(c, &mut track);
if rc != 0 || track.is_null() {
set_seq_error(&format!("invalid clip at index {}", i));
return Err(Error::Invalid);
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(c, &mut in_num, &mut in_den))?;
Error::from_module(n::oaknode_block_get_out(c, &mut out_num, &mut out_den))?;
let gap_cmd = tl::oaktimeline_replace_block_with_gap_command(track, c);
let remove_cmd = n::oaknode_command_create_remove_node(c);
if gap_cmd.is_null() || remove_cmd.is_null() {
return Err(Error::Failed("delete clip command failed".into()));
}
children.push(gap_cmd);
children.push(remove_cmd);
clip_ranges.push((
track,
in_num as i64,
in_den as i64,
out_num as i64,
out_den as i64,
));
}
let mut ripple_command: Option<CHandle> = None;
if ripple != 0 {
let mut ranges: Vec<(CHandle, i64, i64, i64, i64)> = Vec::new();
if !ripple_ranges_ts.is_null() && ripple_range_count > 0 {
for i in 0..ripple_range_count {
let range = ripple_ranges_ts.add(i as usize * 4);
let rtype = *range;
let rindex = *range.add(1);
if rtype < TRACK_TYPE_VIDEO as i64 || rtype > TRACK_TYPE_SUBTITLE as i64 {
set_seq_error(&format!("invalid track type in ripple range {}", i));
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence,
rtype as c_int,
&mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, rindex as c_int, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!(
"no track at index {} in ripple range {}",
rindex, i
));
return Err(Error::NotFound);
}
let (in_num, in_den) = ts_to_rational(*range.add(2), tb);
let (out_num, out_den) = ts_to_rational(*range.add(3), tb);
ranges.push((track, in_num, in_den, out_num, out_den));
}
} else {
ranges = clip_ranges;
}
if !ranges.is_empty() {
let mut in_nums: Vec<i64> = Vec::new();
let mut in_dens: Vec<i64> = Vec::new();
let mut out_nums: Vec<i64> = Vec::new();
let mut out_dens: Vec<i64> = Vec::new();
let mut tracks: Vec<CHandle> = Vec::new();
for (track, in_num, in_den, out_num, out_den) in &ranges {
tracks.push(*track);
in_nums.push(*in_num);
in_dens.push(*in_den);
out_nums.push(*out_num);
out_dens.push(*out_den);
}
let cmd = tl::oaktimeline_ripple_delete_gaps_command(
sequence,
in_nums.as_ptr(),
in_dens.as_ptr(),
out_nums.as_ptr(),
out_dens.as_ptr(),
tracks.as_ptr(),
ranges.len() as c_int,
);
if !cmd.is_null() {
children.push(cmd);
ripple_command = Some(cmd);
}
}
}
push_multi_commands(&children, "Delete Clips")?;
if !rippled.is_null() {
*rippled = if ripple_command.is_some() { 1 } else { 0 };
}
Ok(())
})
}
/// `oakengine_sequence_ripple_delete_range` — remove [in_ts, out_ts) on
/// every track and shift the following content left.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_ripple_delete_range(
seq: *mut OakEngineSequence,
in_ts: i64,
out_ts: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid range");
return Err(Error::Invalid);
}
};
if in_ts < 0 || out_ts <= in_ts {
set_seq_error(&format!("invalid range [{}, {})", in_ts, out_ts));
return Err(Error::Invalid);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
// The module exports no sequence-wide ripple-area command
// (`TimelineRippleRemoveAreaCommand` has no C creator), so the
// per-track `TrackRippleRemoveAreaCommand` composition is used, which
// is what the C++ command itself does internally.
let (in_num, in_den) = ts_to_rational(in_ts, tb);
let (out_num, out_den) = ts_to_rational(out_ts, tb);
let mut all_count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_all_track_count(
sequence,
&mut all_count,
))?;
let mut children: Vec<CHandle> = Vec::new();
for i in 0..all_count {
let mut track = CHandle::null();
Error::from_module(n::oaknode_sequence_get_all_track_at(
sequence, i, &mut track,
))?;
if track.is_null() {
continue;
}
let cmd =
tl::oaktimeline_ripple_remove_area_command(track, in_num, in_den, out_num, out_den);
// NOTE: `track` is intentionally NOT released — the module
// command stores the borrowed handle for its whole lifetime (see
// `oakengine_sequence_ripple_delete_clip`).
if cmd.is_null() {
return Err(Error::Failed("ripple delete command failed".into()));
}
children.push(cmd);
}
push_multi_commands(&children, "Ripple Delete Range")
})
}
/// `oakengine_clip_toggle_enabled` — flip the enabled flag of every given
/// clip (one undoable command).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_toggle_enabled(
clips: *mut *mut OakEngineClip,
count: c_int,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
if count < 0 || (count > 0 && clips.is_null()) {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut children: Vec<CHandle> = Vec::new();
// NULL with a zero count is a legal empty set; the slice must not be
// constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)`
// is UB), so it is only built for a positive count.
let slice: &[*mut OakEngineClip] = if count > 0 {
std::slice::from_raw_parts(clips, count as usize)
} else {
&[]
};
for (i, clip) in slice.iter().enumerate() {
let c = match unbox(*clip) {
Ok(h) => h,
Err(_) => {
set_seq_error(&format!("invalid clip at index {}", i));
return Err(Error::Invalid);
}
};
let mut enabled: c_int = 0;
Error::from_module(n::oaknode_block_get_enabled(c, &mut enabled))?;
let data = Box::into_raw(Box::new(BlockEnabledCmdData {
block: c.addref(),
old_enabled: enabled,
new_enabled: if enabled != 0 { 0 } else { 1 },
}));
let cmd = vtable_command(
block_enabled_redo,
block_enabled_undo,
block_enabled_free,
data as *mut c_void,
)?;
children.push(cmd);
}
push_multi_commands(&children, "Toggle Clips Enabled")?;
Ok(count)
})
}
/// `oakengine_clip_set_linked` — link or unlink every given clip with each
/// other (one undoable command).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_set_linked(
clips: *mut *mut OakEngineClip,
count: c_int,
linked: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if count < 0 || (count > 0 && clips.is_null()) {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
if count == 0 {
return Ok(());
}
let mut handles: Vec<CHandle> = Vec::new();
let slice = std::slice::from_raw_parts(clips, count as usize);
for (i, clip) in slice.iter().enumerate() {
let c = match unbox(*clip) {
Ok(h) => h,
Err(_) => {
set_seq_error(&format!("invalid clip at index {}", i));
return Err(Error::Invalid);
}
};
handles.push(c);
}
// The module has no `NodeLinkManyCommand` creator; pair commands are
// assembled (each clip linked to the first for a link, every pair
// unlinked otherwise).
let mut children: Vec<CHandle> = Vec::new();
for i in 0..handles.len() {
for j in (i + 1)..handles.len() {
let mut cmd = CHandle::null();
let rc = n::oaknode_node_link_undoable(handles[i], handles[j], linked, &mut cmd);
if rc == 0 && !cmd.is_null() {
children.push(cmd);
} else if rc != 0 {
return Err(Error::Module(rc));
}
}
}
push_multi_commands(&children, "Link Clips")
})
}
/// `oakengine_sequence_add_default_transition` — add the configured default
/// transitions around the given clips.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_default_transition(
seq: *mut OakEngineSequence,
clips: *mut *mut OakEngineClip,
count: c_int,
) -> c_int {
guard(|| {
set_seq_error("");
let _ = seq;
if count < 0 || (count > 0 && clips.is_null()) {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
if count == 0 {
return Ok(());
}
// Stub: the module's `TimelineAddDefaultTransitionCommand` is not
// reachable over the C ABI and its transition-node construction is
// itself unimplemented (undogeneral.rs `add_transition` NOTE).
let _ = clips;
set_seq_error("default transitions are not supported by the module");
Err(Error::State)
})
}
/// `oakengine_clip_is_enabled` — 1 if the clip is enabled.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_is_enabled(self_: *const OakEngineClip) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let mut enabled: c_int = 0;
Error::from_module(n::oaknode_block_get_enabled(h, &mut enabled))?;
Ok(enabled)
})
}
/// `oakengine_clip_are_linked` — 1 if the two clips are linked.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_are_linked(
a: *const OakEngineClip,
b: *const OakEngineClip,
) -> c_int {
guard_int(|| unsafe {
if a.is_null() || b.is_null() {
return Ok(0);
}
let ah = unbox(a)?;
let bh = unbox(b)?;
let mut linked: c_int = 0;
Error::from_module(n::oaknode_block_are_linked(ah, bh, &mut linked))?;
Ok(linked)
})
}
/// `oakengine_sequence_ripple_delete_in_to_out` — delete the workarea range
/// on every track (ripple or gap), disabling the workarea afterwards.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_ripple_delete_in_to_out(
seq: *mut OakEngineSequence,
ripple: c_int,
in_ts: i64,
out_ts: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid range");
return Err(Error::Invalid);
}
};
if in_ts < 0 || out_ts <= in_ts {
set_seq_error(&format!("invalid range [{}, {})", in_ts, out_ts));
return Err(Error::Invalid);
}
let wa = seq_workarea(sequence)?;
let mut enabled: c_int = 0;
let rc = tl::oaktimeline_workarea_get(
wa,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut enabled,
);
if rc != 0 || enabled == 0 {
release_handle(wa);
set_seq_error("sequence workarea is not enabled");
return Err(Error::State);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(wa);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (in_num, in_den) = ts_to_rational(in_ts, tb);
let (out_num, out_den) = ts_to_rational(out_ts, tb);
let mut children: Vec<CHandle> = Vec::new();
if ripple != 0 {
// Ripple the area out on every track.
let mut all_count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_all_track_count(
sequence,
&mut all_count,
))?;
for i in 0..all_count {
let mut track = CHandle::null();
Error::from_module(n::oaknode_sequence_get_all_track_at(
sequence, i, &mut track,
))?;
if track.is_null() {
continue;
}
let cmd = tl::oaktimeline_ripple_remove_area_command(
track, in_num, in_den, out_num, out_den,
);
// NOTE: `track` is intentionally NOT released — the module
// command stores the borrowed handle for its whole lifetime
// (see `oakengine_sequence_ripple_delete_clip`).
if cmd.is_null() {
release_handle(wa);
return Err(Error::Failed("ripple remove command failed".into()));
}
children.push(cmd);
}
} else {
// Fill the area with a fresh gap on every unlocked track.
let mut all_count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_all_track_count(
sequence,
&mut all_count,
))?;
let (len_num, len_den) = rat_sub(out_num, out_den, in_num, in_den);
for i in 0..all_count {
let mut track = CHandle::null();
Error::from_module(n::oaknode_sequence_get_all_track_at(
sequence, i, &mut track,
))?;
if track.is_null() {
continue;
}
let mut locked: c_int = 0;
let lrc = n::oaknode_track_get_locked(track, &mut locked);
if lrc != 0 || locked != 0 {
release_handle(track);
continue;
}
let mut ttype: c_int = 0;
let mut tindex: c_int = 0;
if n::oaknode_track_get_type(track, &mut ttype) != 0
|| n::oaknode_track_get_index(track, &mut tindex) != 0
{
release_handle(track);
continue;
}
let project = seq_project_of(sequence);
let mut list = CHandle::null();
let lrc = n::oaknode_sequence_get_track_list(sequence, ttype, &mut list);
let gap = n::oaknode_block_gap_create();
if lrc != 0 || project.is_null() || gap.is_null() || list.is_null() {
release_handle(project);
release_handle(list);
release_handle(track);
release_handle(wa);
return Err(Error::Failed("gap insertion failed".into()));
}
let src = n::oaknode_block_set_length_and_media_out(
gap,
len_num as c_int,
len_den as c_int,
);
let add_cmd = n::oaknode_command_create_add_node(project, gap);
let place_cmd =
tl::oaktimeline_place_block_command(list, tindex, gap, in_num, in_den);
release_handle(project);
release_handle(list);
release_handle(track);
if src != 0 || add_cmd.is_null() || place_cmd.is_null() {
release_handle(wa);
return Err(Error::Failed("gap insertion command failed".into()));
}
children.push(add_cmd);
children.push(place_cmd);
}
}
let disable_cmd = tl::oaktimeline_workarea_set_enabled_command(wa, 0);
release_handle(wa);
if disable_cmd.is_null() {
return Err(Error::Failed("workarea disable command failed".into()));
}
children.push(disable_cmd);
push_multi_commands(&children, "Delete In To Out")
})
}
/// `oakengine_sequence_trim_clips_to` — trim the nearest clip of every
/// unlocked track to `point_ts`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_trim_clips_to(
seq: *mut OakEngineSequence,
edge: c_int,
point_ts: i64,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if point_ts < 0 || edge < 0 || edge > 1 {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (point_num, point_den) = ts_to_rational(point_ts, tb);
let mode = if edge == 0 {
MOVEMENT_MODE_TRIM_IN
} else {
MOVEMENT_MODE_TRIM_OUT
};
let mut children: Vec<CHandle> = Vec::new();
let mut trimmed: c_int = 0;
let mut all_count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_all_track_count(
sequence,
&mut all_count,
))?;
for i in 0..all_count {
let mut track = CHandle::null();
Error::from_module(n::oaknode_sequence_get_all_track_at(
sequence, i, &mut track,
))?;
if track.is_null() {
continue;
}
let mut locked: c_int = 0;
if n::oaknode_track_get_locked(track, &mut locked) != 0 || locked != 0 {
release_handle(track);
continue;
}
// A trim (in or out) is only meaningful for the block that
// CONTAINS the point (in < point < out); the nearest-before
// queries can pick an insertion-order neighbor that ends before
// the point (which would trim to a negative length), so the
// strictly-containing lookup is used for both modes.
let mut block = CHandle::null();
let rc = n::oaknode_track_get_block_containing_time(
track,
point_num as c_int,
point_den as c_int,
&mut block,
);
if rc != 0 || block.is_null() {
release_handle(track);
continue;
}
let mut kind: c_int = 0;
n::oaknode_block_get_kind(block, &mut kind);
if kind == BLOCK_KIND_GAP {
release_handle(block);
release_handle(track);
continue;
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
n::oaknode_block_get_in(block, &mut in_num, &mut in_den);
n::oaknode_block_get_out(block, &mut out_num, &mut out_den);
// new_length = length - |nearest_time - point|; the in-trim
// anchors the out, the out-trim anchors the in (see
// `oakengine_clip_trim`).
let (new_num, new_den) = if mode == MOVEMENT_MODE_TRIM_IN {
rat_sub(out_num as i64, out_den as i64, point_num, point_den)
} else {
rat_sub(point_num, point_den, in_num as i64, in_den as i64)
};
let mut old_len_num: c_int = 0;
let mut old_len_den: c_int = 0;
Error::from_module(n::oaknode_block_get_length(
block,
&mut old_len_num,
&mut old_len_den,
))?;
// Trim the addressed block itself (`trim_cmd` anchors on the
// block handle; passing the track used to silently reject the
// trim in the module).
let cmd = trim_cmd(
block,
mode,
old_len_num,
old_len_den,
new_num as c_int,
new_den as c_int,
)?;
release_handle(block);
release_handle(track);
children.push(cmd);
trimmed += 1;
}
if trimmed == 0 {
return Ok(0);
}
push_multi_commands(&children, "Trim Clips To Point")?;
Ok(trimmed)
})
}
/// `oakengine_sequence_delete_empty_tracks` — remove every empty track.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_delete_empty_tracks(
seq: *mut OakEngineSequence,
track_type: c_int,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < -1 || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut children: Vec<CHandle> = Vec::new();
// (track, owning list) pairs for the live removal compensation
// (the module's `TimelineRemoveTrackCommand` redo is a no-op for the
// list structure; see below).
let mut to_remove: Vec<(CHandle, CHandle)> = Vec::new();
let mut removed: c_int = 0;
let mut all_count: c_int = 0;
Error::from_module(n::oaknode_sequence_get_all_track_count(
sequence,
&mut all_count,
))?;
for i in 0..all_count {
let mut track = CHandle::null();
Error::from_module(n::oaknode_sequence_get_all_track_at(
sequence, i, &mut track,
))?;
if track.is_null() {
continue;
}
if track_type >= 0 {
let mut ttype: c_int = 0;
if n::oaknode_track_get_type(track, &mut ttype) != 0 || ttype != track_type {
release_handle(track);
continue;
}
}
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(track, &mut block_count) != 0 || block_count != 0 {
release_handle(track);
continue;
}
let cmd = tl::oaktimeline_remove_track_command(track);
// Locate the owning list for the live removal (addref the track
// first so it survives the release below).
let mut ttype: c_int = 0;
if n::oaknode_track_get_type(track, &mut ttype) == 0 {
let mut list = CHandle::null();
if n::oaknode_sequence_get_track_list(sequence, ttype, &mut list) == 0
&& !list.is_null()
{
to_remove.push((track.addref(), list));
}
}
release_handle(track);
if cmd.is_null() {
return Err(Error::Failed("remove track command failed".into()));
}
children.push(cmd);
removed += 1;
}
if removed == 0 {
return Ok(0);
}
push_multi_commands(&children, "Delete Empty Tracks")?;
// The module's TimelineRemoveTrackCommand redo is a no-op for the
// list structure (undogeneral.rs NOTE), so the removal is applied
// live as compensation (the same documented deviation as
// `oakengine_sequence_remove_track`).
for (track, list) in &to_remove {
n::oaknode_tracklist_remove_track(*list, *track);
release_handle(*list);
}
Ok(removed)
})
}
/// `oakengine_sequence_marker_remove_many` — remove the markers at the
/// given times (one undoable command).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_remove_many(
seq: *mut OakEngineSequence,
times_ts: *const i64,
count: c_int,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if count < 0 || (count > 0 && times_ts.is_null()) {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
if count == 0 {
return Ok(0);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let list = seq_marker_list(sequence)?;
// Resolve all markers first so a bad time fails without side effects
// (markers are unique per time in the engine).
let mut indices: Vec<c_int> = Vec::new();
for i in 0..count {
let (num, den) = ts_to_rational(*times_ts.add(i as usize), tb);
let idx = marker_index_at(list, num, den);
if idx < 0 {
release_handle(list);
set_seq_error(&format!("no marker at time {}", *times_ts.add(i as usize)));
return Err(Error::NotFound);
}
indices.push(idx);
}
let mut children: Vec<CHandle> = Vec::new();
// The module's MarkerRemoveCommand captures the list INDEX at redo
// time (not the marker identity), so removals must run in descending
// index order for earlier removals not to shift later targets.
indices.sort_unstable();
indices.dedup();
for idx in indices.iter().rev() {
let cmd = tl::oaktimeline_marker_remove_at_command(list, *idx);
if cmd.is_null() {
release_handle(list);
return Err(Error::Failed("remove marker command failed".into()));
}
children.push(cmd);
}
release_handle(list);
push_multi_commands(&children, "Remove Markers")?;
Ok(count)
})
}
/* ---- Track structure and markers ------------------------------------------ */
/// `oakengine_sequence_remove_track` — remove a track and its content.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_remove_track(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
// The module's TimelineRemoveTrackCommand redo is a no-op for the
// list structure (undogeneral.rs NOTE), so the removal is applied
// live as compensation (documented deviation).
let cmd = tl::oaktimeline_remove_track_command(track);
if cmd.is_null() {
release_handle(track);
return Err(Error::Failed("remove track command failed".into()));
}
push_command(cmd, "Remove Track")?;
let mut list2 = CHandle::null();
let rc = n::oaknode_sequence_get_track_list(sequence, track_type, &mut list2);
if rc == 0 && !list2.is_null() {
n::oaknode_tracklist_remove_track(list2, track);
release_handle(list2);
}
release_handle(track);
Ok(())
})
}
/// `oakengine_sequence_move_track` — move a track within its list.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_move_track(
seq: *mut OakEngineSequence,
track_type: c_int,
from_index: c_int,
to_index: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid sequence or track type");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut count: c_int = 0;
Error::from_module(n::oaknode_tracklist_get_track_count(list, &mut count))?;
release_handle(list);
if from_index < 0 || from_index >= count || to_index < 0 || to_index >= count {
set_seq_error(&format!("track index out of range ({} tracks)", count));
return Err(Error::NotFound);
}
if from_index == to_index {
return Ok(());
}
// Stub: a true move needs undoable element-aware edge commands (the
// sequence's track inputs are array elements) plus a track re-order
// surface; the module provides neither (tracks are not connected to
// the sequence inputs at all in the module world).
set_seq_error("track moves are not supported by the module");
Err(Error::State)
})
}
/// `oakengine_track_get_height` — track height in internal units.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_get_height(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
height: *mut f64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if height.is_null() {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let rc = n::oaknode_track_get_height(track, height);
release_handle(track);
Error::from_module(rc)
})
}
/// `oakengine_track_set_height` — set the track height (NOT undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_set_height(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
height: f64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if height <= 0.0 || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let rc = n::oaknode_track_set_height(track, height);
release_handle(track);
Error::from_module(rc)
})
}
/// `oakengine_track_is_muted` — 1 if the track is muted.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_is_muted(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
) -> c_int {
guard_int(|| unsafe {
if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Ok(0);
}
let h = unbox(seq)?;
let mut list = CHandle::null();
if n::oaknode_sequence_get_track_list(h, track_type, &mut list) != 0 {
return Ok(0);
}
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
return Ok(0);
}
let mut muted: c_int = 0;
let rc = n::oaknode_track_get_muted(track, &mut muted);
release_handle(track);
if rc != 0 {
return Ok(0);
}
Ok(muted)
})
}
/// `oakengine_track_set_muted` — mute/unmute the track (NOT undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_set_muted(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
muted: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let rc = n::oaknode_track_set_muted(track, muted);
release_handle(track);
Error::from_module(rc)
})
}
/// `oakengine_track_is_locked` — 1 if the track is locked.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_is_locked(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
) -> c_int {
guard_int(|| unsafe {
if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Ok(0);
}
let h = unbox(seq)?;
let mut list = CHandle::null();
if n::oaknode_sequence_get_track_list(h, track_type, &mut list) != 0 {
return Ok(0);
}
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
return Ok(0);
}
let mut locked: c_int = 0;
let rc = n::oaknode_track_get_locked(track, &mut locked);
release_handle(track);
if rc != 0 {
return Ok(0);
}
Ok(locked)
})
}
/// `oakengine_track_set_locked` — lock/unlock the track (NOT undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_set_locked(
seq: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
locked: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let rc = n::oaknode_track_set_locked(track, locked);
release_handle(track);
Error::from_module(rc)
})
}
/// `oakengine_sequence_marker_add` — add a marker at `time_ts` (color 0).
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_add(
seq: *mut OakEngineSequence,
time_ts: i64,
name: *const c_char,
) -> c_int {
unsafe { oakengine_sequence_marker_add_ex(seq, time_ts, name, 0) }
}
/// `oakengine_sequence_marker_add_ex` — add a marker with an explicit color.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_add_ex(
seq: *mut OakEngineSequence,
time_ts: i64,
name: *const c_char,
color: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
};
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (num, den) = ts_to_rational(time_ts, tb);
let list = seq_marker_list(sequence)?;
if marker_index_at(list, num, den) >= 0 {
release_handle(list);
// The engine's marker insertion asserts on duplicate times.
set_seq_error(&format!("a marker already exists at time {}", time_ts));
return Err(Error::State);
}
let name_c = std::ffi::CString::new(read_cstr(name))
.map_err(|_| Error::Failed("invalid name".into()))?;
let cmd = tl::oaktimeline_marker_add_command(
list,
num as c_int,
den as c_int,
num as c_int,
den as c_int,
name_c.as_ptr(),
color,
);
release_handle(list);
if cmd.is_null() {
set_seq_error("add marker command failed");
return Err(Error::Failed("add marker command failed".into()));
}
push_command(cmd, "Add Marker")
})
}
/// `oakengine_sequence_marker_remove` — remove the marker at `time_ts`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_remove(
seq: *mut OakEngineSequence,
time_ts: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
};
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (num, den) = ts_to_rational(time_ts, tb);
let list = seq_marker_list(sequence)?;
let idx = marker_index_at(list, num, den);
if idx < 0 {
release_handle(list);
set_seq_error(&format!("no marker at time {}", time_ts));
return Err(Error::NotFound);
}
let cmd = tl::oaktimeline_marker_remove_at_command(list, idx);
release_handle(list);
if cmd.is_null() {
set_seq_error("remove marker command failed");
return Err(Error::Failed("remove marker command failed".into()));
}
push_command(cmd, "Remove Marker")
})
}
/// `oakengine_sequence_marker_rename` — rename the marker at `time_ts`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_marker_rename(
seq: *mut OakEngineSequence,
time_ts: i64,
name: *const c_char,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence");
return Err(Error::Invalid);
}
};
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (num, den) = ts_to_rational(time_ts, tb);
let list = seq_marker_list(sequence)?;
let idx = marker_index_at(list, num, den);
if idx < 0 {
release_handle(list);
set_seq_error(&format!("no marker at time {}", time_ts));
return Err(Error::NotFound);
}
let name_c = std::ffi::CString::new(read_cstr(name))
.map_err(|_| Error::Failed("invalid name".into()))?;
let cmd = tl::oaktimeline_marker_set_props_command(list, idx, -1, name_c.as_ptr());
release_handle(list);
if cmd.is_null() {
set_seq_error("rename marker command failed");
return Err(Error::Failed("rename marker command failed".into()));
}
push_command(cmd, "Rename Marker")
})
}
/* ---- Marker handle family -------------------------------------------------- */
/// `oakengine_marker_list_count` — number of markers in the list.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_list_count(list: *const OakEngineMarkerList) -> c_int {
guard_int(|| unsafe {
if list.is_null() {
return Ok(0);
}
let h = unbox(list)?;
let mut count: c_int = 0;
Error::from_module(tl::oaktimeline_marker_count(h, &mut count))?;
Ok(count)
})
}
/// `oakengine_marker_list_add` — add a marker (undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_list_add(
list: *mut OakEngineMarkerList,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
name: *const c_char,
color: c_int,
) -> c_int {
guard(|| unsafe {
if list.is_null() {
return Err(Error::Invalid);
}
let h = unbox(list)?;
let name_c = std::ffi::CString::new(read_cstr(name))
.map_err(|_| Error::Failed("invalid name".into()))?;
let cmd = tl::oaktimeline_marker_add_command(
h,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
name_c.as_ptr(),
color,
);
if cmd.is_null() {
return Err(Error::Failed("add marker command failed".into()));
}
push_command(cmd, "Add Marker")
})
}
/// `oakengine_marker_create` — create a detached marker.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_create(
color: c_int,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
name: *const c_char,
) -> *mut OakEngineMarker {
guard_ptr(|| {
// Stub: the oaktimeline module has no standalone marker handle (all
// marker operations are list-based over the C ABI), so a detached
// marker cannot be represented.
let _ = (color, in_num, in_den, out_num, out_den, name);
Ok(std::ptr::null_mut())
})
}
/// `oakengine_marker_free` — free a detached marker (NULL-safe no-op).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_free(marker: *mut OakEngineMarker) {
guard_void(|| unsafe {
if marker.is_null() {
return;
}
// Detached markers are a stub (`oakengine_marker_create` returns
// NULL); borrowed marker boxes are freed by the caller's box
// lifecycle, so this only releases a borrowed box.
free_box::<OakEngineMarker>(marker);
})
}
/// `oakengine_marker_list_add_existing` — re-add a marker to the list.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_list_add_existing(
list: *mut OakEngineMarkerList,
marker: *mut OakEngineMarker,
) -> c_int {
guard(|| unsafe {
if list.is_null() || marker.is_null() {
return Err(Error::Invalid);
}
let lh = unbox(list)?;
let (mlist, index) = marker_unbox(marker)?;
// Read the marker's data back through the list and add a fresh
// marker with it (the module has no marker-insert-by-handle).
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
mlist,
index,
&mut in_num,
&mut in_den,
&mut out_num,
&mut out_den,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
return Err(Error::Module(rc));
}
let mut name_buf = [0 as c_char; 4096];
let rc = tl::oaktimeline_marker_at(
mlist,
index,
&mut in_num,
&mut in_den,
&mut out_num,
&mut out_den,
&mut color,
name_buf.as_mut_ptr(),
name_buf.len() as c_int,
);
if rc < 0 {
return Err(Error::Module(rc));
}
let name = read_cstr(name_buf.as_ptr());
let name_c =
std::ffi::CString::new(name).map_err(|_| Error::Failed("invalid name".into()))?;
let cmd = tl::oaktimeline_marker_add_command(
lh,
in_num,
in_den,
out_num,
out_den,
name_c.as_ptr(),
color,
);
if cmd.is_null() {
return Err(Error::Failed("add existing marker command failed".into()));
}
push_command(cmd, "Add Existing Marker")
})
}
/// `oakengine_marker_list_at` — marker at the given sorted index.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_list_at(
list: *const OakEngineMarkerList,
index: c_int,
) -> *mut OakEngineMarker {
guard_ptr(|| unsafe {
if list.is_null() || index < 0 {
return Ok(std::ptr::null_mut());
}
let h = unbox(list)?;
let mut count: c_int = 0;
if tl::oaktimeline_marker_count(h, &mut count) != 0 || index >= count {
return Ok(std::ptr::null_mut());
}
// The module has no marker handle; the marker is represented as its
// (list, index) position.
let borrowed = h.addref();
Ok(box_marker(borrowed, index))
})
}
/// `oakengine_marker_list_marker_at_time` — marker by exact in-point.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_list_marker_at_time(
list: *const OakEngineMarkerList,
num: i64,
den: i64,
) -> *mut OakEngineMarker {
guard_ptr(|| unsafe {
if list.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(list)?;
let idx = marker_index_at(h, num, den);
if idx < 0 {
return Ok(std::ptr::null_mut());
}
let borrowed = h.addref();
Ok(box_marker(borrowed, idx))
})
}
/// `oakengine_marker_get_time` — the marker's time range as rational
/// seconds.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_get_time(
self_: *const OakEngineMarker,
in_num: *mut i64,
in_den: *mut i64,
out_num: *mut i64,
out_den: *mut i64,
) -> c_int {
guard(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let (list, index) = marker_unbox(self_)?;
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
index,
&mut n0,
&mut d0,
&mut n1,
&mut d1,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
return Err(Error::Module(rc));
}
if !in_num.is_null() {
*in_num = n0 as i64;
}
if !in_den.is_null() {
*in_den = d0 as i64;
}
if !out_num.is_null() {
*out_num = n1 as i64;
}
if !out_den.is_null() {
*out_den = d1 as i64;
}
Ok(())
})
}
/// `oakengine_marker_get_name` — the marker's name (buf/size).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_get_name(
self_: *const OakEngineMarker,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let (list, index) = marker_unbox(self_)?;
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list, index, &mut n0, &mut d0, &mut n1, &mut d1, &mut color, buf, buf_size,
);
if rc < 0 {
Err(Error::Module(rc))
} else {
Ok(string_result(rc))
}
})
}
/// `oakengine_marker_get_color` — the marker's color index (-1 on NULL).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_get_color(self_: *const OakEngineMarker) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(-1);
}
let (list, index) = marker_unbox(self_)?;
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
index,
&mut n0,
&mut d0,
&mut n1,
&mut d1,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
return Err(Error::Module(rc));
}
Ok(color)
})
}
/// `oakengine_marker_has_sibling_at_time` — 1 if the list has another
/// marker at the given time.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_has_sibling_at_time(
self_: *const OakEngineMarker,
num: i64,
den: i64,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let (list, index) = marker_unbox(self_)?;
// Scan the list for ANOTHER marker with the same in-point.
let mut count: c_int = 0;
if tl::oaktimeline_marker_count(list, &mut count) != 0 {
return Ok(0);
}
for i in 0..count {
if i == index {
continue;
}
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
i,
&mut n0,
&mut d0,
&mut n1,
&mut d1,
&mut color,
std::ptr::null_mut(),
0,
);
if rc >= 0 && n0 as i64 == num && d0 as i64 == den {
return Ok(1);
}
}
Ok(0)
})
}
/// `oakengine_marker_set_time_live` — set the marker's time range directly.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_set_time_live(
self_: *mut OakEngineMarker,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
) -> c_int {
guard(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let (list, index) = marker_unbox(self_)?;
// The module has no live marker time setter; the change is applied
// through the undoable MarkerChangeTimeCommand (documented deviation
// from the non-undoable contract).
let cmd = tl::oaktimeline_marker_set_time_command(
list,
index,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
);
if cmd.is_null() {
return Err(Error::Failed("set marker time command failed".into()));
}
push_command(cmd, "Move Marker")
})
}
/// `oakengine_marker_commit_time` — commit a time change as an undoable
/// command (optionally into `command`).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_commit_time(
self_: *mut OakEngineMarker,
old_in_num: i64,
old_in_den: i64,
old_out_num: i64,
old_out_den: i64,
new_in_num: i64,
new_in_den: i64,
new_out_num: i64,
new_out_den: i64,
command: *mut c_void,
) -> c_int {
guard(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let (list, index) = marker_unbox(self_)?;
// The module's MarkerChangeTimeCommand takes only the new range and
// captures the old range from the list at redo time; the explicit
// old range is validated against the marker instead.
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
index,
&mut n0,
&mut d0,
&mut n1,
&mut d1,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
return Err(Error::Module(rc));
}
let _ = (old_in_num, old_in_den, old_out_num, old_out_den);
let cmd = tl::oaktimeline_marker_set_time_command(
list,
index,
new_in_num as c_int,
new_in_den as c_int,
new_out_num as c_int,
new_out_den as c_int,
);
if cmd.is_null() {
return Err(Error::Failed("set marker time command failed".into()));
}
if command.is_null() {
push_command(cmd, "Move Marker")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
}
/// `oakengine_marker_set_time_command` — create a MarkerChangeTimeCommand.
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_set_time_command(
marker: *mut OakEngineMarker,
new_time_num: i64,
new_time_den: i64,
) -> *mut c_void {
guard_ptr(|| unsafe {
if marker.is_null() || new_time_den == 0 {
return Ok(std::ptr::null_mut());
}
let (list, index) = marker_unbox(marker)?;
// The marker's out offset is preserved (new range = new in point +
// old length).
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut color: c_int = 0;
let rc = tl::oaktimeline_marker_at(
list,
index,
&mut n0,
&mut d0,
&mut n1,
&mut d1,
&mut color,
std::ptr::null_mut(),
0,
);
if rc < 0 {
return Ok(std::ptr::null_mut());
}
// The marker's out offset is preserved: new out = new in + old length.
let (off_num, off_den) = rat_sub(n1 as i64, d1 as i64, n0 as i64, d0 as i64);
let (out_num, out_den) = rat_add(new_time_num, new_time_den, off_num, off_den);
let cmd = tl::oaktimeline_marker_set_time_command(
list,
index,
new_time_num as c_int,
new_time_den as c_int,
out_num as c_int,
out_den as c_int,
);
if cmd.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(command_box(cmd)?.cast())
})
}
/// `oakengine_marker_remove` — remove the marker from its list (undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_remove(self_: *mut OakEngineMarker) -> c_int {
guard(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let (list, index) = marker_unbox(self_)?;
let cmd = tl::oaktimeline_marker_remove_at_command(list, index);
if cmd.is_null() {
return Err(Error::Failed("remove marker command failed".into()));
}
push_command(cmd, "Remove Marker")
})
}
/// `oakengine_marker_set_properties` — batch-set properties on one or more
/// markers (one undoable command).
#[no_mangle]
pub unsafe extern "C" fn oakengine_marker_set_properties(
markers: *mut *mut OakEngineMarker,
count: c_int,
color: c_int,
name: *const c_char,
move_time: c_int,
new_in_num: i64,
new_in_den: i64,
new_out_num: i64,
new_out_den: i64,
command: *mut c_void,
) -> c_int {
guard(|| unsafe {
if markers.is_null() || count <= 0 {
return Err(Error::Invalid);
}
let mut children: Vec<CHandle> = Vec::new();
let slice = std::slice::from_raw_parts(markers, count as usize);
for (_i, m) in slice.iter().enumerate() {
let (list, index) = match marker_unbox(*m) {
Ok(pair) => pair,
Err(_) => continue,
};
if color >= 0 || !name.is_null() {
let name_c = if name.is_null() {
None
} else {
Some(
std::ffi::CString::new(read_cstr(name))
.map_err(|_| Error::Failed("invalid name".into()))?,
)
};
let name_ptr = match &name_c {
Some(c) => c.as_ptr(),
None => std::ptr::null(),
};
let cmd = tl::oaktimeline_marker_set_props_command(list, index, color, name_ptr);
if !cmd.is_null() {
children.push(cmd);
}
}
if move_time != 0 && count == 1 {
let cmd = tl::oaktimeline_marker_set_time_command(
list,
index,
new_in_num as c_int,
new_in_den as c_int,
new_out_num as c_int,
new_out_den as c_int,
);
if !cmd.is_null() {
children.push(cmd);
}
}
}
if children.is_empty() {
return Ok(());
}
if command.is_null() {
push_multi_commands(&children, "Set Marker Properties")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
for child in &children {
let rc = u::command_multi_add_child(parent, *child);
if rc != 0 {
return Err(Error::Module(rc));
}
}
Ok(())
}
})
}
/* ---- Workarea handle family ------------------------------------------------- */
/// `oakengine_workarea_create` — create a standalone workarea.
#[no_mangle]
pub extern "C" fn oakengine_workarea_create() -> *mut OakEngineWorkarea {
guard_ptr(|| {
let wa = unsafe { tl::oaktimeline_workarea_create() };
if wa.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineWorkarea>(wa))
})
}
/// `oakengine_workarea_free` — free a standalone workarea (NULL-safe).
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_free(wa: *mut OakEngineWorkarea) {
guard_void(|| unsafe {
free_box::<OakEngineWorkarea>(wa);
})
}
/// `oakengine_workarea_get` — read the workarea state.
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_get(
self_: *const OakEngineWorkarea,
in_num: *mut i64,
in_den: *mut i64,
out_num: *mut i64,
out_den: *mut i64,
enabled: *mut c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let mut en: c_int = 0;
let rc = tl::oaktimeline_workarea_get(h, &mut n0, &mut d0, &mut n1, &mut d1, &mut en);
Error::from_module(rc)?;
if !in_num.is_null() {
*in_num = n0 as i64;
}
if !in_den.is_null() {
*in_den = d0 as i64;
}
if !out_num.is_null() {
*out_num = n1 as i64;
}
if !out_den.is_null() {
*out_den = d1 as i64;
}
if !enabled.is_null() {
*enabled = en;
}
Ok(())
})
}
/// `oakengine_workarea_set_range` — set the workarea range (non-undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_set_range(
self_: *mut OakEngineWorkarea,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
Error::from_module(tl::oaktimeline_workarea_set_range(
h,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
))
})
}
/// `oakengine_workarea_set_enabled` — enable/disable (non-undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_set_enabled(
self_: *mut OakEngineWorkarea,
enabled: c_int,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
Error::from_module(tl::oaktimeline_workarea_set_enabled(h, enabled))
})
}
/// `oakengine_workarea_set_range_undoable` — set the range with undo
/// support.
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_set_range_undoable(
self_: *mut OakEngineWorkarea,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
old_in_num: i64,
old_in_den: i64,
old_out_num: i64,
old_out_den: i64,
command: *mut c_void,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let cmd = tl::oaktimeline_workarea_set_range_command(
h,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
old_in_num as c_int,
old_in_den as c_int,
old_out_num as c_int,
old_out_den as c_int,
);
if cmd.is_null() {
return Err(Error::Failed("workarea range command failed".into()));
}
if command.is_null() {
push_command(cmd, "Set Workarea Range")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
}
/// `oakengine_workarea_set_enabled_undoable` — enable/disable with undo
/// support.
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_set_enabled_undoable(
self_: *mut OakEngineWorkarea,
enabled: c_int,
command: *mut c_void,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let cmd = tl::oaktimeline_workarea_set_enabled_command(h, enabled);
if cmd.is_null() {
return Err(Error::Failed("workarea enabled command failed".into()));
}
if command.is_null() {
push_command(cmd, "Set Workarea Enabled")
} else {
let parent = unbox(command.cast::<OakEngineClipboard>())?;
let rc = u::command_multi_add_child(parent, cmd);
Error::from_module(rc)
}
})
}
/// `oakengine_workarea_reset_in_out` — fill the reset sentinel values.
#[no_mangle]
pub unsafe extern "C" fn oakengine_workarea_reset_in_out(
in_num: *mut i64,
in_den: *mut i64,
out_num: *mut i64,
out_den: *mut i64,
) {
guard_void(|| unsafe {
let mut n0: c_int = 0;
let mut d0: c_int = 0;
let mut n1: c_int = 0;
let mut d1: c_int = 0;
let rc = tl::oaktimeline_workarea_reset(&mut n0, &mut d0, &mut n1, &mut d1);
if rc != 0 {
return;
}
if !in_num.is_null() {
*in_num = n0 as i64;
}
if !in_den.is_null() {
*in_den = d0 as i64;
}
if !out_num.is_null() {
*out_num = n1 as i64;
}
if !out_den.is_null() {
*out_den = d1 as i64;
}
})
}
/* ---- Clip media range / cache / media in ----------------------------------- */
/// `oakengine_clip_get_media_range_rational` — clip media range as
/// rational seconds.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_media_range_rational(
self_: *const OakEngineClip,
in_num: *mut i64,
in_den: *mut i64,
out_num: *mut i64,
out_den: *mut i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut mi_num: c_int = 0;
let mut mi_den: c_int = 0;
let mut len_num: c_int = 0;
let mut len_den: c_int = 0;
Error::from_module(n::oaknode_clip_get_media_in(h, &mut mi_num, &mut mi_den))?;
Error::from_module(n::oaknode_block_get_length(h, &mut len_num, &mut len_den))?;
// media_out = media_in + length (speed/reverse ignored, like the
// capi).
let (mo_num, mo_den) =
rat_add(mi_num as i64, mi_den as i64, len_num as i64, len_den as i64);
if !in_num.is_null() {
*in_num = mi_num as i64;
}
if !in_den.is_null() {
*in_den = mi_den as i64;
}
if !out_num.is_null() {
*out_num = mo_num;
}
if !out_den.is_null() {
*out_den = mo_den;
}
Ok(())
})
}
/// `oakengine_clip_get_media_in_rational` — clip media in-point as rational
/// seconds.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_media_in_rational(
self_: *const OakEngineClip,
num: *mut i64,
den: *mut i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(self_)?;
let mut n: c_int = 0;
let mut d: c_int = 0;
Error::from_module(n::oaknode_clip_get_media_in(h, &mut n, &mut d))?;
if !num.is_null() {
*num = n as i64;
}
if !den.is_null() {
*den = d as i64;
}
Ok(())
})
}
/// `oakengine_clip_set_media_in` — move the clip's media in-point (as a
/// frame timestamp; undoable when `undoable` != 0).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_set_media_in(
self_: *mut OakEngineClip,
media_in_ts: i64,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let h = match unbox(self_) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid clip handle");
return Err(Error::Invalid);
}
};
// The clip's sequence provides the timebase.
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
set_seq_error("clip is not on a track");
return Err(Error::State);
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
release_handle(track);
if rc != 0 || sequence.is_null() {
set_seq_error("clip is not on a track");
return Err(Error::State);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(media_in_ts, tb);
apply_clip_media_in(h, num as c_int, den as c_int, undoable)
})
}
/// Shared media-in write honoring the undoable flag (the capi's
/// `BlockSetMediaInCommand` vs the direct setter).
///
/// # Safety
/// `clip` must be a live module clip handle.
unsafe fn apply_clip_media_in(
clip: CHandle,
num: c_int,
den: c_int,
undoable: c_int,
) -> Result<()> {
unsafe {
if undoable != 0 {
let mut old_num: c_int = 0;
let mut old_den: c_int = 0;
Error::from_module(n::oaknode_clip_get_media_in(
clip,
&mut old_num,
&mut old_den,
))?;
let data = Box::into_raw(Box::new(ClipMediaInCmdData {
clip: clip.addref(),
old_num,
old_den,
new_num: num,
new_den: den,
}));
let cmd = vtable_command(
clip_media_in_redo,
clip_media_in_undo,
clip_media_in_free,
data as *mut c_void,
)?;
push_command(cmd, "Set Media In")
} else {
Error::from_module(n::oaknode_clip_set_media_in(clip, num, den))
}
}
}
/// `oakengine_clip_set_media_in_rational` — move the media in-point as a
/// rational seconds value.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_set_media_in_rational(
self_: *mut OakEngineClip,
num: i64,
den: i64,
undoable: c_int,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
let h = match unbox(self_) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid clip handle");
return Err(Error::Invalid);
}
};
if den == 0 {
set_seq_error("invalid rational denominator");
return Err(Error::Invalid);
}
apply_clip_media_in(h, num as c_int, den as c_int, undoable)
})
}
/// `oakengine_clip_request_invalidate` — request cache invalidation
/// (NULL-safe no-op).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_request_invalidate(
self_: *mut OakEngineClip,
in_ts: i64,
out_ts: i64,
type_: c_int,
) {
guard_void(|| {
// Stub: matches the capi's headless no-op (the module has no cache
// invalidation surface).
let _ = (self_, in_ts, out_ts, type_);
})
}
/// `oakengine_clip_add_cache_passthrough` — add a cache passthrough
/// dependency (NULL-safe no-op).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_add_cache_passthrough(
dest: *mut OakEngineClip,
source: *mut OakEngineClip,
) {
guard_void(|| unsafe {
if dest.is_null() || source.is_null() {
return;
}
// The module's passthrough is itself a no-op until per-node caches
// exist; forwarded for parity.
let d = match unbox(dest) {
Ok(h) => h,
Err(_) => return,
};
let s = match unbox(source) {
Ok(h) => h,
Err(_) => return,
};
n::oaknode_clip_add_cache_passthrough_from(d, s);
})
}
/// `oakengine_clip_discard_cache` — discard the clip's cache (NULL-safe
/// no-op).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_discard_cache(self_: *mut OakEngineClip) {
guard_void(|| {
// Stub: matches the capi's headless no-op.
let _ = self_;
})
}
/// `oakengine_clip_create_empty` — create a new empty ClipBlock (caller
/// owns it).
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_create_empty(label: *const c_char) -> *mut OakEngineClip {
guard_ptr(|| unsafe {
let clip = n::oaknode_block_clip_create();
if clip.is_null() {
return Ok(std::ptr::null_mut());
}
if !label.is_null() {
let label_c = std::ffi::CString::new(read_cstr(label))
.map_err(|_| Error::Failed("invalid label".into()))?;
let node = n::oaknode_block_as_node(clip);
let rc = n::oaknode_node_set_label(node, label_c.as_ptr());
release_handle(node);
if rc != 0 {
return Err(Error::Module(rc));
}
}
Ok(box_handle::<OakEngineClip>(clip))
})
}
/// `oakengine_clip_request_invalidate_connected` — request invalidated cache
/// ranges from the connected node.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_request_invalidate_connected(
self_: *mut OakEngineClip,
force_all: c_int,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
) {
guard_void(|| {
// Stub: the module clip has no buffer input and no
// `request_invalidated_from_connected` surface; matches the capi's
// headless behavior.
let _ = (self_, force_all, in_num, in_den, out_num, out_den);
})
}
/* ---- Block functions ------------------------------------------------------- */
/// `oakengine_block_is_enabled` — 1 if the block is enabled.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_is_enabled(self_: *const OakEngineBlock) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let h = unbox(self_)?;
let mut enabled: c_int = 0;
Error::from_module(n::oaknode_block_get_enabled(h, &mut enabled))?;
Ok(enabled)
})
}
/// `oakengine_block_set_enabled` — enable or disable the block (undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_set_enabled(
self_: *mut OakEngineBlock,
enabled: c_int,
) -> c_int {
guard(|| unsafe {
if self_.is_null() {
return Err(Error::Invalid);
}
let h = unbox(self_)?;
let mut old_enabled: c_int = 0;
Error::from_module(n::oaknode_block_get_enabled(h, &mut old_enabled))?;
let data = Box::into_raw(Box::new(BlockEnabledCmdData {
block: h.addref(),
old_enabled,
new_enabled: enabled,
}));
let cmd = vtable_command(
block_enabled_redo,
block_enabled_undo,
block_enabled_free,
data as *mut c_void,
)?;
push_command(cmd, "Set Block Enabled")
})
}
/* ---- Block traversal -------------------------------------------------------- */
/// `oakengine_track_block_count` — number of blocks (including gaps).
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_block_count(track: *const OakEngineTrack) -> c_int {
guard_int(|| unsafe {
let h = unbox(track)?;
let mut count: c_int = 0;
Error::from_module(n::oaknode_track_get_block_count(h, &mut count))?;
Ok(count)
})
}
/// `oakengine_track_block_at` — block at `index` (0-based, includes gaps).
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_block_at(
track: *const OakEngineTrack,
index: c_int,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() || index < 0 {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
let mut block = CHandle::null();
let rc = n::oaknode_track_get_block_at(h, index, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_track_block_at_time` — block containing the timestamp.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_block_at_time(
track: *const OakEngineTrack,
timestamp: i64,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
// The track's owning sequence timebase.
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(h, &mut sequence);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Ok(std::ptr::null_mut());
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(timestamp, tb);
let mut block = CHandle::null();
let rc =
n::oaknode_track_get_block_containing_time(h, num as c_int, den as c_int, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// Shared "nearest block at timestamp" helpers.
///
/// # Safety
/// `track` must be a live module track handle.
unsafe fn track_nearest_boxed(
track: *const OakEngineTrack,
timestamp: i64,
via: fn(CHandle, c_int, c_int, *mut CHandle) -> c_int,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(h, &mut sequence);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Ok(std::ptr::null_mut());
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(timestamp, tb);
let mut block = CHandle::null();
let rc = via(h, num as c_int, den as c_int, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_track_nearest_block_before` — nearest block whose out-point is
/// strictly before `timestamp`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_nearest_block_before(
track: *const OakEngineTrack,
timestamp: i64,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(h, &mut sequence);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Ok(std::ptr::null_mut());
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(timestamp, tb);
let block = nearest_block_before(h, num, den);
if block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_track_nearest_block_after` — nearest block whose in-point is
/// strictly after `timestamp`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_nearest_block_after(
track: *const OakEngineTrack,
timestamp: i64,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(h, &mut sequence);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Ok(std::ptr::null_mut());
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(timestamp, tb);
// The module exposes only the after-or-at variant; the strictly-after
// result is the first block whose in-point is strictly after the
// time.
let mut best = CHandle::null();
let mut block_count: c_int = 0;
if n::oaknode_track_get_block_count(h, &mut block_count) != 0 {
return Ok(std::ptr::null_mut());
}
for i in 0..block_count {
let mut b = CHandle::null();
if n::oaknode_track_get_block_at(h, i, &mut b) != 0 || b.is_null() {
continue;
}
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let after = n::oaknode_block_get_in(b, &mut in_num, &mut in_den) == 0
&& rat_cmp(in_num as i64, in_den as i64, num, den) == std::cmp::Ordering::Greater;
if after {
best = b;
break;
}
release_handle(b);
}
if best.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(best))
})
}
/// `oakengine_track_nearest_block_before_or_at` — nearest block whose
/// out-point >= `timestamp`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_nearest_block_before_or_at(
track: *const OakEngineTrack,
timestamp: i64,
) -> *mut OakEngineBlock {
unsafe {
track_nearest_boxed(
track,
timestamp,
n::oaknode_track_get_nearest_block_before_or_at,
)
}
}
/// `oakengine_track_nearest_block_after_or_at` — nearest block whose
/// in-point <= `timestamp`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_nearest_block_after_or_at(
track: *const OakEngineTrack,
timestamp: i64,
) -> *mut OakEngineBlock {
unsafe {
track_nearest_boxed(
track,
timestamp,
n::oaknode_track_get_nearest_block_after_or_at,
)
}
}
/// `oakengine_block_is_gap` — 1 if the block is a GapBlock.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_is_gap(block: *const OakEngineBlock) -> c_int {
guard_int(|| unsafe {
if block.is_null() {
return Ok(0);
}
let h = unbox(block)?;
Ok(is_gap_block(h))
})
}
/// Whether the module block carries a gap behavior.
///
/// # Safety
/// `h` must be a live module block handle.
unsafe fn is_gap_block(h: CHandle) -> c_int {
unsafe {
let mut kind: c_int = 0;
if n::oaknode_block_get_kind(h, &mut kind) != 0 {
return 0;
}
if kind == BLOCK_KIND_GAP {
1
} else {
0
}
}
}
/// `oakengine_block_get_track` — the block's track.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_get_track(
block: *const OakEngineBlock,
) -> *mut OakEngineTrack {
guard_ptr(|| unsafe {
if block.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(block)?;
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineTrack>(track))
})
}
/// `oakengine_block_next` — next block in the track's linked list.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_next(block: *const OakEngineBlock) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if block.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(block)?;
let mut next = CHandle::null();
let rc = n::oaknode_block_get_next(h, &mut next);
if rc != 0 || next.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(next))
})
}
/// `oakengine_block_prev` — previous block in the track's linked list.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_prev(block: *const OakEngineBlock) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if block.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(block)?;
let mut prev = CHandle::null();
let rc = n::oaknode_block_get_previous(h, &mut prev);
if rc != 0 || prev.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(prev))
})
}
/// `oakengine_block_get_range` — block range as timestamps in the owning
/// track's sequence timebase.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_get_range(
block: *const OakEngineBlock,
in_: *mut i64,
out: *mut i64,
) -> c_int {
guard(|| unsafe {
let h = unbox(block)?;
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
return Err(Error::Invalid);
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
release_handle(track);
if rc != 0 || sequence.is_null() {
return Err(Error::Invalid);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Err(Error::Invalid);
}
};
release_handle(sequence);
let mut in_num: c_int = 0;
let mut in_den: c_int = 0;
let mut out_num: c_int = 0;
let mut out_den: c_int = 0;
Error::from_module(n::oaknode_block_get_in(h, &mut in_num, &mut in_den))?;
Error::from_module(n::oaknode_block_get_out(h, &mut out_num, &mut out_den))?;
if !in_.is_null() {
*in_ = rational_to_ts(in_num as i64, in_den as i64, tb);
}
if !out.is_null() {
*out = rational_to_ts(out_num as i64, out_den as i64, tb);
}
Ok(())
})
}
/* ---- Clip input ID getters -------------------------------------------------- */
/// `oakengine_clip_buffer_input_id` — `ClipBlock::k_buffer_in`.
#[no_mangle]
pub extern "C" fn oakengine_clip_buffer_input_id() -> *const c_char {
b"buffer_in\0".as_ptr() as *const c_char
}
/// `oakengine_clip_speed_input_id` — `ClipBlock::k_speed_input`.
#[no_mangle]
pub extern "C" fn oakengine_clip_speed_input_id() -> *const c_char {
b"speed_in\0".as_ptr() as *const c_char
}
/// `oakengine_clip_reverse_input_id` — `ClipBlock::k_reverse_input`.
#[no_mangle]
pub extern "C" fn oakengine_clip_reverse_input_id() -> *const c_char {
b"reverse_in\0".as_ptr() as *const c_char
}
/// `oakengine_clip_maintain_audio_pitch_input_id` — the maintain-audio-pitch
/// input.
#[no_mangle]
pub extern "C" fn oakengine_clip_maintain_audio_pitch_input_id() -> *const c_char {
b"maintain_audio_pitch_in\0".as_ptr() as *const c_char
}
/// `oakengine_clip_loop_mode_input_id` — `ClipBlock::k_loop_mode_input`.
#[no_mangle]
pub extern "C" fn oakengine_clip_loop_mode_input_id() -> *const c_char {
b"loop_in\0".as_ptr() as *const c_char
}
/// `oakengine_clip_auto_cache_input_id` — `ClipBlock::k_auto_cache_input`.
#[no_mangle]
pub extern "C" fn oakengine_clip_auto_cache_input_id() -> *const c_char {
b"autocache_in\0".as_ptr() as *const c_char
}
/* ---- Sequence: add_default_nodes -------------------------------------------- */
/// `oakengine_sequence_add_default_nodes` — add one video and one audio
/// track as ONE undoable command.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_default_nodes(
seq: *mut OakEngineSequence,
) -> c_int {
guard(|| unsafe {
if seq.is_null() {
return Err(Error::Invalid);
}
let h = unbox(seq)?;
let mut video_list = CHandle::null();
let mut audio_list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
h,
TRACK_TYPE_VIDEO,
&mut video_list,
))?;
Error::from_module(n::oaknode_sequence_get_track_list(
h,
TRACK_TYPE_AUDIO,
&mut audio_list,
))?;
let vcmd = tl::oaktimeline_add_track_command(video_list);
let acmd = tl::oaktimeline_add_track_command(audio_list);
if vcmd.is_null() || acmd.is_null() {
release_handle(video_list);
release_handle(audio_list);
return Err(Error::Failed("add track command failed".into()));
}
let children = [vcmd, acmd];
push_multi_commands(&children, "Add Default Nodes")?;
// The two commands' redos registered their tracks in the video and
// audio lists already (see `oakengine_sequence_add_track`); no
// separate registration is needed.
release_handle(video_list);
release_handle(audio_list);
Ok(())
})
}
/* ---- Sequence: add_sequence_clip -------------------------------------------- */
/// `oakengine_sequence_add_sequence_clip` — place a nested Sequence as a
/// clip on a track.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_add_sequence_clip(
seq: *mut OakEngineSequence,
nested: *mut OakEngineSequence,
track_type: c_int,
track_index: c_int,
in_: i64,
out: i64,
media_in: i64,
) -> *mut OakEngineClip {
guard_ptr(|| unsafe {
set_seq_error("");
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence handles");
return Ok(std::ptr::null_mut());
}
};
let nested_h = match unbox(nested) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid sequence handles");
return Ok(std::ptr::null_mut());
}
};
if track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
set_seq_error("invalid track type");
return Ok(std::ptr::null_mut());
}
if track_type != TRACK_TYPE_VIDEO && track_type != TRACK_TYPE_AUDIO {
set_seq_error("subtitle sequence clips not supported");
return Ok(std::ptr::null_mut());
}
if sequence.ctx == nested_h.ctx {
set_seq_error("a sequence cannot nest itself");
return Ok(std::ptr::null_mut());
}
// Cross-project check.
let p1 = seq_project_of(sequence);
let p2 = seq_project_of(nested_h);
let same = !p1.is_null() && !p2.is_null() && p1.ctx == p2.ctx;
release_handle(p1);
release_handle(p2);
if !same {
set_seq_error("sequence belongs to a different project");
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
set_seq_error("sequence has no valid frame rate");
return Ok(std::ptr::null_mut());
}
};
if out <= in_ || in_ < 0 || media_in < 0 {
set_seq_error("invalid range");
return Ok(std::ptr::null_mut());
}
let mut list = CHandle::null();
if n::oaknode_sequence_get_track_list(sequence, track_type, &mut list) != 0
|| list.is_null()
{
set_seq_error("sequence has no track list for this type");
return Ok(std::ptr::null_mut());
}
let mut track_count: c_int = 0;
if n::oaknode_tracklist_get_track_count(list, &mut track_count) != 0
|| track_index < 0
|| track_index >= track_count
{
release_handle(list);
set_seq_error(&format!("no track at index {}", track_index));
return Ok(std::ptr::null_mut());
}
let (len_num, len_den) = ts_to_rational(out - in_, tb);
let (mi_num, mi_den) = ts_to_rational(media_in, tb);
let clip = n::oaknode_block_clip_create();
if clip.is_null() {
release_handle(list);
set_seq_error("clip creation failed");
return Ok(std::ptr::null_mut());
}
// Set length first, then media in (set_length_and_media_in modifies
// media in internally).
Error::from_module(n::oaknode_block_set_length_and_media_in(
clip,
len_num as c_int,
len_den as c_int,
))?;
Error::from_module(n::oaknode_clip_set_media_in(
clip,
mi_num as c_int,
mi_den as c_int,
))?;
let project = seq_project_of(sequence);
let add_cmd = n::oaknode_command_create_add_node(project, clip);
release_handle(project);
if add_cmd.is_null() {
release_handle(list);
set_seq_error("node add command failed");
return Ok(std::ptr::null_mut());
}
// As with footage clips, the module clip has no `buffer_in` input, so
// the nested-sequence connection cannot be built.
let clip_node = n::oaknode_block_as_node(clip);
let mut edge = CHandle::null();
let rc =
n::oaknode_node_connect_undoable(nested_h, clip_node, c"buffer_in".as_ptr(), &mut edge);
release_handle(clip_node);
if rc != 0 {
release_handle(add_cmd);
release_handle(list);
set_seq_error("nested-sequence connection failed: module clips have no buffer input");
return Ok(std::ptr::null_mut());
}
let (in_num, in_den) = ts_to_rational(in_, tb);
let place_cmd =
tl::oaktimeline_place_block_command(list, track_index, clip, in_num, in_den);
release_handle(list);
if place_cmd.is_null() {
release_handle(add_cmd);
release_handle(edge);
set_seq_error("place block command failed");
return Ok(std::ptr::null_mut());
}
let children = [add_cmd, edge, place_cmd];
if let Err(e) = push_multi_commands(&children, "Add Sequence Clip") {
set_seq_error(&format!(
"failed to push add-sequence-clip command: {:?}",
e
));
return Err(e);
}
Ok(box_handle::<OakEngineClip>(clip))
})
}
/* ---- Track handle queries ---------------------------------------------------- */
/// `oakengine_sequence_track_at` — borrowed track handle.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_track_at(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
) -> *mut OakEngineTrack {
guard_ptr(|| unsafe {
if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Ok(std::ptr::null_mut());
}
let h = unbox(seq)?;
let mut track = CHandle::null();
let rc = n::oaknode_sequence_get_track_at(h, track_type, track_index, &mut track);
if rc != 0 || track.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineTrack>(track))
})
}
/// `oakengine_track_type` — track type, or -1 on a NULL handle.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_type(track: *const OakEngineTrack) -> c_int {
guard_int(|| unsafe {
if track.is_null() {
return Ok(-1);
}
let h = unbox(track)?;
let mut type_: c_int = 0;
Error::from_module(n::oaknode_track_get_type(h, &mut type_))?;
Ok(type_)
})
}
/// `oakengine_track_get_length` — track content length in frame timestamps.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_get_length(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
length: *mut i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if length.is_null() {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(track);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let mut num: c_int = 0;
let mut den: c_int = 0;
let rc = n::oaknode_track_get_length(track, &mut num, &mut den);
release_handle(track);
Error::from_module(rc)?;
*length = rational_to_ts(num as i64, den as i64, tb);
Ok(())
})
}
/// `oakengine_track_is_range_free` — 1 if [in_ts, out_ts) is free.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_is_range_free(
seq: *const OakEngineSequence,
track_type: c_int,
track_index: c_int,
in_ts: i64,
out_ts: i64,
) -> c_int {
guard_int(|| unsafe {
set_seq_error("");
if seq.is_null() || in_ts < 0 || out_ts <= in_ts {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let sequence = match unbox(seq) {
Ok(h) => h,
Err(_) => {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
};
let mut list = CHandle::null();
Error::from_module(n::oaknode_sequence_get_track_list(
sequence, track_type, &mut list,
))?;
let mut track = CHandle::null();
let rc = n::oaknode_tracklist_get_track_at(list, track_index, &mut track);
release_handle(list);
if rc != 0 || track.is_null() {
set_seq_error(&format!("no track at index {}", track_index));
return Err(Error::NotFound);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(track);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
let (in_num, in_den) = ts_to_rational(in_ts, tb);
let (out_num, out_den) = ts_to_rational(out_ts, tb);
let mut is_free: c_int = 0;
let rc = n::oaknode_track_is_range_free(
track,
in_num as c_int,
in_den as c_int,
out_num as c_int,
out_den as c_int,
&mut is_free,
);
release_handle(track);
Error::from_module(rc)?;
Ok(is_free)
})
}
/// `oakengine_track_height_default` — default track height in internal
/// units.
#[no_mangle]
pub extern "C" fn oakengine_track_height_default() -> f64 {
TRACK_HEIGHT_DEFAULT
}
/// `oakengine_track_default_height_in_pixels` — default track height in
/// pixels.
#[no_mangle]
pub extern "C" fn oakengine_track_default_height_in_pixels() -> c_int {
unsafe { n::oaknode_track_get_default_height_in_pixels() }
}
/// `oakengine_track_height_internal_to_pixels` — convert internal height to
/// pixels.
#[no_mangle]
pub extern "C" fn oakengine_track_height_internal_to_pixels(height: f64) -> c_int {
(height * TRACK_FONT_HEIGHT).round() as c_int
}
/// `oakengine_track_height_pixels_to_internal` — convert pixels to internal
/// height.
#[no_mangle]
pub extern "C" fn oakengine_track_height_pixels_to_internal(pixels: c_int) -> f64 {
pixels as f64 / TRACK_FONT_HEIGHT
}
/// `oakengine_track_height_interval` — track height step interval.
#[no_mangle]
pub extern "C" fn oakengine_track_height_interval() -> f64 {
TRACK_HEIGHT_INTERVAL
}
/// `oakengine_track_height_minimum` — minimum track height.
#[no_mangle]
pub extern "C" fn oakengine_track_height_minimum() -> f64 {
TRACK_HEIGHT_MINIMUM
}
/* ---- Multicam helpers --------------------------------------------------------- */
/// `oakengine_clip_find_multicam` — find the MultiCamNode ancestor of a
/// node.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_find_multicam(
node: *mut OakEngineNode,
) -> *mut OakEngineNode {
guard_ptr(|| {
// Stub: the C++ walks the clip's buffer-input chain looking for a
// MultiCamNode; module clips have no buffer input, so no ancestor can
// be reached.
let _ = node;
Ok(std::ptr::null_mut())
})
}
/// `oakengine_multicam_switch_source` — switch the multicam source.
#[no_mangle]
pub unsafe extern "C" fn oakengine_multicam_switch_source(
multicam_node: *mut OakEngineNode,
footage_node: *mut OakEngineNode,
track_type: c_int,
track_index: c_int,
time_seconds: f64,
command: *mut c_void,
) -> c_int {
guard(|| {
if multicam_node.is_null() {
return Err(Error::Invalid);
}
// Stub: matches the capi — multicam switching requires complex undo
// commands the module does not expose; the call is accepted and
// ignored (the capi's `Q_UNUSED` body).
let _ = (footage_node, track_type, track_index, time_seconds, command);
unsafe { unbox(multicam_node)? };
Ok(())
})
}
/* ---- Track lists, block/clip/transition navigation and links ---------------- */
/// `oakengine_sequence_track_list` — borrowed track list handle.
#[no_mangle]
pub unsafe extern "C" fn oakengine_sequence_track_list(
seq: *mut OakEngineSequence,
track_type: c_int,
) -> *mut OakEngineTrackList {
guard_ptr(|| unsafe {
if seq.is_null() || track_type < TRACK_TYPE_VIDEO || track_type > TRACK_TYPE_SUBTITLE {
return Ok(std::ptr::null_mut());
}
let h = unbox(seq)?;
let mut list = CHandle::null();
let rc = n::oaknode_sequence_get_track_list(h, track_type, &mut list);
if rc != 0 || list.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineTrackList>(list))
})
}
/// `oakengine_track_visible_block_at_time` — the block visible at `time_ts`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_track_visible_block_at_time(
track: *mut OakEngineTrack,
time_ts: i64,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if track.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(track)?;
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(h, &mut sequence);
if rc != 0 || sequence.is_null() {
return Ok(std::ptr::null_mut());
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
return Ok(std::ptr::null_mut());
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(time_ts, tb);
let mut block = CHandle::null();
let rc =
n::oaknode_track_get_visible_block_at_time(h, num as c_int, den as c_int, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_node_is_block` — 1 if the node is a block of any kind.
#[no_mangle]
pub unsafe extern "C" fn oakengine_node_is_block(node: *const OakEngineNode) -> c_int {
guard_int(|| unsafe {
if node.is_null() {
return Ok(0);
}
let h = unbox(node)?;
let block = n::oaknode_block_from_node(h);
let is_block = !block.is_null();
release_handle(block);
Ok(is_block as c_int)
})
}
/// `oakengine_node_is_transition` — 1 if the node is a transition block.
#[no_mangle]
pub unsafe extern "C" fn oakengine_node_is_transition(node: *const OakEngineNode) -> c_int {
guard_int(|| unsafe {
if node.is_null() {
return Ok(0);
}
let h = unbox(node)?;
// The module has exactly one transition type, identified by its type
// id (the C++ class check has no module equivalent).
Ok((node_type_id(h) == TYPE_ID_TRANSITION) as c_int)
})
}
/// `oakengine_block_set_length_and_media_out` — set the block's length
/// keeping its in point (undoable).
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_set_length_and_media_out(
block: *mut OakEngineBlock,
length_ts: i64,
) -> c_int {
guard(|| unsafe {
set_seq_error("");
if block.is_null() || length_ts <= 0 {
set_seq_error("invalid arguments");
return Err(Error::Invalid);
}
let h = unbox(block)?;
let mut track = CHandle::null();
let rc = n::oaknode_block_get_track(h, &mut track);
if rc != 0 || track.is_null() {
set_seq_error("block is not on a track");
return Err(Error::State);
}
let mut sequence = CHandle::null();
let rc = n::oaknode_track_get_sequence(track, &mut sequence);
release_handle(track);
if rc != 0 || sequence.is_null() {
set_seq_error("block is not on a track");
return Err(Error::State);
}
let tb = match seq_time_base(sequence) {
Ok(tb) => tb,
Err(_) => {
release_handle(sequence);
set_seq_error("sequence has no valid frame rate");
return Err(Error::State);
}
};
release_handle(sequence);
let (num, den) = ts_to_rational(length_ts, tb);
let mut old_num: c_int = 0;
let mut old_den: c_int = 0;
Error::from_module(n::oaknode_block_get_length(h, &mut old_num, &mut old_den))?;
let data = Box::into_raw(Box::new(BlockResizeCmdData {
block: h.addref(),
old_num,
old_den,
new_num: num as c_int,
new_den: den as c_int,
}));
let cmd = vtable_command(
block_resize_redo,
block_resize_undo,
block_resize_free,
data as *mut c_void,
)?;
push_command(cmd, "Set Block Length")
})
}
/// `oakengine_block_link_count` — blocks linked to this block.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_link_count(block: *const OakEngineBlock) -> c_int {
guard_int(|| unsafe {
if block.is_null() {
return Ok(0);
}
let h = unbox(block)?;
let mut count: c_int = 0;
Error::from_module(n::oaknode_block_get_link_count(h, &mut count))?;
Ok(count)
})
}
/// `oakengine_block_link_at` — linked block at `index`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_block_link_at(
block: *const OakEngineBlock,
index: c_int,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if block.is_null() || index < 0 {
return Ok(std::ptr::null_mut());
}
let h = unbox(block)?;
let mut linked = CHandle::null();
let rc = n::oaknode_block_get_link_at(h, index, &mut linked);
if rc != 0 || linked.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(linked))
})
}
/// `oakengine_clip_in_transition` — the transition at the clip's in-point.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_in_transition(
clip: *const OakEngineBlock,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if clip.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(clip)?;
// The in-transition is the previous block of the track when it is a
// transition.
let mut prev = CHandle::null();
let rc = n::oaknode_block_get_previous(h, &mut prev);
if rc != 0 || prev.is_null() {
return Ok(std::ptr::null_mut());
}
let node = n::oaknode_block_as_node(prev);
let is_transition = node_type_id(node) == TYPE_ID_TRANSITION;
release_handle(node);
if !is_transition {
release_handle(prev);
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(prev))
})
}
/// `oakengine_clip_out_transition` — the transition at the clip's out-point.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_out_transition(
clip: *const OakEngineBlock,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if clip.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(clip)?;
let mut next = CHandle::null();
let rc = n::oaknode_block_get_next(h, &mut next);
if rc != 0 || next.is_null() {
return Ok(std::ptr::null_mut());
}
let node = n::oaknode_block_as_node(next);
let is_transition = node_type_id(node) == TYPE_ID_TRANSITION;
release_handle(node);
if !is_transition {
release_handle(next);
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(next))
})
}
/// `oakengine_transition_connected_in_block` — the block feeding the
/// transition's in side.
#[no_mangle]
pub unsafe extern "C" fn oakengine_transition_connected_in_block(
transition: *const OakEngineBlock,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if transition.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(transition)?;
let mut block = CHandle::null();
let rc = n::oaknode_transition_get_connected_in_block(h, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_transition_connected_out_block` — the block feeding the
/// transition's out side.
#[no_mangle]
pub unsafe extern "C" fn oakengine_transition_connected_out_block(
transition: *const OakEngineBlock,
) -> *mut OakEngineBlock {
guard_ptr(|| unsafe {
if transition.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(transition)?;
let mut block = CHandle::null();
let rc = n::oaknode_transition_get_connected_out_block(h, &mut block);
if rc != 0 || block.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineBlock>(block))
})
}
/// `oakengine_clip_get_connected_viewer` — the node feeding the clip's
/// buffer input.
#[no_mangle]
pub unsafe extern "C" fn oakengine_clip_get_connected_viewer(
clip: *const OakEngineBlock,
) -> *mut OakEngineNode {
guard_ptr(|| unsafe {
if clip.is_null() {
return Ok(std::ptr::null_mut());
}
let h = unbox(clip)?;
// Module clips have no `buffer_in` input, so there is no connected
// viewer (mirrors the C++ result for an unconnected clip).
let node = n::oaknode_block_as_node(h);
let mut out = CHandle::null();
let rc = n::oaknode_node_input_get_connected_node(node, c"buffer_in".as_ptr(), &mut out);
release_handle(node);
if rc != 0 || out.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineNode>(out))
})
}