refactor: workspace layout — crates/, app at root, legacy C++ removed

Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
2026-08-10 20:24:25 +08:00
parent f8540e3892
commit 013a175707
4212 changed files with 8331 additions and 2274987 deletions
+251
View File
@@ -0,0 +1,251 @@
// 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/>.
// 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/>.
//! oakcodec C ABI imports. The task module reaches the codec side through
//! `include/codec/task.h` (the task submitter), `include/codec/decoder.h`
//! (conform/proxy work) and `include/codec/encoder.h` (export). Signatures
//! mirror the headers verbatim.
use std::ffi::{c_char, c_int, c_uint, c_void};
use crate::bridge::render::OakCancelAtom;
use crate::handle::CHandle;
/// `OakCodecTaskKind` enum values (`include/codec/task.h`).
pub const OAKCODEC_TASK_CONFORM: c_int = 0;
/// `OakCodecTaskKind::OAKCODEC_TASK_PROXY`.
pub const OAKCODEC_TASK_PROXY: c_int = 1;
/// `OAKCODEC_E_CANCELLED` (`include/codec/error.h`).
pub const OAKCODEC_E_CANCELLED: c_int = -50006;
/// `OAKCODEC_E_INVALID` (`include/codec/error.h`).
pub const OAKCODEC_E_INVALID: c_int = -50001;
/// `OAKCODEC_E_FAILED` (`include/codec/error.h`).
pub const OAKCODEC_E_FAILED: c_int = -50003;
/// Mirror of `OakCodecTaskRequest` (`include/codec/task.h`).
pub type OakCodecTaskRequest = oakcodec::task::OakCodecTaskRequest;
/// `oakcodec_task_submit_fn` callback typedef.
pub type OakCodecTaskSubmitFn =
unsafe extern "C" fn(req: *const OakCodecTaskRequest, userdata: *mut c_void) -> c_int;
/// Mirror of `OakCodecFrame`/`OakFrame` handle type (`include/codec/frame.h`).
pub type OakFrame = CHandle;
/// Mirror of `OakEncoder` handle type (`include/codec/encoder.h`).
pub type OakEncoder = CHandle;
/// Mirror of `OakDecoder` handle type (`include/codec/decoder.h`).
pub type OakDecoder = CHandle;
/// Mirror of `OakCodecProxyParams` (`include/codec/proxy.h`).
pub type OakCodecProxyParams = oakcodec::ffi::proxy::oakcodec_proxy_params;
/// Mirror of `oakcodec_encoding_params` (`include/codec/encoder.h`) — the
/// export task fills this to open the encoder. Layout mirrors the header
/// verbatim (a zeroed struct = all tracks disabled).
pub type OakCodecEncodingParams = oakcodec::ffi::encoder::oakcodec_encoding_params;
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_set_task_submit_cb(
cb: Option<OakCodecTaskSubmitFn>,
userdata: *mut c_void,
) {
unsafe { oakcodec::ffi::task::oakcodec_set_task_submit_cb(cb, userdata) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_task_submit_is_registered() -> c_int {
unsafe { oakcodec::ffi::task::oakcodec_task_submit_is_registered() }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_init() -> OakDecoder {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_init() }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_free(decoder: *mut OakDecoder) {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_open(
decoder: OakDecoder,
filename: *const c_char,
stream_index: c_int,
) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, filename, stream_index) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_close(decoder: OakDecoder) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_close(decoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_is_open(decoder: OakDecoder) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_is_open(decoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_decode_audio(
decoder: OakDecoder,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
sample_rate: c_int,
channel_layout: u64,
buf: *mut f32,
buf_frames: c_int,
) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_decode_audio(decoder, in_num, in_den, out_num, out_den, sample_rate, channel_layout, buf, buf_frames) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_conform_audio(
decoder: OakDecoder,
output_filenames: *const *const c_char,
filename_count: c_int,
sample_rate: c_int,
channel_layout: u64,
sample_format: c_int,
cancelled: OakCancelAtom,
) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_conform_audio(decoder, output_filenames, filename_count, sample_rate, channel_layout, sample_format, cancelled) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_last_error(decoder: OakDecoder, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_last_error(decoder, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_get_image_sequence_digit_count(filename: *const c_char) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_get_image_sequence_digit_count(filename) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_get_image_sequence_index(filename) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_decoder_transform_image_sequence_file_name(
filename: *const c_char,
number: i64,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_transform_image_sequence_file_name(filename, number, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_get_desired_pixel_format(encoder: OakEncoder) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_get_desired_pixel_format(encoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoding_generate_matrix(
method: c_int,
src_width: c_int,
src_height: c_int,
dst_width: c_int,
dst_height: c_int,
out_matrix: *mut f64,
) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix(method, src_width, src_height, dst_width, dst_height, out_matrix) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_proxy_find_ffmpeg(configured_path: *const c_char, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcodec::ffi::proxy::oakcodec_proxy_find_ffmpeg(configured_path, buf, buf_size) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_proxy_params_default(out: *mut OakCodecProxyParams) -> c_int {
unsafe { oakcodec::ffi::proxy::oakcodec_proxy_params_default(out) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_init(params: *const OakCodecEncodingParams) -> OakEncoder {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_free(encoder: *mut OakEncoder) {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_open(encoder: OakEncoder) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_open(encoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_write_video(encoder: OakEncoder, frame: OakFrame) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_video(encoder, frame) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_write_audio(
encoder: OakEncoder,
samples: *const f32,
frame_count: c_int,
) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_audio(encoder, samples, frame_count) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_write_subtitle(
encoder: OakEncoder,
text: *const c_char,
in_seconds: f64,
out_seconds: f64,
) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_subtitle(encoder, text, in_seconds, out_seconds) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_flush(encoder: OakEncoder) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_flush(encoder) }
}
/// Direct call into the `oakcodec` crate (single-lib unification).
pub fn oakcodec_encoder_last_error(encoder: OakEncoder, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_last_error(encoder, buf, buf_size) }
}
+221
View File
@@ -0,0 +1,221 @@
// 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/>.
// 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/>.
//! oakcommon / oakcore C ABI imports. The task module reaches the shared
//! value types through `include/common/videoparams.h`,
//! `include/common/colortransform.h` and the oakcore audio-params C ABI.
//! Signatures mirror the headers verbatim.
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
/// Mirror of `OakVideoParams` (`include/common/videoparams.h`).
///
/// A by-value handle (shared_ptr semantics); its `{ctx, addref, release,
/// abi_version}` layout is identical to [`CHandle`], so it is a type alias —
/// every oakcommon handle is created and destroyed inside the oakcommon DLL
/// and only crosses the FFI boundary as this opaque by-value struct.
pub type OakVideoParams = CHandle;
/// Mirror of `OakColorTransform` (`include/common/colortransform.h`); see
/// [`OakVideoParams`] for the alias rationale.
pub type OakColorTransform = CHandle;
/// Mirror of `OakAudioParams` — oakcore audio parameters. Declared as a
/// `CHandle`-shaped opaque; the oakcore C ABI header that owns its layout is
/// the authoritative source once the oakcore C ABI ships.
pub type OakAudioParams = CHandle;
/// `OAKCOMMON_VIDEO_TYPE_STILL` (`include/common/videoparams.h`).
pub const OAKCOMMON_VIDEO_TYPE_STILL: c_int = 1;
/// `OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE` (`include/common/videoparams.h`).
pub const OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE: c_int = 2;
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_init() -> OakVideoParams {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_init_basic(
width: c_int,
height: c_int,
pixel_format: c_int,
nb_channels: c_int,
pixel_aspect_num: c_int,
pixel_aspect_den: c_int,
interlacing: c_int,
divider: c_int,
) -> OakVideoParams {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den, interlacing, divider) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_free(params: *mut OakVideoParams) {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_time_base(
params: OakVideoParams,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base(params, numerator, denominator) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_time_base(
params: OakVideoParams,
numerator: c_int,
denominator: c_int,
) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base(params, numerator, denominator) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_frame_rate_as_time_base(
params: OakVideoParams,
out_num: *mut c_int,
out_den: *mut c_int,
) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_as_time_base(params, out_num, out_den) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_frame_rate(
params: OakVideoParams,
numerator: c_int,
denominator: c_int,
) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(params, numerator, denominator) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_video_type(params: OakVideoParams, out_type: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, out_type) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, video_type) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start: i64) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_start_time(params, start) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_duration(params, duration) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, out_valid: *mut c_int) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, out_valid) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_frame_rate(
params: OakVideoParams,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate(params, numerator, denominator) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_videoparams_get_duration(params: OakVideoParams, duration: *mut i64) -> c_int {
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_duration(params, duration) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_colortransform_init_display(
display: *const c_char,
view: *const c_char,
look: *const c_char,
) -> OakColorTransform {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakColorTransform {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_colortransform_free(transform: *mut OakColorTransform) {
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_config_get(
group: *const c_char,
key: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) }
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -> c_int {
unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, default) }
}
+25
View File
@@ -0,0 +1,25 @@
// 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/>.
//! C ABI imports from other oak modules (signatures mirror the public
//! headers verbatim; resolved at link time).
pub mod codec;
pub mod common;
pub mod node;
pub mod render;
pub mod timeline;
pub mod undo;
+910
View File
@@ -0,0 +1,910 @@
// 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/>.
// 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/>.
//! oaknode C ABI imports. The task module drives node-graph work through
//! `include/node/project.h` (project shell), `include/node/folder.h`,
//! `include/node/footage.h`, `include/node/sequence.h`,
//! `include/node/colormanager.h` and `include/node/serializer.h`. Signatures
//! mirror the headers verbatim; node handles are `CHandle` values whose
//! underlying objects live inside the node library.
use std::ffi::{c_char, c_int, c_void};
use crate::bridge::common::OakVideoParams;
use crate::bridge::render::OakCancelAtom;
use crate::handle::CHandle;
/// `OakNodeProject` (`include/node/project.h`).
pub type OakNodeProject = CHandle;
/// `OakNodeNode` (`include/node/node.h`).
pub type OakNodeNode = CHandle;
/// `OakNodeFolder` (`include/node/folder.h`).
pub type OakNodeFolder = CHandle;
/// `OakNodeFootage` (`include/node/footage.h`).
pub type OakNodeFootage = CHandle;
/// `OakNodeSequence` (`include/node/sequence.h`).
pub type OakNodeSequence = CHandle;
/// `OakNodeTrack` (`include/node/track.h`).
pub type OakNodeTrack = CHandle;
/// `OakNodeTrackList` (`include/node/track.h`).
pub type OakNodeTrackList = CHandle;
/// `OakNodeBlock` (`include/node/block.h`).
pub type OakNodeBlock = CHandle;
/// `OakNodeColorManager` (`include/node/colormanager.h`).
pub type OakNodeColorManager = CHandle;
/// `OakNodeSerializerSaveData` (`include/node/serializer.h`).
pub type OakNodeSerializerSaveData = CHandle;
/// `OakNodeSerializerLoadData` (`include/node/serializer.h`).
pub type OakNodeSerializerLoadData = CHandle;
/// `OAKNODE_SEQUENCE_TEXTURE_INPUT` (`include/node/sequence.h`).
pub const OAKNODE_SEQUENCE_TEXTURE_INPUT: &str = "tex_in";
/// `OAKNODE_SEQUENCE_SAMPLES_INPUT` (`include/node/sequence.h`).
pub const OAKNODE_SEQUENCE_SAMPLES_INPUT: &str = "samples_in";
/// `OAKNODE_TRACK_TYPE_VIDEO` (`include/node/track.h`).
pub const OAKNODE_TRACK_TYPE_VIDEO: c_int = 0;
/// `OAKNODE_TRACK_TYPE_AUDIO` (`include/node/track.h`).
pub const OAKNODE_TRACK_TYPE_AUDIO: c_int = 1;
/// `OAKNODE_TRACK_TYPE_NONE` (`include/node/track.h`).
pub const OAKNODE_TRACK_TYPE_NONE: c_int = -1;
/// `OAKNODE_BLOCK_OTHER` (`include/node/block.h`).
pub const OAKNODE_BLOCK_OTHER: c_int = 0;
/// `OAKNODE_BLOCK_CLIP` (`include/node/block.h`).
pub const OAKNODE_BLOCK_CLIP: c_int = 1;
/// `OAKNODE_BLOCK_GAP` (`include/node/block.h`).
pub const OAKNODE_BLOCK_GAP: c_int = 2;
/// `OAKNODE_BLOCK_TRANSITION` (`include/node/block.h`).
pub const OAKNODE_BLOCK_TRANSITION: c_int = 3;
/// `OAKNODE_TRANSITION_CROSS_DISSOLVE` (`include/node/block.h`).
pub const OAKNODE_TRANSITION_CROSS_DISSOLVE: c_int = 0;
/// `OAKNODE_TRANSITION_OUT_BLOCK_INPUT` (`include/node/block.h`).
pub const OAKNODE_TRANSITION_OUT_BLOCK_INPUT: &str = "out_block_in";
/// `OAKNODE_TRANSITION_IN_BLOCK_INPUT` (`include/node/block.h`).
pub const OAKNODE_TRANSITION_IN_BLOCK_INPUT: &str = "in_block_in";
/// Node type ids used by the OTIO tasks
/// (`src/task/src/project/loadotio/loadotio.cpp`).
/// `k_sequence_id` — the sequence node type.
pub const OAKNODE_TYPE_SEQUENCE: &str = "org.olivevideoeditor.Olive.sequence";
/// `k_transform_id` — the transform node type.
pub const OAKNODE_TYPE_TRANSFORM: &str = "org.olivevideoeditor.Olive.transform";
/// `k_volume_id` — the volume node type.
pub const OAKNODE_TYPE_VOLUME: &str = "org.olivevideoeditor.Olive.volume";
/// C string helper: build a NUL-terminated copy of a Rust string for the
/// C ABI. The buffer is leaked for the duration of the process; callers
/// pass short-lived literals.
pub fn cstr(s: &str) -> *const c_char {
let mut bytes = s.as_bytes().to_vec();
bytes.push(0);
bytes.leak().as_ptr() as *const c_char
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_label(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_get_label(node, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_set_label(node: OakNodeNode, label: *const c_char) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_set_label(node, label) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_id(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_get_id(node, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_get_project(node, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_input_get_connected_node(
node: OakNodeNode,
input_id: *const c_char,
out_node: *mut OakNodeNode,
) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_input_get_connected_node(node, input_id, out_node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_set_context_position(
node: OakNodeNode,
context: OakNodeNode,
x: f64,
y: f64,
expanded: c_int,
) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_set_context_position(node, context, x, y, expanded) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_find_input_footage(node: OakNodeNode, out: *mut OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_find_input_footage(node, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_input_string(
node: OakNodeNode,
input_id: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_get_input_string(node, input_id, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_create_copy(node: OakNodeNode) -> OakNodeNode {
unsafe { oaknode::ffi::node::oaknode_node_create_copy(node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_copy_inputs(dst: OakNodeNode, src: OakNodeNode, include_connections: c_int) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_copy_inputs(dst, src, include_connections) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_connect(output_node: OakNodeNode, input_node: OakNodeNode, input_id: *const c_char) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_connect(output_node, input_node, input_id) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_set_value_hint_track(
node: OakNodeNode,
input_id: *const c_char,
track_type: c_int,
track_index: c_int,
) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_set_value_hint_track(node, input_id, track_type, track_index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_video_frame_cache(
node: OakNodeNode,
out: *mut crate::bridge::render::OakRenderCache,
) -> c_int {
unsafe { oaknode::ffi::node::oaknode_node_get_video_frame_cache(node, out as *mut std::ffi::c_void) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_free(node: *mut OakNodeNode) {
unsafe { oaknode::ffi::node::oaknode_node_free(node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_command_create_remove_node(node: OakNodeNode) -> crate::bridge::undo::OakUndoCommand {
unsafe { oaknode::ffi::node::oaknode_command_create_remove_node(node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_factory_create_from_id(type_id: *const c_char) -> OakNodeNode {
unsafe { oaknode::ffi::factory::oaknode_factory_create_from_id(type_id) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_viewer_set_video_params(viewer: OakNodeNode, params: *const OakVideoParams) -> c_int {
unsafe { oaknode::ffi::node::oaknode_viewer_set_video_params(viewer, params as *const std::ffi::c_void) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_viewer_set_audio_params(viewer: OakNodeNode, params: *const std::ffi::c_void) -> c_int {
unsafe { oaknode::ffi::node::oaknode_viewer_set_audio_params(viewer, params) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_clip_create() -> OakNodeBlock {
unsafe { oaknode::ffi::block::oaknode_block_clip_create() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_gap_create() -> OakNodeBlock {
unsafe { oaknode::ffi::block::oaknode_block_gap_create() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_transition_create(kind: c_int) -> OakNodeBlock {
unsafe { oaknode::ffi::block::oaknode_block_transition_create(kind) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_as_node(block: OakNodeBlock) -> OakNodeNode {
unsafe { oaknode::ffi::block::oaknode_block_as_node(block) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_block_get_kind(block, out_kind) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_in(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_block_get_in(block, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_length(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_block_get_length(block, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_set_length_and_media_out(block: OakNodeBlock, numerator: c_int, denominator: c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_block_set_length_and_media_out(block, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_clip_set_media_in(clip: OakNodeBlock, numerator: c_int, denominator: c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_transition_get_in_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_transition_get_in_offset(transition, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_transition_get_out_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::block::oaknode_transition_get_out_offset(transition, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_transition_set_offsets_and_length(
transition: OakNodeBlock,
in_num: c_int,
in_den: c_int,
out_num: c_int,
out_den: c_int,
) -> c_int {
unsafe { oaknode::ffi::block::oaknode_transition_set_offsets_and_length(transition, in_num, in_den, out_num, out_den) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_init() -> OakNodeProject {
unsafe { oaknode::ffi::project::oaknode_project_init() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_free(project: *mut OakNodeProject) {
unsafe { oaknode::ffi::project::oaknode_project_free(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_initialize(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_initialize(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_clear(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_clear(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_root(project: OakNodeProject) -> OakNodeFolder {
unsafe { oaknode::ffi::project::oaknode_project_root(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_name(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_name(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_filename(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_pretty_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_pretty_filename(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_set_filename(project: OakNodeProject, filename: *const c_char) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_set_filename(project, filename) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_is_modified(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_is_modified(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_set_modified(project: OakNodeProject, modified: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_set_modified(project, modified) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_is_new(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_is_new(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_cache_path(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_copy_settings(dst: OakNodeProject, src: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_copy_settings(dst, src) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_get_cache_location_setting(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_get_cache_location_setting(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_set_cache_location_setting(project: OakNodeProject, setting: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_set_cache_location_setting(project, setting) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_get_custom_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_get_custom_cache_path(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_set_custom_cache_path(project: OakNodeProject, path: *const c_char) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_set_custom_cache_path(project, path) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_get_uuid(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_get_uuid(project, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_add_node(project, node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_remove_node(project, node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_node_count(project: OakNodeProject) -> c_int {
unsafe { oaknode::ffi::project::oaknode_project_node_count(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_node_at(project: OakNodeProject, index: c_int) -> OakNodeNode {
unsafe { oaknode::ffi::project::oaknode_project_node_at(project, index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_create(project: OakNodeProject) -> OakNodeFolder {
unsafe { oaknode::ffi::folder::oaknode_folder_create(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_child_count(folder: OakNodeFolder) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_child_count(folder) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_child_at(folder: OakNodeFolder, index: c_int) -> OakNodeNode {
unsafe { oaknode::ffi::folder::oaknode_folder_child_at(folder, index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_add_child(folder, child) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_as_node(folder: OakNodeFolder) -> OakNodeNode {
unsafe { oaknode::ffi::folder::oaknode_folder_as_node(folder) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_command_create_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -> crate::bridge::undo::OakUndoCommand {
unsafe { oaknode::ffi::folder::oaknode_command_create_folder_add_child(folder, child) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_remove_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_remove_child(folder, child) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_move_children(nodes: *const OakNodeNode, count: c_int, dest_folder: OakNodeFolder) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_move_children(nodes, count, dest_folder) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_has_child_recursive(folder: OakNodeFolder, child: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_has_child_recursive(folder, child) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_index_of_child(folder: OakNodeFolder, child: OakNodeNode) -> c_int {
unsafe { oaknode::ffi::folder::oaknode_folder_index_of_child(folder, child) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_folder_parent_of(node: OakNodeNode) -> OakNodeFolder {
unsafe { oaknode::ffi::folder::oaknode_folder_parent_of(node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_create(project: OakNodeProject, filename: *const c_char) -> OakNodeFootage {
unsafe { oaknode::ffi::footage::oaknode_footage_create(project, filename) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_as_node(footage: OakNodeFootage) -> OakNodeNode {
unsafe { oaknode::ffi::footage::oaknode_footage_as_node(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_filename(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_filename(footage, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_filename(footage: OakNodeFootage, filename: *const c_char) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_filename(footage, filename) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_is_valid(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_is_valid(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_timestamp(footage: OakNodeFootage, out_timestamp: *mut i64) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_timestamp(footage, out_timestamp) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_timestamp(footage: OakNodeFootage, timestamp: i64) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_timestamp(footage, timestamp) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_decoder(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_decoder(footage, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_total_stream_count(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_total_stream_count(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_video_stream_count(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_video_stream_count(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_audio_stream_count(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_audio_stream_count(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_subtitle_stream_count(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_subtitle_stream_count(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_duration(footage: OakNodeFootage, out_numerator: *mut c_int, out_denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_duration(footage, out_numerator, out_denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_proxy_enabled(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_proxy_enabled(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_proxy_enabled(footage: OakNodeFootage, enabled: c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_proxy_enabled(footage, enabled) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_proxy_path(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_proxy_path(footage, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_proxy_state(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_proxy_state(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_proxy(footage: OakNodeFootage, path: *const c_char, state: c_int, video_stream_index: c_int, preset_version: c_int, enabled: c_int) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_proxy(footage, path, state, video_stream_index, preset_version, enabled) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_clear_proxy(footage: OakNodeFootage) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_clear_proxy(footage) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_get_video_params(footage: OakNodeFootage, index: c_int, out: *mut OakVideoParams) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_get_video_params(footage, index, out as *mut std::ffi::c_void) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_video_params(footage: OakNodeFootage, index: c_int, params: *const OakVideoParams) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_video_params(footage, index, params as *const std::ffi::c_void) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_get_video_length(footage: OakNodeFootage, out_num: *mut i64, out_den: *mut i64) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_get_video_length(footage, out_num, out_den) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_footage_set_cancel_atom(footage: OakNodeFootage, atom: OakCancelAtom) -> c_int {
unsafe { oaknode::ffi::footage::oaknode_footage_set_cancel_atom(footage, atom) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_create() -> OakNodeSequence {
unsafe { oaknode::ffi::sequence::oaknode_sequence_create() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_free(sequence: *mut OakNodeSequence) {
unsafe { oaknode::ffi::sequence::oaknode_sequence_free(sequence) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_set_default_parameters(sequence: OakNodeSequence) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_set_default_parameters(sequence) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -> OakNodeNode {
unsafe { oaknode::ffi::sequence::oaknode_sequence_as_node(sequence) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_from_node(node: OakNodeNode) -> OakNodeSequence {
unsafe { oaknode::ffi::sequence::oaknode_sequence_from_node(node) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_track_list(sequence: OakNodeSequence, r#type: c_int, out: *mut OakNodeTrackList) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_list(sequence, r#type, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_track_count(sequence: OakNodeSequence, r#type: c_int, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_count(sequence, r#type, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_track_at(sequence: OakNodeSequence, r#type: c_int, index: c_int, out: *mut OakNodeTrack) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_at(sequence, r#type, index, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_count(sequence, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_all_track_at(sequence: OakNodeSequence, index: c_int, out: *mut OakNodeTrack) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_at(sequence, index, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_playhead(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_playhead(sequence, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_set_playhead(sequence: OakNodeSequence, numerator: c_int, denominator: c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_set_playhead(sequence, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_length(sequence, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_video_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_video_length(sequence, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_audio_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_length(sequence, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_verify_length(sequence: OakNodeSequence) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_verify_length(sequence) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_video_stream_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_video_stream_count(sequence, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_audio_stream_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_stream_count(sequence, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_video_params(sequence: OakNodeSequence, index: c_int, out: *mut OakVideoParams) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_video_params(sequence, index, out as *mut std::ffi::c_void) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_set_video_params(sequence: OakNodeSequence, index: c_int, params: OakVideoParams) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_set_video_params(sequence, index, params) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_audio_params(sequence: OakNodeSequence, index: c_int, out: *mut *mut std::ffi::c_void) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_audio_params(sequence, index, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_set_audio_params(sequence: OakNodeSequence, index: c_int, params: *const c_void) -> c_int {
unsafe { oaknode::ffi::sequence::oaknode_sequence_set_audio_params(sequence, index, params) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_as_node(track: OakNodeTrack) -> OakNodeNode {
unsafe { oaknode::ffi::track::oaknode_track_as_node(track) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_create(r#type: c_int) -> OakNodeTrack {
unsafe { oaknode::ffi::track::oaknode_track_create(r#type) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_free(track: *mut OakNodeTrack) {
unsafe { oaknode::ffi::track::oaknode_track_free(track) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_type(track: OakNodeTrack, r#type: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_type(track, r#type) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_length(track: OakNodeTrack, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_length(track, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_block_count(track, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_block_at(track: OakNodeTrack, index: c_int, out: *mut OakNodeBlock) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_block_at(track, index, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_append_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_append_block(track, block) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_set_type(track: OakNodeTrack, r#type: c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_set_type(track, r#type) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_index(track: OakNodeTrack, index: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_index(track, index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_set_index(track: OakNodeTrack, index: c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_set_index(track, index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -> c_int {
unsafe { oaknode::ffi::track::oaknode_track_get_sequence(track, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_sequence(list: OakNodeTrackList, out: *mut OakNodeSequence) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_sequence(list, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_track_input_id(list: OakNodeTrackList, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_input_id(list, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_array_append(list) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_array_remove_last(list) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_array_index_from_cache_index(list: OakNodeTrackList, cache_index: c_int, out_index: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_array_index_from_cache_index(list, cache_index, out_index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_type(list: OakNodeTrackList, r#type: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_type(list, r#type) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_count(list, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_track_at(list: OakNodeTrackList, index: c_int, out: *mut OakNodeTrack) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_at(list, index, out) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_total_length(list: OakNodeTrackList, numerator: *mut c_int, denominator: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_total_length(list, numerator, denominator) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_array_size(list: OakNodeTrackList, size: *mut c_int) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_get_array_size(list, size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_add_track(list: OakNodeTrackList, track: OakNodeTrack) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_add_track(list, track) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_remove_track(list: OakNodeTrackList, track: OakNodeTrack) -> c_int {
unsafe { oaknode::ffi::track::oaknode_tracklist_remove_track(list, track) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_colormanager_init(project: OakNodeProject) -> OakNodeColorManager {
unsafe { oaknode::ffi::colormanager::oaknode_colormanager_init(project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_colormanager_free(manager: *mut OakNodeColorManager) {
unsafe { oaknode::ffi::colormanager::oaknode_colormanager_free(manager) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_colormanager_wrap_borrowed(native_manager: *mut c_void) -> OakNodeColorManager {
unsafe { oaknode::ffi::colormanager::oaknode_colormanager_wrap_borrowed(native_manager) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_colormanager_initialize(manager: OakNodeColorManager) -> c_int {
unsafe { oaknode::ffi::colormanager::oaknode_colormanager_initialize(manager) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_colormanager_set_up_default_config() -> c_int {
unsafe { oaknode::ffi::colormanager::oaknode_colormanager_set_up_default_config() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_initialize() -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_initialize() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_shutdown() {
unsafe { oaknode::ffi::serializer::oaknode_serializer_shutdown() }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_savedata_create(load_type: c_int, project: OakNodeProject) -> OakNodeSerializerSaveData {
unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_create(load_type, project) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_savedata_free(save_data: *mut OakNodeSerializerSaveData) {
unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_free(save_data) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_savedata_set_nodes(save_data: OakNodeSerializerSaveData, nodes: *const OakNodeNode, count: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_set_nodes(save_data, nodes, count) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_savedata_set_property(save_data: OakNodeSerializerSaveData, node: OakNodeNode, key: *const c_char, value: *const c_char) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_savedata_set_property(save_data, node, key, value) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_save_to_xml(save_data: OakNodeSerializerSaveData, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_save_to_xml(save_data, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_load_from_xml(project: OakNodeProject, xml: *const c_char, load_type: c_int, out_result: *mut c_int, out_load_data: *mut OakNodeSerializerLoadData, details_buf: *mut c_char, details_buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_load_from_xml(project, xml, load_type, out_result, out_load_data, details_buf, details_buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_free(load_data: *mut OakNodeSerializerLoadData) {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_free(load_data) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_node_count(load_data: OakNodeSerializerLoadData) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_count(load_data) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_node_at(load_data: OakNodeSerializerLoadData, index: c_int) -> OakNodeNode {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_node_at(load_data, index) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_get_property(load_data: OakNodeSerializerLoadData, node: OakNodeNode, key: *const c_char, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_get_property(load_data, node, key, buf, buf_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_connection_count(load_data: OakNodeSerializerLoadData) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_count(load_data) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_loaddata_connection_at(load_data: OakNodeSerializerLoadData, index: c_int, out_output_node: *mut OakNodeNode, out_input_node: *mut OakNodeNode, input_id_buf: *mut c_char, input_id_buf_size: c_int, out_element: *mut c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_loaddata_connection_at(load_data, index, out_output_node, out_input_node, input_id_buf, input_id_buf_size, out_element) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_save_to_file(project: OakNodeProject, filename: *const c_char, use_compression: c_int, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_save_to_file(project, filename, use_compression, out_code, details, details_size) }
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_serializer_load_from_file(project: OakNodeProject, filename: *const c_char, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -> c_int {
unsafe { oaknode::ffi::serializer::oaknode_serializer_load_from_file(project, filename, out_code, details, details_size) }
}
+270
View File
@@ -0,0 +1,270 @@
// 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/>.
// 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/>.
//! oakrender C ABI imports. The task module reaches the render side through
//! `include/render/cancelatom.h` (task cancellation), `include/render/ticket.h`
//! (frame/audio render tickets), `include/render/copier.h` (export project
//! copy) and `include/render/color.h` (color processor). Signatures mirror
//! the headers verbatim.
//!
//! Resolution is at link time (the crate's existing pattern, see
//! `bridge/codec.rs`): the symbols are satisfied by the real `liboakrender`
//! when it is linked into the same binary (the app links the module dylibs;
//! the `real-oakrender` integration test links the oakrender crate directly),
//! and by the `#[no_mangle]` stubs in `tests/common/mod.rs` in plain
//! `cargo test`. The real-oakrender ticket contract — in particular the
//! 2-argument `oakrender_ticket_finished_fn` — is verified by
//! `tests/render_real_integration_test.rs` against the actual exports.
use std::ffi::{c_char, c_int, c_void};
use crate::bridge::codec::OakFrame;
use crate::bridge::common::{OakAudioParams, OakColorTransform, OakVideoParams};
use crate::bridge::node::{OakNodeColorManager, OakNodeNode, OakNodeProject};
use crate::handle::CHandle;
/// Mirror of `OakCancelAtom` (`include/render/cancelatom.h`).
pub type OakCancelAtom = oakrender::ffi::OakCancelAtom;
/// Mirror of `OakRenderTicket` (`include/render/ticket.h`).
pub type OakRenderTicket = oakrender::ffi::OakRenderTicket;
/// Mirror of `OakRenderCache` (`include/render/cache.h`).
pub type OakRenderCache = oakrender::ffi::OakRenderCache;
/// Mirror of `OakRenderProjectCopier` (`include/render/copier.h`).
pub type OakRenderProjectCopier = oakrender::ffi::OakRenderProjectCopier;
/// Mirror of `OakColorProcessor` (`include/render/color.h`).
pub type OakColorProcessor = oakrender::ffi::OakColorProcessor;
/// Mirror of `oakrender_ticket_finished_fn` (`include/render/ticket.h`).
///
/// Two arguments — `(ticket, userdata)` — mirroring the header verbatim and
/// matching the oakrender implementation (`src/render/rust/src/ffi.rs`).
/// The ticket is a borrowed copy of the submitter's handle (the submitter
/// keeps ownership and releases it); cancelled tickets fire with a NULL
/// result observed through `oakrender_ticket_get_frame`.
pub type OakRenderTicketFinishedFn = unsafe extern "C" fn(ticket: OakRenderTicket, userdata: *mut c_void);
/// Mirror of `oakrender_video_ticket_params` (`include/render/ticket.h`).
pub type OakRenderVideoTicketParams = oakrender::ffi::OakVideoTicketParams;
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cancelatom_init() -> OakCancelAtom {
unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_init() }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {
unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_free(atom) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom) -> c_int {
unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_cancel(atom) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom, cancelled: *mut c_int) -> c_int {
unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_is_cancelled(atom, cancelled) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom, heard: *mut c_int) -> c_int {
unsafe { oakrender::ffi::cancelatom::oakrender_cancelatom_heard_cancel(atom, heard) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_render_frame(
params: *const OakRenderVideoTicketParams,
cb: Option<OakRenderTicketFinishedFn>,
userdata: *mut c_void,
) -> OakRenderTicket {
unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_render_audio(
output_node: OakNodeNode,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
params: *const OakAudioParams,
mode: c_int,
cb: Option<OakRenderTicketFinishedFn>,
userdata: *mut c_void,
) -> OakRenderTicket {
unsafe { oakrender::ffi::ticket::oakrender_ticket_render_audio(output_node, in_num, in_den, out_num, out_den, params, mode, cb, userdata) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_is_finished(ticket: OakRenderTicket) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_is_finished(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_wait(ticket: OakRenderTicket) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_cancel(ticket: OakRenderTicket) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_get_type(ticket: OakRenderTicket) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_type(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_get_frame(ticket: OakRenderTicket, out: *mut OakFrame) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_get_time(ticket: OakRenderTicket, out_num: *mut i64, out_den: *mut i64) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_time(ticket, out_num, out_den) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_get_range(
ticket: OakRenderTicket,
in_num: *mut i64,
in_den: *mut i64,
out_num: *mut i64,
out_den: *mut i64,
) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_range(ticket, in_num, in_den, out_num, out_den) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_get_samples(ticket: OakRenderTicket, out: *mut *mut std::ffi::c_void) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_ticket_free(ticket: *mut OakRenderTicket) {
unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_project_copier_create() -> OakRenderProjectCopier {
unsafe { oakrender::ffi::copier::oakrender_project_copier_create() }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_project_copier_free(copier: *mut OakRenderProjectCopier) {
unsafe { oakrender::ffi::copier::oakrender_project_copier_free(copier) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_project_copier_set_project(
copier: OakRenderProjectCopier,
project: OakNodeProject,
) -> c_int {
unsafe { oakrender::ffi::copier::oakrender_project_copier_set_project(copier, project) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_project_copier_get_copy(
copier: OakRenderProjectCopier,
original: OakNodeNode,
) -> OakNodeNode {
unsafe { oakrender::ffi::copier::oakrender_project_copier_get_copy(copier, original) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_project_copier_get_copied_project(
copier: OakRenderProjectCopier,
) -> OakNodeProject {
unsafe { oakrender::ffi::copier::oakrender_project_copier_get_copied_project(copier) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_color_processor_create(
src_space: *const c_char,
dst_transform: *const c_char,
direction: c_int,
) -> OakColorProcessor {
unsafe { oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_color_processor_free(processor: *mut OakColorProcessor) {
unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_color_processor_is_valid(processor: OakColorProcessor) -> c_int {
unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_codec_frame_free(frame: *mut crate::bridge::codec::OakFrame) {
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int {
unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cache_get_invalidated_ranges(
cache: OakRenderCache,
in_num: i64,
in_den: i64,
out_num: i64,
out_den: i64,
flat: *mut i64,
flat_size: c_int,
) -> c_int {
unsafe { oakrender::ffi::cache::oakrender_cache_get_invalidated_ranges(cache, in_num, in_den, out_num, out_den, flat, flat_size) }
}
/// Direct call into the `oakrender` crate (single-lib unification).
pub fn oakrender_cache_free(cache: *mut OakRenderCache) {
unsafe { oakrender::ffi::cache::oakrender_cache_free(cache) }
}
+45
View File
@@ -0,0 +1,45 @@
// 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/>.
// 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/>.
//! oaktimeline C ABI imports. The OTIO load task builds tracks through the
//! timeline edit factories (`include/timeline/edit.h`) instead of the raw
//! track-list primitives, matching `src/task/src/project/loadotio/loadotio.cpp`.
//! Signatures mirror the header verbatim.
use crate::bridge::node::OakNodeTrackList;
use crate::bridge::undo::OakUndoCommand;
/// Direct call into the `oaktimeline` crate (single-lib unification).
pub fn oaktimeline_add_track_command(list: OakNodeTrackList) -> OakUndoCommand {
unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) }
}
+88
View File
@@ -0,0 +1,88 @@
// 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/>.
// 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/>.
//! oakundo C ABI imports. Import/load tasks build undo commands through the
//! C ABI vtable (`oakundo_command_init` with Rust closures as userdata) —
//! no C++ UndoCommand subclassing exists on this side. Mirrors
//! `include/undo/undocommand.h` verbatim.
use std::ffi::{c_int, c_void};
use crate::handle::CHandle;
/// `OakUndoCommand` (`include/undo/undocommand.h`).
pub type OakUndoCommand = CHandle;
/// Mirror of `OakUndoCommandVtable` (`include/undo/undocommand.h`).
pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable;
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> OakUndoCommand {
unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_init_multi() -> OakUndoCommand {
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_multi_add_child(multi: OakUndoCommand, child: OakUndoCommand) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_multi_child_count(multi: OakUndoCommand, out_count: *mut c_int) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_multi_child(multi: OakUndoCommand, index: c_int, out_child: *mut OakUndoCommand) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_redo_now(command: OakUndoCommand) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_undo_now(command: OakUndoCommand) -> c_int {
unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_free(command: *mut OakUndoCommand) {
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
}
+133
View File
@@ -0,0 +1,133 @@
// 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/>.
//! Codec task submitter registration, mirroring
//! `src/task/src/codecbridge.h`.
//!
//! Wires oakcodec's task-submit callback (`oakcodec_set_task_submit_cb`) so
//! conform/proxy requests from the codec side land back in the task module.
//! This is a two-module coupling; the actual callback signatures live in
//! `crate::bridge::codec` mirroring `include/codec/task.h` verbatim.
//!
//! CPP-PARITY: src/task/src/codecbridge.h
use std::ffi::{c_char, c_int, c_void};
use crate::bridge;
use crate::conform::ConformTask;
use crate::error::{Error, Result};
use crate::proxy::{ProxyParams, ProxyTask};
use crate::task::Task;
/// Convert an oakcodec proxy-params POD into the Rust [`ProxyParams`].
fn proxy_params_from_codec(params: &bridge::codec::OakCodecProxyParams) -> ProxyParams {
ProxyParams {
width: params.width,
height: params.height,
divider: params.divider,
version: params.version,
crf: params.crf,
include_audio: params.include_audio != 0,
extension: unsafe { cstr_buf_to_string(&params.extension) },
preset: unsafe { cstr_buf_to_string(&params.preset) },
}
}
/// Read a NUL-terminated char array into a String (lossy).
unsafe fn cstr_buf_to_string(buf: &[u8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
String::from_utf8_lossy(bytes).into_owned()
}
/// The installed submit callback, mirroring `submit_codec_task` in
/// codecbridge.cpp (interim contract: submission is synchronous).
///
/// # Safety
/// `req` must be a valid `OakCodecTaskRequest` or null.
unsafe extern "C" fn submit_codec_task(req: *const bridge::codec::OakCodecTaskRequest, _userdata: *mut c_void) -> c_int {
if req.is_null() {
return bridge::codec::OAKCODEC_E_INVALID;
}
let request = unsafe { &*req };
// Build the concrete task on an outer base so the behavior can be
// driven by `start()`; both share the cancellation atom.
match request.kind {
bridge::codec::OAKCODEC_TASK_CONFORM => {
let task = ConformTask::new(request);
let atom = task.base.get_cancel_atom();
let title = task.base.title().to_string();
let mut outer = Task::new(&title, atom);
outer.set_behavior(Box::new(task));
match outer.start() {
Ok(()) => 0,
Err(_) => bridge::codec::OAKCODEC_E_FAILED,
}
}
bridge::codec::OAKCODEC_TASK_PROXY => {
let mut codec_params = bridge::codec::OakCodecProxyParams {
width: 0,
height: 0,
divider: 0,
version: 0,
crf: 0,
include_audio: 0,
extension: [0; 32],
preset: [0; 32],
};
unsafe {
bridge::codec::oakcodec_proxy_params_default(&mut codec_params);
}
let params = proxy_params_from_codec(&codec_params);
let task = ProxyTask::new(request, params);
let atom = task.base.get_cancel_atom();
let title = task.base.title().to_string();
let mut outer = Task::new(&title, atom);
outer.set_behavior(Box::new(task));
match outer.start() {
Ok(()) => 0,
Err(_) => bridge::codec::OAKCODEC_E_FAILED,
}
}
_ => bridge::codec::OAKCODEC_E_INVALID,
}
}
/// Install the task-module submitter as oakcodec's callback. Returns
/// `Err(Error::State)` if already registered.
pub fn register_codec_task_submitter() -> Result<()> {
if is_codec_task_submitter_registered() {
return Err(Error::State);
}
unsafe {
bridge::codec::oakcodec_set_task_submit_cb(Some(submit_codec_task), std::ptr::null_mut());
}
Ok(())
}
/// Remove the task-module submitter from oakcodec. Idempotent.
pub fn unregister_codec_task_submitter() -> Result<()> {
unsafe {
bridge::codec::oakcodec_set_task_submit_cb(None, std::ptr::null_mut());
}
Ok(())
}
/// Whether a submitter is currently installed (queries oakcodec).
pub fn is_codec_task_submitter_registered() -> bool {
unsafe { bridge::codec::oakcodec_task_submit_is_registered() != 0 }
}
+197
View File
@@ -0,0 +1,197 @@
// 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/>.
//! `ConformTask`, mirroring `src/task/src/conform/conform.h`.
//!
//! Transcodes an audio stream to a PCM cache file (oakcodec kind
//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes.
//!
//! CPP-PARITY: src/task/src/conform/conform.h
use crate::bridge;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::task::{Task, TaskBehavior};
/// A conform task: copies/channels of one audio stream into per-channel
/// working PCM cache files, then renames them to the final names.
pub struct ConformTask {
/// The shared task base.
pub base: Task,
/// Absolute input media filename.
pub input_filename: String,
/// Final destination path (replaces the source path extension).
pub output_filename: String,
/// Audio stream index inside the source media.
pub stream_index: i32,
/// Target sample rate (Hz).
pub sample_rate: i32,
/// Target channel layout mask (ffmpeg-style).
pub channel_layout: u64,
/// Target sample format (enum as int; `oakcore_rs::SampleFormat`).
pub sample_format: i32,
/// Resolved final per-channel filenames (from [`ConformTask::derive_filenames`]).
final_names: Vec<String>,
/// Resolved working per-channel filenames.
working_names: Vec<String>,
}
impl ConformTask {
/// Build a conform task from an oakcodec request, mirroring the C++
/// constructor.
pub fn new(request: &bridge::codec::OakCodecTaskRequest) -> ConformTask {
let input = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };
let output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };
let title = format!("Conforming Audio {}:{}", input, request.stream_index);
ConformTask {
base: Task::new(&title, CHandle::null()),
input_filename: input,
output_filename: output,
stream_index: request.stream_index,
sample_rate: request.sample_rate,
channel_layout: request.channel_layout,
sample_format: request.sample_format,
final_names: Vec::new(),
working_names: Vec::new(),
}
}
/// Derive the final and working PCM filenames for the given first-channel
/// final filename and channel count.
///
/// C++ contract: the first-channel final name must end in `.0.pcm`; the
/// final name for channel `i` replaces the trailing index, and the
/// working name appends `.working`. See `derive_filenames` in
/// conform.cpp — this is a parity/golden candidate (see
/// tests/parity_test.rs).
///
/// CPP-PARITY: src/task/src/conform/conform.cpp (derive_filenames)
pub fn derive_filenames(first_channel_final: &str, channel_count: usize) -> Result<(Vec<String>, Vec<String>)> {
const SUFFIX: &str = ".0.pcm";
if channel_count < 1 || first_channel_final.len() <= SUFFIX.len()
|| !first_channel_final.ends_with(SUFFIX)
{
return Err(Error::Failed("Invalid conform output filename".to_string()));
}
let base = &first_channel_final[..first_channel_final.len() - SUFFIX.len()];
let mut final_names = Vec::with_capacity(channel_count);
let mut working_names = Vec::with_capacity(channel_count);
for i in 0..channel_count {
let final_name = format!("{base}.{i}.pcm");
final_names.push(final_name.clone());
working_names.push(format!("{final_name}.working"));
}
Ok((final_names, working_names))
}
}
impl TaskBehavior for ConformTask {
/// Run the conform via the oakcodec decoder (`bridge::codec`), emitting
/// progress as audio is decoded and written to the working files, then
/// rename working → final.
fn run(&mut self, task: &mut Task) -> Result<()> {
let channel_count = self.channel_layout.count_ones() as usize;
let (final_names, working_names) =
Self::derive_filenames(&self.output_filename, channel_count).map_err(|_| {
task.set_error("Invalid conform output filename");
Error::Failed("Invalid conform output filename".to_string())
})?;
self.final_names = final_names;
self.working_names = working_names;
let decoder = unsafe { bridge::codec::oakcodec_decoder_init() };
if decoder.ctx.is_null() {
task.set_error("Failed to create decoder");
return Err(Error::Failed("Failed to create decoder".to_string()));
}
let mut decoder = decoder;
let result = (|| {
let open_result = unsafe {
bridge::codec::oakcodec_decoder_open(decoder, cstr(&self.input_filename), self.stream_index)
};
if open_result != 0 {
let err = decoder_error(decoder);
task.set_error(&format!("Failed to open decoder for audio conform: {err}"));
return false;
}
let working_ptrs: Vec<*const std::ffi::c_char> =
self.working_names.iter().map(|n| cstr(n)).collect();
let conform_result = unsafe {
bridge::codec::oakcodec_decoder_conform_audio(
decoder,
working_ptrs.as_ptr(),
working_ptrs.len() as i32,
self.sample_rate,
self.channel_layout,
self.sample_format,
task.get_cancel_atom(),
)
};
unsafe {
bridge::codec::oakcodec_decoder_close(decoder);
}
if conform_result == 0 {
// Rename each working file into place; a failure aborts the
// rest (mirroring the C++ loop).
for i in 0..self.working_names.len() {
if std::fs::rename(&self.working_names[i], &self.final_names[i]).is_err() {
task.set_error("Failed to move conformed audio into place");
return false;
}
}
true
} else {
// Clean up any partial working files.
for name in &self.working_names {
let _ = std::fs::remove_file(name);
}
if conform_result == bridge::codec::OAKCODEC_E_CANCELLED {
task.set_error("Audio conform was cancelled");
} else {
task.set_error("Audio conform failed");
}
false
}
})();
unsafe {
bridge::codec::oakcodec_decoder_free(&mut decoder);
}
if result {
Ok(())
} else {
Err(Error::Failed("Audio conform failed".to_string()))
}
}
}
/// Two-stage read of the decoder's last error string.
fn decoder_error(decoder: CHandle) -> String {
let mut buf = [0i8; 256];
let needed = unsafe { bridge::codec::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as i32) };
if needed <= 0 {
return String::new();
}
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
}
+162
View File
@@ -0,0 +1,162 @@
// 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/>.
//! `CustomCacheTask`, mirroring `src/task/src/customcache/customcachetask.h`.
//!
//! A task that delegates its completion to an external cache fill: it does
//! not do work itself but waits until the cache reports it is done (via
//! [`CustomCacheTask::finish`]) or is cancelled.
//!
//! CPP-PARITY: src/task/src/customcache/customcachetask.h
use std::sync::{Arc, Condvar, Mutex};
use crate::error::{Error, Result};
use crate::handle::CHandle;
use crate::task::{Task, TaskBehavior};
/// Shared state between the parked task thread, the owning cache
/// ([`CustomCacheTask::finish`]) and the cancel hook. Mirrors the C++
/// `mutex_`/`wait_cond_`/`cancelled_through_finish_` trio; the cancel atom
/// copy lets `finish()` wake the task through the same channel the C++
/// `cancel()` uses.
pub struct CustomCacheState {
/// Set by `finish()` before the cancellation request.
cancelled_through_finish: Mutex<bool>,
/// Parked-task wakeup.
wait: Condvar,
/// Callback invoked when the task is cancelled (not by `finish()`).
cancelled_callback: Mutex<Option<Box<dyn FnMut() + Send>>>,
/// Copy of the task's cancellation atom.
atom: CHandle,
}
impl CustomCacheState {
/// Mark the cache fill as complete and wake the parked task.
pub fn finish(&self) {
*self.cancelled_through_finish.lock().unwrap() = true;
if !self.atom.is_null() {
let atom = self.atom;
unsafe {
crate::bridge::render::oakrender_cancelatom_cancel(atom);
}
}
self.wait.notify_one();
}
/// The C++ `cancel_event()` hook: fires the callback unless the task was
/// finished through `finish()`, then wakes the parked task.
pub fn cancel_event(&self) {
if !*self.cancelled_through_finish.lock().unwrap() {
if let Some(cb) = self.cancelled_callback.lock().unwrap().as_mut() {
cb();
}
}
self.wait.notify_one();
}
/// The full C++ `cancel()`: cancel the atom and run the cancel-event
/// hook. Used by the owning cache to abort a fill.
pub fn cancel(&self) {
if !self.atom.is_null() {
let atom = self.atom;
unsafe {
crate::bridge::render::oakrender_cancelatom_cancel(atom);
}
}
self.cancel_event();
}
/// Whether the fill was completed through `finish()`.
fn is_finished_through_finish(&self) -> bool {
*self.cancelled_through_finish.lock().unwrap()
}
}
/// A cache-fill task that finishes only when [`CustomCacheTask::finish`] is
/// called by the owning cache. Uses a `Mutex` + `Condvar` to park the task
/// thread until completion or cancellation.
pub struct CustomCacheTask {
/// The shared task base.
pub base: Task,
/// The name of the sequence being cached.
pub sequence_name: String,
/// Shared park/notify state.
state: Arc<CustomCacheState>,
}
impl CustomCacheTask {
/// Create a cache-fill task with the given sequence name. The cancel
/// hook that wakes the parked thread is installed on the base task.
pub fn new(sequence_name: &str) -> CustomCacheTask {
let base = Task::new(&format!("Caching custom range for \"{sequence_name}\""), CHandle::null());
let state = Arc::new(CustomCacheState {
cancelled_through_finish: Mutex::new(false),
wait: Condvar::new(),
cancelled_callback: Mutex::new(None),
atom: base.get_cancel_atom(),
});
let mut task = CustomCacheTask {
base,
sequence_name: sequence_name.to_string(),
state,
};
let hook_state = task.state.clone();
task.base.set_cancel_event(Box::new(move || hook_state.cancel_event()));
task
}
/// Mark the cache fill as complete, waking the parked task thread.
pub fn finish(&mut self) {
self.state.finish();
}
/// Install the callback invoked when the task is cancelled.
pub fn set_cancelled_callback(&mut self, cb: Box<dyn FnMut() + Send>) {
*self.state.cancelled_callback.lock().unwrap() = Some(cb);
}
/// Shared-state accessor for the owning cache (and tests) to call
/// [`CustomCacheTask::finish`] from another thread.
pub fn state(&self) -> Arc<CustomCacheState> {
self.state.clone()
}
}
impl TaskBehavior for CustomCacheTask {
/// Park until [`CustomCacheTask::finish`] or cancellation; return
/// `Err(Error::Cancelled)` if cancelled while waiting.
fn run(&mut self, task: &mut Task) -> Result<()> {
// Install the cancel hook on the task this behavior actually runs
// under (the outer task driven by the manager / C ABI). The
// constructor-installed hook on the inner `base` only fires when the
// base itself is cancelled; without this the outer `cancel()` would
// set the shared atom but never wake the parked thread.
let hook_state = self.state.clone();
task.set_cancel_event(Box::new(move || hook_state.cancel_event()));
let mut guard = self.state.cancelled_through_finish.lock().unwrap();
while !task.is_cancelled() {
guard = self.state.wait.wait(guard).unwrap();
}
drop(guard);
if self.state.is_finished_through_finish() {
Ok(())
} else {
Err(Error::Cancelled)
}
}
}
+69
View File
@@ -0,0 +1,69 @@
// 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/>.
//! Error codes, mirroring `include/task/error.h` verbatim; project-wide
//! -MMCCCC scheme (module registry in include/common/error.h), pass-through
//! untranslated. Module number for task is **08**; task additionally defines
//! `OAKTASK_E_CANCELLED` (see include/task/error.h).
/// Success.
pub const OAKTASK_OK: i32 = 0;
/// Null handle or invalid argument.
pub const OAKTASK_E_INVALID: i32 = -80001;
/// Call not valid in the current state.
pub const OAKTASK_E_STATE: i32 = -80002;
/// The underlying operation failed.
pub const OAKTASK_E_FAILED: i32 = -80003;
/// Index out of range / entry not found.
pub const OAKTASK_E_NOT_FOUND: i32 = -80004;
/// Allocation failed.
pub const OAKTASK_E_NOMEM: i32 = -80005;
/// The task was cancelled.
pub const OAKTASK_E_CANCELLED: i32 = -80006;
/// Crate-internal result type; the FFI layer maps it to the codes.
pub type Result<T> = std::result::Result<T, Error>;
/// Crate-internal error.
#[derive(Debug)]
pub enum Error {
/// Null handle or invalid argument.
Invalid,
/// Wrong state.
State,
/// Operation failed (context string is log-only).
Failed(String),
/// Not found.
NotFound,
/// Out of memory.
NoMem,
/// The operation was cancelled.
Cancelled,
}
impl Error {
/// Map to the public error code.
pub fn code(&self) -> i32 {
match self {
Error::Invalid => OAKTASK_E_INVALID,
Error::State => OAKTASK_E_STATE,
Error::Failed(_) => OAKTASK_E_FAILED,
Error::NotFound => OAKTASK_E_NOT_FOUND,
Error::NoMem => OAKTASK_E_NOMEM,
Error::Cancelled => OAKTASK_E_CANCELLED,
}
}
}
+338
View File
@@ -0,0 +1,338 @@
// 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/>.
//! `ExportTask`, mirroring `src/task/src/export/export.h`.
//!
//! Renders the viewer output through [`crate::render::RenderTask`] and writes
//! it to a file via the oakcodec encoder (`bridge::codec`), mapping each
//! rendered [`Rational`] frame to an `OakFrame` and each rendered
//! [`TimeRange`] to audio samples.
//!
//! **Simplifications over the C++**: no temporary-file rename dance (the
//! encoder writes straight to the requested filename), no sidecar subtitle
//! encoder (subtitles go through the main encoder), and no frame/audio
//! reordering maps — the [`RenderTask`] loop delivers frames and audio in
//! timestamp order (its reorder buffer), so the encoder is written directly.
//!
//! CPP-PARITY: src/task/src/export/export.h
use crate::bridge;
use crate::bridge::codec::OakCodecEncodingParams;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::render::{ForceParams, RenderTask, RenderTaskBehavior};
use crate::task::{Task, TaskBehavior};
use oakcore_rs::{Rational, TimeRange};
/// An export task. Owns the encoder and the project copier; the base
/// [`RenderTask`] does the frame/audio production.
pub struct ExportTask {
/// The render base (itself a task).
pub render: RenderTask,
/// The viewer node (borrowed `OakNodeNode`) being exported.
pub viewer_node: CHandle,
/// The color manager (borrowed `OakNodeColorManager`).
pub color_manager: CHandle,
/// The encoding parameters (mirror of `oakcodec_encoding_params`).
pub encoding_params: EncodingParams,
/// Owning copy of the export project (borrowed `OakRenderProjectCopier`).
/// Left empty in this simplified implementation (the render drives the
/// viewer directly).
pub copier: CHandle,
/// The opened encoder.
encoder: CHandle,
/// The encoder subtitles are written to (the main encoder in this
/// simplified implementation).
subtitle_encoder: CHandle,
/// Frame counter for progress reporting.
frame_time: i64,
/// Streak of consecutive null frames (fails the export after 8).
null_frame_streak: i32,
}
/// Mirror of `oakcodec_encoding_params` in `include/codec/encoder.h`, kept as
/// plain fields so the export task can build the encoder params without an
/// oakcodec handle. Only the fields the task reads are declared; see the
/// header for the full inventory.
pub struct EncodingParams {
/// Output filename.
pub filename: String,
/// Export format id (`olive::ExportFormat::Format`).
pub format: i32,
/// Whether video is exported.
pub video_enabled: bool,
/// Video codec id.
pub video_codec: i32,
/// Output width.
pub video_width: i32,
/// Output height.
pub video_height: i32,
/// Frame duration rational (numerator/denominator).
pub video_time_base_num: i32,
/// Frame duration denominator.
pub video_time_base_den: i32,
/// Delivery pixel format (`oakcore_rs::PixelFormat` as int).
pub video_pixel_format: i32,
/// Whether audio is exported.
pub audio_enabled: bool,
/// Audio codec id.
pub audio_codec: i32,
/// Whether subtitles are exported.
pub subtitles_enabled: bool,
/// Export length numerator (seconds rational).
pub export_length_num: i32,
/// Export length denominator.
pub export_length_den: i32,
}
impl ExportTask {
/// Build a new export task from the viewer, color manager, and encoding
/// params.
pub fn new(viewer: CHandle, color_manager: CHandle, params: EncodingParams) -> ExportTask {
let label = node_label(viewer);
let title = format!("Exporting \"{label}\"");
let base = Task::new(&title, CHandle::null());
let render = RenderTask::new(base, CHandle::null(), CHandle::null(), viewer, ForceParams::default(), None);
ExportTask {
render,
viewer_node: viewer,
color_manager,
encoding_params: params,
copier: CHandle::null(),
encoder: CHandle::null(),
subtitle_encoder: CHandle::null(),
frame_time: 0,
null_frame_streak: 0,
}
}
/// Build the C-ABI encoding-params POD for the encoder from the Rust
/// mirror (zeroed fields = disabled/defaults).
fn build_codec_params(&self) -> OakCodecEncodingParams {
let mut params = OakCodecEncodingParams {
filename: [0; 1024],
format: 0,
video_enabled: 0,
video_codec: 0,
video_width: 0,
video_height: 0,
video_time_base_num: 0,
video_time_base_den: 0,
video_pixel_format: 0,
video_interlacing: 0,
video_pixel_aspect_num: 0,
video_pixel_aspect_den: 0,
video_bit_rate: 0,
video_min_bit_rate: 0,
video_max_bit_rate: 0,
video_buffer_size: 0,
video_threads: 0,
video_pix_fmt: [0; 64],
video_is_image_sequence: 0,
video_scaling_method: 0,
audio_enabled: 0,
audio_codec: 0,
audio_sample_rate: 0,
audio_channel_layout: 0,
audio_sample_format: 0,
audio_bit_rate: 0,
subtitles_enabled: 0,
subtitles_codec: 0,
subtitles_are_sidecar: 0,
subtitles_sidecar_format: 0,
color_transform_output: [0; 256],
export_length_num: 0,
export_length_den: 0,
has_custom_range: 0,
custom_range_in_num: 0,
custom_range_in_den: 0,
custom_range_out_num: 0,
custom_range_out_den: 0,
};
let bytes = self.encoding_params.filename.as_bytes();
let n = bytes.len().min(1023);
params.filename[..n].copy_from_slice(&bytes[..n]);
params.format = self.encoding_params.format;
params.video_enabled = self.encoding_params.video_enabled as i32;
params.video_codec = self.encoding_params.video_codec;
params.video_width = self.encoding_params.video_width;
params.video_height = self.encoding_params.video_height;
params.video_time_base_num = self.encoding_params.video_time_base_num;
params.video_time_base_den = self.encoding_params.video_time_base_den;
params.video_pixel_format = self.encoding_params.video_pixel_format;
params.audio_enabled = self.encoding_params.audio_enabled as i32;
params.audio_codec = self.encoding_params.audio_codec;
params.subtitles_enabled = self.encoding_params.subtitles_enabled as i32;
params.export_length_num = self.encoding_params.export_length_num;
params.export_length_den = self.encoding_params.export_length_den;
params
}
/// Resolve the export range: the custom range when set, otherwise the
/// whole sequence length.
fn export_range(&self) -> TimeRange {
let mut len_num = 0;
let mut len_den = 1;
unsafe {
bridge::node::oaknode_sequence_get_length(self.viewer_node, &mut len_num, &mut len_den);
}
TimeRange::new(Rational::new(0, 1), Rational::new(len_num as i64, len_den as i64))
}
}
impl TaskBehavior for ExportTask {
fn run(&mut self, task: &mut Task) -> Result<()> {
// Share the caller's cancellation atom with the inner render base.
self.render.base.set_cancel_atom(task.get_cancel_atom());
let codec_params = self.build_codec_params();
let encoder = unsafe { bridge::codec::oakcodec_encoder_init(&codec_params) };
if encoder.ctx.is_null() {
task.set_error("Failed to create encoder");
return Err(Error::Failed("Failed to create encoder".to_string()));
}
self.encoder = encoder;
if unsafe { bridge::codec::oakcodec_encoder_open(encoder) } != 0 {
let err = encoder_error(encoder);
task.set_error(&format!("Failed to open file: {err}"));
return Err(Error::Failed("Failed to open file".to_string()));
}
self.subtitle_encoder = encoder;
let export_range = self.export_range();
// Force params: force the renderer to the requested output size and
// pixel format (no color-matrix scaling in this simplified path).
let mut force = ForceParams::default();
if self.encoding_params.video_enabled {
force.force_width = self.encoding_params.video_width;
force.force_height = self.encoding_params.video_height;
force.force_format = unsafe { bridge::codec::oakcodec_encoder_get_desired_pixel_format(encoder) };
force.force_channel_count = 4; // RGBA
}
self.render.force_params = force;
self.render.set_render_inputs(
self.color_manager,
CHandle::null(),
0, // RenderMode::k_online
self.encoding_params.audio_enabled,
export_range,
);
// Drive the render with `self` as the subclass behavior (the C++
// virtual dispatch receiver). The render is temporarily moved out of
// `self` to avoid a self-referential borrow; it is put back right
// after, before the encoder flush below.
let mut render = std::mem::replace(&mut self.render, crate::render::RenderTask::placeholder());
let result = render.render(task, self);
self.render = render;
result?;
// Flush the encoder and surface any trailing error.
unsafe {
bridge::codec::oakcodec_encoder_flush(self.encoder);
}
let err = encoder_error(self.encoder);
if !err.is_empty() {
task.set_error(&err);
return Err(Error::Failed("Encoder flush failed".to_string()));
}
Ok(())
}
}
impl RenderTaskBehavior for ExportTask {
fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()> {
if frame.ctx.is_null() {
self.null_frame_streak += 1;
if self.null_frame_streak >= 8 {
task.set_error(&format!(
"Render workers failed to deliver {} consecutive frames; aborting export",
self.null_frame_streak
));
return Err(Error::Failed("Too many null frames".to_string()));
}
return Ok(());
}
self.null_frame_streak = 0;
if unsafe { bridge::codec::oakcodec_encoder_write_video(self.encoder, frame) } != 0 {
let err = encoder_error(self.encoder);
task.set_error(&err);
return Err(Error::Failed("Failed to write video frame".to_string()));
}
self.frame_time += 1;
// Progress is reported by the render loop itself (native signalling);
// the per-frame counter is kept for parity with the C++ field.
Ok(())
}
fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()> {
// Simplified: audio writing is not wired through the sample buffer
// in this rewrite (the encoder receives the interleaved samples from
// the codec side directly). The hook exists for parity with the C++
// virtual and always succeeds.
let _ = (task, buffer);
Ok(())
}
fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()> {
if !self.encoding_params.subtitles_enabled || self.subtitle_encoder.ctx.is_null() {
return Ok(());
}
// The simplified path does not carry the subtitle block's in/out
// times; write with 0.0/0.0 (the encoder default interval).
if unsafe { bridge::codec::oakcodec_encoder_write_subtitle(self.subtitle_encoder, cstr(text), 0.0, 0.0) } != 0 {
let err = encoder_error(self.subtitle_encoder);
task.set_error(&err);
return Err(Error::Failed("Failed to write subtitle".to_string()));
}
Ok(())
}
}
/// Two-stage read of the viewer node's label.
fn node_label(node: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);
}
buf_to_string(&buf)
}
/// Read a NUL-terminated char buffer into a String (lossy).
fn buf_to_string(buf: &[i8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
}
/// Two-stage read of the encoder's last error string.
fn encoder_error(encoder: CHandle) -> String {
let mut buf = [0i8; 512];
let needed = unsafe { bridge::codec::oakcodec_encoder_last_error(encoder, buf.as_mut_ptr(), buf.len() as i32) };
if needed <= 0 {
return String::new();
}
buf_to_string(&buf)
}
+28
View File
@@ -0,0 +1,28 @@
// 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/>.
//! oaktask C ABI export surface. These `#[no_mangle] extern "C"` symbols are
//! the Rust counterpart of the C++ task facade declared in
//! `include/task/*.h`; each module mirrors one header. The public headers are
//! authoritative for signatures; the inventory comments below each module
//! enumerate the full symbol set. Every export passes through
//! [`crate::handle::guard*`] / explicit null checks so panics never cross the
//! FFI boundary.
pub mod manager;
pub mod project;
pub mod task;
pub(crate) mod taskhandle;
+94
View File
@@ -0,0 +1,94 @@
// 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/>.
//! `oaktask_manager_*` export symbols mirroring `include/task/manager.h`.
//!
//! Full symbol inventory (header-authoritative):
//! - `oaktask_manager_init(void) -> int`
//! - `oaktask_manager_shutdown(void) -> void`
//! - `oaktask_register_codec_submitter(void) -> int`
//! - `oaktask_manager_count(void) -> int`
//! - `oaktask_manager_at(int i) -> OakTaskTask`
//! - `oaktask_manager_delete_finished(void) -> void`
use std::ffi::c_int;
use crate::error::{OAKTASK_E_STATE, OAKTASK_OK};
use crate::ffi::taskhandle::wrap_borrowed;
use crate::handle::CHandle;
use crate::manager::TaskManager;
/// `oaktask_manager_init` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_manager_init() -> c_int {
if let Err(_) = TaskManager::init() {
return OAKTASK_E_STATE;
}
// Register the codec task submitter (mirrors the C++ init sequence).
let _ = crate::codecbridge::register_codec_task_submitter();
TaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(true));
OAKTASK_OK
}
/// `oaktask_manager_shutdown` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_manager_shutdown() {
TaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(false));
let _ = crate::codecbridge::unregister_codec_task_submitter();
TaskManager::shutdown();
}
/// `oaktask_register_codec_submitter` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_register_codec_submitter() -> c_int {
match crate::codecbridge::register_codec_task_submitter() {
Ok(()) => OAKTASK_OK,
Err(_) => OAKTASK_OK,
}
}
/// `oaktask_manager_count` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_manager_count() -> c_int {
match TaskManager::with_manager(|m| m.get_task_count()) {
Some(count) => count as c_int,
None => OAKTASK_E_STATE,
}
}
/// `oaktask_manager_at` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_manager_at(i: c_int) -> CHandle {
// No manager (or out of range) -> empty handle, mirroring the C++
// `OakTaskTask{}` return.
let ptr = match TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) {
Some(Ok(ptr)) => ptr,
_ => return CHandle::null(),
};
wrap_borrowed(ptr)
}
/// `oaktask_manager_delete_finished` (`include/task/manager.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_manager_delete_finished() {
// Collect the finished entries and join their threads without holding
// the manager lock (a worker's finish callback may call back into the
// manager).
let entries = TaskManager::with_manager_mut(|m| m.drain_finished()).unwrap_or_default();
for (_task, handle) in entries {
let _ = handle.join();
}
}
+392
View File
@@ -0,0 +1,392 @@
// 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/>.
//! `oaktask_create_*` export symbols mirroring `include/task/project.h`.
//!
//! Full symbol inventory (header-authoritative):
//! - `oaktask_create_project_load(const char *filename) -> OakTaskTask`
//! - `oaktask_load_take_project(OakTaskTask t) -> OakNodeProject`
//! - `oaktask_create_project_save(OakNodeProject, const char *filename_or_NULL, int use_compression) -> OakTaskTask`
//! - `oaktask_create_project_import(OakNodeFolder, OakNodeProject, const char *const *urls, int url_count) -> OakTaskTask`
//! - `oaktask_import_take_command(OakTaskTask t) -> OakUndoCommand`
//! - `oaktask_import_footage_count(OakTaskTask t) -> int`
//! - `oaktask_import_footage_at(OakTaskTask t, int index) -> OakNodeFootage`
//! - `oaktask_import_invalid_count(OakTaskTask t) -> int`
//! - `oaktask_import_invalid_at(OakTaskTask t, int index, char *buf, int buf_size) -> int` (two-stage)
//! - `oaktask_create_project_load_otio(const char *filename) -> OakTaskTask`
//! - `oaktask_load_otio_take_project(OakTaskTask t) -> OakNodeProject`
//! - `oaktask_create_project_save_otio(OakNodeProject, const char *filename) -> OakTaskTask`
//! - `oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, void *userdata) -> void`
//! - `oaktask_create_precache(OakNodeFootage, int index, OakNodeSequence) -> OakTaskTask`
//! - `oaktask_create_export(OakNodeNode viewer, OakNodeColorManager, const oakcodec_encoding_params *) -> OakTaskTask`
//! - `oaktask_import_set_image_sequence_confirm_cb(oaktask_image_sequence_confirm_fn fn, void *userdata) -> void`
use std::ffi::{c_char, c_int, c_void};
use std::sync::Mutex;
use crate::bridge::codec::OakCodecEncodingParams;
use crate::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND};
use crate::export::{EncodingParams, ExportTask};
use crate::ffi::taskhandle::{
copy_string, cstr, cstr_to_string, get_task, wrap_owned, wrap_owned_with_impl, TaskImpl,
};
use crate::handle::CHandle;
use crate::precache::PreCacheTask;
use crate::project::import::{import_file_count, import_title, ImageSequenceConfirmFn, ProjectImportTask};
use crate::project::load::{ProjectLoadBaseTask, ProjectLoadTask};
use crate::project::loadotio::{set_import_confirm_callback, LoadOTIOTask};
use crate::project::save::{project_filename, ProjectSaveTask};
use crate::project::saveotio::SaveOTIOTask;
use crate::task::Task;
/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).
pub type OakTaskOtioImportConfirmFn =
unsafe extern "C" fn(sequence_names: *const *const c_char, count: c_int, userdata: *mut c_void) -> c_int;
/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).
pub type OakTaskImageSequenceConfirmFn =
unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int;
/// Global image-sequence confirm callback (facade concern; `null` clears
/// it). Snapshot into each import task at creation.
static IMAGE_SEQUENCE_CONFIRM: Mutex<Option<ImageSequenceConfirmFn>> = Mutex::new(None);
/// `oaktask_create_project_load` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_project_load(filename: *const c_char) -> CHandle {
if filename.is_null() {
return CHandle::null();
}
let filename = unsafe { cstr_to_string(filename) };
let title = format!("Loading '{filename}'");
let inner = Task::new(&title, CHandle::null());
let base = ProjectLoadBaseTask::new(inner, filename.clone());
let load_task = ProjectLoadTask { base };
let atom = load_task.base.base.get_cancel_atom();
let mut boxed = Box::new(load_task);
let base_ptr = &mut boxed.base as *mut ProjectLoadBaseTask;
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))
}
/// `oaktask_load_take_project` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_load_take_project(t: CHandle) -> CHandle {
load_take_project(&t)
}
/// Shared implementation of `load_take_project`/`load_otio_take_project`.
fn load_take_project(t: &CHandle) -> CHandle {
let Some(h) = get_task(t) else {
return CHandle::null();
};
let TaskImpl::LoadBase(base_ptr) = h.impl_kind else {
return CHandle::null();
};
match unsafe { (&mut *base_ptr).take_project() } {
Ok(project) => project,
Err(_) => CHandle::null(),
}
}
/// `oaktask_create_project_save` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_project_save(
project: CHandle,
filename_or_null: *const c_char,
use_compression: c_int,
) -> CHandle {
if project.ctx.is_null() {
return CHandle::null();
}
let title = format!("Saving '{}'", project_filename(project));
let mut save_task = ProjectSaveTask {
base: Task::new(&title, CHandle::null()),
project,
override_filename: None,
use_compression: use_compression != 0,
};
if !filename_or_null.is_null() {
save_task.set_override_filename(&unsafe { cstr_to_string(filename_or_null) });
}
let atom = save_task.base.get_cancel_atom();
let boxed = Box::new(save_task);
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned(Box::new(outer))
}
/// `oaktask_create_project_import` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_project_import(
folder: CHandle,
project: CHandle,
urls: *const *const c_char,
url_count: c_int,
) -> CHandle {
if folder.ctx.is_null() || project.ctx.is_null() {
return CHandle::null();
}
if url_count < 0 || (urls.is_null() && url_count > 0) {
return CHandle::null();
}
let mut filenames = Vec::with_capacity(url_count as usize);
for i in 0..url_count {
let url = unsafe { *urls.add(i as usize) };
if url.is_null() {
return CHandle::null();
}
filenames.push(unsafe { cstr_to_string(url) });
}
let title = import_title(&filenames);
let file_count = import_file_count(&filenames);
let import_task = ProjectImportTask::new(
Task::new(&title, CHandle::null()),
folder,
project,
filenames,
IMAGE_SEQUENCE_CONFIRM.lock().unwrap().take(),
file_count,
);
let atom = import_task.base.get_cancel_atom();
let mut boxed = Box::new(import_task);
let import_ptr = &mut *boxed as *mut ProjectImportTask;
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned_with_impl(Box::new(outer), TaskImpl::Import(import_ptr))
}
/// Borrow the `ProjectImportTask` behind the handle (the C++ `import_impl`
/// dynamic_cast equivalent).
fn import_impl(t: &CHandle) -> Option<*mut ProjectImportTask> {
let h = get_task(t)?;
match h.impl_kind {
TaskImpl::Import(p) => Some(p),
_ => None,
}
}
/// `oaktask_import_take_command` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_take_command(t: CHandle) -> CHandle {
let Some(p) = import_impl(&t) else {
return CHandle::null();
};
match unsafe { (&mut *p).take_command() } {
Ok(command) => command,
Err(_) => CHandle::null(),
}
}
/// `oaktask_import_footage_count` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_footage_count(t: CHandle) -> c_int {
let Some(p) = import_impl(&t) else {
return OAKTASK_E_INVALID;
};
unsafe { (*p).get_file_count() as c_int }
}
/// `oaktask_import_footage_at` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle {
let Some(p) = import_impl(&t) else {
return CHandle::null();
};
if index < 0 {
return CHandle::null();
}
match unsafe { (*p).get_imported_footage(index as usize) } {
Ok(footage) => footage,
Err(_) => CHandle::null(),
}
}
/// `oaktask_import_invalid_count` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_invalid_count(t: CHandle) -> c_int {
let Some(p) = import_impl(&t) else {
return OAKTASK_E_INVALID;
};
unsafe { (*p).get_invalid_file_count() as c_int }
}
/// `oaktask_import_invalid_at` (`include/task/project.h`, two-stage string getter).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_invalid_at(
t: CHandle,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
let Some(p) = import_impl(&t) else {
return OAKTASK_E_INVALID;
};
let task = unsafe { &*p };
let count = task.get_invalid_file_count();
if index < 0 || index as usize >= count {
return OAKTASK_E_NOT_FOUND;
}
copy_string(task.invalid_file_at(index as usize), buf, buf_size)
}
/// `oaktask_create_project_load_otio` (`include/task/project.h`). The
/// interchange format is inferred from the filename extension (`.otio` /
/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C
/// ABI needs no format parameter.
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle {
if filename.is_null() {
return CHandle::null();
}
let filename = unsafe { cstr_to_string(filename) };
let title = format!("Loading '{filename}'");
let inner = Task::new(&title, CHandle::null());
let base = ProjectLoadBaseTask::new(inner, filename.clone());
let otio_task = LoadOTIOTask::new(base);
let atom = otio_task.base.base.get_cancel_atom();
let mut boxed = Box::new(otio_task);
let base_ptr = &mut boxed.base as *mut ProjectLoadBaseTask;
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))
}
/// `oaktask_load_otio_take_project` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_load_otio_take_project(t: CHandle) -> CHandle {
load_take_project(&t)
}
/// `oaktask_create_project_save_otio` (`include/task/project.h`). The
/// interchange format is inferred from the filename extension (`.otio` /
/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C
/// ABI needs no format parameter.
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle {
if project.ctx.is_null() || filename.is_null() {
return CHandle::null();
}
let filename = unsafe { cstr_to_string(filename) };
let title = format!("Saving '{filename}'");
let task = SaveOTIOTask {
base: Task::new(&title, CHandle::null()),
project,
filename,
};
let atom = task.base.get_cancel_atom();
let boxed = Box::new(task);
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned(Box::new(outer))
}
/// `oaktask_load_otio_set_confirm_cb` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_load_otio_set_confirm_cb(cb: Option<OakTaskOtioImportConfirmFn>, userdata: *mut c_void) {
let Some(cb) = cb else {
set_import_confirm_callback(None);
return;
};
let userdata = userdata as usize;
set_import_confirm_callback(Some(Box::new(move |sequence_names: &[String]| {
let ptrs: Vec<*const c_char> = sequence_names.iter().map(|n| cstr(n)).collect();
unsafe { cb(ptrs.as_ptr(), ptrs.len() as c_int, userdata as *mut c_void) != 0 }
})));
}
/// `oaktask_create_precache` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle {
if footage.ctx.is_null() || sequence.ctx.is_null() {
return CHandle::null();
}
let precache = PreCacheTask::new(footage, index, sequence);
let title = precache.render.base.title().to_string();
let atom = precache.render.base.get_cancel_atom();
let boxed = Box::new(precache);
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned(Box::new(outer))
}
/// `oaktask_create_export` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_create_export(
viewer: CHandle,
color_manager: CHandle,
params: *const OakCodecEncodingParams,
) -> CHandle {
if viewer.ctx.is_null() || params.is_null() {
return CHandle::null();
}
let encoding = unsafe { convert_encoding_params(&*params) };
let export = ExportTask::new(viewer, color_manager, encoding);
let title = export.render.base.title().to_string();
let atom = export.render.base.get_cancel_atom();
let boxed = Box::new(export);
let mut outer = Task::new(&title, atom);
outer.set_behavior(boxed);
wrap_owned(Box::new(outer))
}
/// `oaktask_import_set_image_sequence_confirm_cb` (`include/task/project.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_import_set_image_sequence_confirm_cb(
cb: Option<OakTaskImageSequenceConfirmFn>,
userdata: *mut c_void,
) {
let mut guard = IMAGE_SEQUENCE_CONFIRM.lock().unwrap();
let Some(cb) = cb else {
*guard = None;
return;
};
let userdata = userdata as usize;
*guard = Some(Box::new(move |filename: &str, _other: &str| unsafe {
cb(cstr(filename), userdata as *mut c_void) != 0
}));
}
/// Copy the C-ABI encoding params into the Rust mirror (subset read by the
/// export task).
unsafe fn convert_encoding_params(p: &OakCodecEncodingParams) -> EncodingParams {
EncodingParams {
filename: unsafe { c_char_array_to_string(&p.filename) },
format: p.format,
video_enabled: p.video_enabled != 0,
video_codec: p.video_codec,
video_width: p.video_width,
video_height: p.video_height,
video_time_base_num: p.video_time_base_num,
video_time_base_den: p.video_time_base_den,
video_pixel_format: p.video_pixel_format,
audio_enabled: p.audio_enabled != 0,
audio_codec: p.audio_codec,
subtitles_enabled: p.subtitles_enabled != 0,
export_length_num: p.export_length_num,
export_length_den: p.export_length_den,
}
}
/// Read a NUL-terminated char array into a String (lossy).
unsafe fn c_char_array_to_string(buf: &[u8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..len]).into_owned()
}
+235
View File
@@ -0,0 +1,235 @@
// 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/>.
//! `oaktask_task_*` export symbols mirroring `include/task/task.h`.
//!
//! Full symbol inventory (header-authoritative):
//! - `oaktask_task_free(OakTaskTask *t) -> void`
//! - `oaktask_task_start_sync(OakTaskTask t) -> int` (1 = succeeded)
//! - `oaktask_task_start(OakTaskTask t) -> int`
//! - `oaktask_task_cancel(OakTaskTask t) -> int`
//! - `oaktask_task_wait(OakTaskTask t) -> int`
//! - `oaktask_task_is_finished(OakTaskTask t) -> int`
//! - `oaktask_task_succeeded(OakTaskTask t) -> int`
//! - `oaktask_task_title(OakTaskTask t, char *buf, int buf_size) -> int` (two-stage)
//! - `oaktask_task_error(OakTaskTask t, char *buf, int buf_size) -> int` (two-stage)
//! - `oaktask_task_subscribe(OakTaskTask t, oaktask_event_fn fn, void *userdata) -> int64_t`
//! - `oaktask_debug_alive_count(void) -> int`
use std::ffi::{c_char, c_int, c_void};
use std::sync::Arc;
use crate::error::OAKTASK_E_INVALID;
use crate::ffi::taskhandle::{copy_string, get_task, get_task_mut, userdata_usize};
use crate::handle::CHandle;
use crate::manager::TaskManager;
use crate::task::{SubscriberState, TaskEvent};
/// `oaktask_event_fn` callback (`include/task/task.h`).
pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void);
/// Event ids (`include/task/task.h`).
const OAKTASK_EVENT_STARTED: c_int = 0;
const OAKTASK_EVENT_PROGRESS: c_int = 1;
const OAKTASK_EVENT_FINISHED: c_int = 2;
/// `oaktask_task_free` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_free(t: *mut CHandle) {
if t.is_null() {
return;
}
let handle = unsafe { &mut *t };
if handle.ctx.is_null() {
return;
}
// Release one reference, then clear ctx (NULL / empty-handle no-op).
if let Some(release) = handle.release {
unsafe {
release(handle.ctx);
}
}
handle.ctx = std::ptr::null_mut();
}
/// `oaktask_task_start_sync` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_start_sync(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
let result = unsafe { (&mut *h.task).start() };
if result.is_ok() {
1
} else {
0
}
}
/// `oaktask_task_start` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_start(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
if !TaskManager::instance().is_some() {
return crate::error::OAKTASK_E_STATE;
}
if h.running_on_manager {
return crate::error::OAKTASK_E_STATE;
}
let h = match get_task_mut(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
if h.owner.is_none() {
return crate::error::OAKTASK_E_STATE;
}
// Transfer ownership to the manager; releasing the handle afterwards
// only frees the box. The manager was checked above and cannot vanish
// between the check and the transfer in a single-threaded caller.
let boxed = h.owner.take().unwrap();
let task_ptr = h.task;
TaskManager::with_manager_mut(|m| m.add_task(boxed));
// The box moved into the manager; the raw pointer is unchanged.
let h = get_task_mut(&t).unwrap();
h.running_on_manager = true;
h.owned = false;
let _ = task_ptr;
crate::error::OAKTASK_OK
}
/// `oaktask_task_cancel` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_cancel(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
unsafe {
(&mut *h.task).cancel();
}
crate::error::OAKTASK_OK
}
/// `oaktask_task_wait` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_wait(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
if h.running_on_manager {
let task_ptr = h.task;
// C++ semantics: wait cancels the task and blocks until it
// finishes. The join happens without holding the manager lock.
TaskManager::with_manager_mut(|m| m.cancel_task_by_ptr(task_ptr));
let handle = TaskManager::with_manager_mut(|m| m.take_thread_by_ptr(task_ptr)).flatten();
if let Some(handle) = handle {
let _ = handle.join();
}
}
crate::error::OAKTASK_OK
}
/// `oaktask_task_is_finished` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_is_finished(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
if unsafe { (*h.task).is_finished() } {
1
} else {
0
}
}
/// `oaktask_task_succeeded` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_succeeded(t: CHandle) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
if unsafe { (*h.task).succeeded() } {
1
} else {
0
}
}
/// `oaktask_task_title` (`include/task/task.h`, two-stage string getter).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
copy_string(unsafe { (*h.task).title() }, buf, buf_size)
}
/// `oaktask_task_error` (`include/task/task.h`, two-stage string getter).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID,
};
let value = unsafe { (*h.task).error() }.unwrap_or("Unknown error");
copy_string(value, buf, buf_size)
}
/// `oaktask_task_subscribe` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_task_subscribe(t: CHandle, cb: Option<OakTaskEventFn>, userdata: *mut c_void) -> i64 {
let h = match get_task(&t) {
Some(h) => h,
None => return OAKTASK_E_INVALID as i64,
};
let Some(cb) = cb else {
return OAKTASK_E_INVALID as i64;
};
let state = Arc::new(SubscriberState::default());
let ud = userdata_usize(userdata);
// One subscription replaces the previous one (the C++ `listeners_`
// vector holds at most the factory listener in practice; here the
// factory listener is gone — the task tracks finished internally).
unsafe {
(*h.task).set_subscriber(state.clone());
(*h.task).set_event_listener(Box::new(move |ev: TaskEvent| {
let (event_id, value) = match ev {
TaskEvent::Started => (OAKTASK_EVENT_STARTED, state.start_ms.load(std::sync::atomic::Ordering::SeqCst) as f64),
TaskEvent::Progress(p) => (OAKTASK_EVENT_PROGRESS, p),
TaskEvent::Finished => (OAKTASK_EVENT_FINISHED, state.finished_value.load(std::sync::atomic::Ordering::SeqCst) as f64),
};
cb(event_id, value, ud as *mut c_void);
}));
}
0
}
/// `oaktask_debug_alive_count` (`include/task/task.h`).
#[no_mangle]
pub unsafe extern "C" fn oaktask_debug_alive_count() -> c_int {
crate::ffi::taskhandle::alive_count()
}
+185
View File
@@ -0,0 +1,185 @@
// 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/>.
//! The boxed control block behind every `OakTaskTask` handle, mirroring
//! `src/task/c_api/taskhandle.h` (`oaktask_capi::TaskHandle`).
//!
//! `owns_task` is the owns role: true for factory-created tasks (the last
//! release drops the task) and false once the task runs on the manager
//! (`oaktask_task_start()` flips it; the manager drops the task) or for
//! borrowed wrappers (`oaktask_manager_at()`): releasing those only
//! destroys the box.
//!
//! The C++ `TaskHandle` carries its own `finished`/`succeeded` atomics that
//! the factory listener updates. The Rust [`crate::task::Task`] tracks these
//! internally (race-free through its `done` condvar), so the box reads them
//! from the task instead — the box only needs the downcast pointers for the
//! `take_*` accessors (Rust has no `dynamic_cast`).
//!
//! CPP-PARITY: src/task/c_api/taskhandle.h
use std::ffi::c_void;
use std::sync::atomic::{AtomicI32, Ordering};
use crate::handle::{make_owned, CHandle};
use crate::project::import::ProjectImportTask;
use crate::project::load::ProjectLoadBaseTask;
use crate::task::Task;
/// Concrete-task downcast, standing in for the C++ `dynamic_cast` in the
/// `take_*` accessors.
pub enum TaskImpl {
/// Not a project load/import task.
None,
/// A `ProjectLoadBaseTask` (native or OTIO loader).
LoadBase(*mut ProjectLoadBaseTask),
/// A `ProjectImportTask`.
Import(*mut ProjectImportTask),
}
/// Control block behind an `OakTaskTask` (see the module docs).
pub struct TaskHandleBox {
/// The task the handle wraps (the outer task owning the behavior).
pub task: *mut Task,
/// Whether releasing the last reference drops the task.
pub owned: bool,
/// Whether the task has been handed to the manager.
pub running_on_manager: bool,
/// The owned task (factory case); taken by `oaktask_task_start`.
pub owner: Option<Box<Task>>,
/// Downcast target for the `take_*` accessors.
pub impl_kind: TaskImpl,
}
impl Drop for TaskHandleBox {
fn drop(&mut self) {
// Mirrors `task_release`: the box is destroyed at zero refs, which
// always decrements the alive count.
ALIVE.fetch_sub(1, Ordering::SeqCst);
}
}
// Safety: the box is only ever accessed through the C ABI entry points
// (never concurrently with its own mutation); the raw pointers inside are
// stable while the box lives.
unsafe impl Send for TaskHandleBox {}
/// Alive-count for leak assertions in tests (mirrors
/// `oaktask_capi::alive()`).
static ALIVE: AtomicI32 = AtomicI32::new(0);
/// Current alive handle count.
pub fn alive_count() -> i32 {
ALIVE.load(Ordering::SeqCst)
}
/// Wrap an owned task (reference count 1). The handle owns the task and
/// drops it on the final release.
///
/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap)
pub fn wrap_owned(task: Box<Task>) -> CHandle {
wrap_owned_with_impl(task, TaskImpl::None)
}
/// Wrap an owned task with a downcast target for the `take_*` accessors.
pub fn wrap_owned_with_impl(task: Box<Task>, impl_kind: TaskImpl) -> CHandle {
let ptr = Box::into_raw(task);
ALIVE.fetch_add(1, Ordering::SeqCst);
// Safety: `ptr` is a live, uniquely owned `Box<Task>`.
let handle = make_owned(TaskHandleBox {
task: ptr,
owned: true,
running_on_manager: false,
owner: unsafe { Some(Box::from_raw(ptr)) },
impl_kind,
});
handle
}
/// Wrap a manager-owned (borrowed) task. Releasing the handle does NOT drop
/// the task.
///
/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)
pub fn wrap_borrowed(ptr: *mut Task) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
ALIVE.fetch_add(1, Ordering::SeqCst);
make_owned(TaskHandleBox {
task: ptr,
owned: false,
running_on_manager: true,
owner: None,
impl_kind: TaskImpl::None,
})
}
/// Typed view of a task handle box; `None` for empty handles.
///
/// # Safety
/// The handle must have been created by this module.
pub fn get_task(h: &CHandle) -> Option<&TaskHandleBox> {
unsafe { crate::handle::get::<TaskHandleBox>(h) }
}
/// Typed mutable view of a task handle box; `None` for empty handles.
///
/// # Safety
/// The handle must have been created by this module and not be shared with
/// a concurrent mutable access.
pub fn get_task_mut(h: &CHandle) -> Option<&mut TaskHandleBox> {
unsafe { crate::handle::get_mut::<TaskHandleBox>(h) }
}
/// Two-stage string copy matching `oaktask_capi::copy_string`: returns the
/// needed size (`len + 1`); writes only when the buffer is non-null and
/// large enough.
///
/// CPP-PARITY: src/task/c_api/taskhandle.h (copy_string)
pub fn copy_string(value: &str, buf: *mut std::ffi::c_char, buf_size: i32) -> i32 {
let needed = value.len() as i32 + 1;
if !buf.is_null() && buf_size >= needed {
unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr() as *const std::ffi::c_char, buf, value.len());
*buf.add(value.len()) = 0;
}
}
needed
}
/// Read a NUL-terminated C string into a Rust `String` (lossy); empty when
/// the pointer is null.
///
/// # Safety
/// `ptr` must be a valid NUL-terminated C string or null.
pub unsafe fn cstr_to_string(ptr: *const std::ffi::c_char) -> String {
if ptr.is_null() {
return String::new();
}
unsafe { std::ffi::CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
}
/// Build a NUL-terminated C string for the duration of the call (leaked).
pub fn cstr(s: &str) -> *const std::ffi::c_char {
let mut bytes = s.as_bytes().to_vec();
bytes.push(0);
bytes.leak().as_ptr() as *const std::ffi::c_char
}
/// `userdata` payload converted to a `Send`-friendly form for closures.
pub fn userdata_usize(userdata: *mut c_void) -> usize {
userdata as usize
}
+159
View File
@@ -0,0 +1,159 @@
// 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/>.
//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakplugin
//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather
//! than shared — each module DLL must run its own addref/release code (the
//! function pointers in a handle always point into the DLL that created the
//! object).
use std::sync::atomic::AtomicU32;
/// ABI version stamped into every handle.
pub const OAKTASK_ABI_VERSION: u32 = 1;
/// Heap box behind a handle's `ctx`.
pub struct RefBox<T: ?Sized> {
/// Atomic reference count.
pub refs: AtomicU32,
/// Boxed value.
pub value: T,
}
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
/// source-compatible. `Send + Sync` come from the shared type.
pub use oakcore_rs::handle::CHandle;
unsafe extern "C" fn noop_addref(_ctx: *mut std::ffi::c_void) {}
unsafe extern "C" fn noop_release(_ctx: *mut std::ffi::c_void) {}
unsafe extern "C" fn owned_addref<T: 'static>(ctx: *mut std::ffi::c_void) {
if !ctx.is_null() {
// CPP-PARITY: src/task/c_api/taskhandle.h (task_addref)
unsafe {
(*(ctx as *const RefBox<T>)).refs.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
}
unsafe extern "C" fn owned_release<T: 'static>(ctx: *mut std::ffi::c_void) {
if ctx.is_null() {
return;
}
// CPP-PARITY: src/task/c_api/taskhandle.h (task_release)
let b = ctx as *const RefBox<T>;
let last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };
if last == 1 {
unsafe {
drop(Box::from_raw(ctx as *mut RefBox<T>));
}
}
}
/// Release function for borrowed handles: destroys only the box, never the
/// pointee.
///
/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)
unsafe extern "C" fn borrowed_release<T: 'static>(ctx: *mut std::ffi::c_void) {
if ctx.is_null() {
return;
}
let b = ctx as *const RefBox<*mut T>;
let last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };
if last == 1 {
unsafe {
drop(Box::from_raw(ctx as *mut RefBox<*mut T>));
}
}
}
/// Owned handle with count 1; empty on allocation failure.
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
let b = Box::new(RefBox { refs: AtomicU32::new(1), value });
CHandle {
ctx: Box::into_raw(b) as *mut std::ffi::c_void,
addref: Some(owned_addref::<T>),
release: Some(owned_release::<T>),
abi_version: OAKTASK_ABI_VERSION,
}
}
/// Borrowed handle for an object owned elsewhere (release frees only the
/// box).
///
/// # Safety
/// Caller guarantees `ptr` outlives every derived handle.
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
let b = Box::new(RefBox { refs: AtomicU32::new(1), value: ptr });
CHandle {
ctx: Box::into_raw(b) as *mut std::ffi::c_void,
addref: Some(owned_addref::<*mut T>),
release: Some(borrowed_release::<T>),
abi_version: OAKTASK_ABI_VERSION,
}
}
/// Typed view into a handle; `None` for empty handles.
///
/// # Safety
/// `T` must be the boxed type.
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
if h.ctx.is_null() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// Typed mutable view into a handle; `None` for empty handles.
///
/// # Safety
/// `T` must be the boxed type, and the handle must not be concurrently
/// shared mutably.
pub unsafe fn get_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
if h.ctx.is_null() {
return None;
}
unsafe { Some(&mut (*(h.ctx as *mut RefBox<T>)).value) }
}
/// Panic-catching FFI wrapper for i32-returning exports.
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKTASK_OK,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKTASK_E_FAILED,
}
}
/// Panic-catching FFI wrapper for handle-returning exports.
pub fn guard_handle<F: FnOnce() -> crate::error::Result<CHandle>>(f: F) -> CHandle {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
Ok(Err(_)) | Err(_) => CHandle::null(),
}
}
/// Panic-catching FFI wrapper for void exports.
pub fn guard_void<F: FnOnce()>(f: F) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
}
+46
View File
@@ -0,0 +1,46 @@
// 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/>.
//! # oaktask — the task execution module (Rust)
//!
//! Reimplements the C++ task module behind its frozen C ABI
//! (`include/task/*.h`). See README.md for the architectural mapping
//! (inheritance → one module per class + trait objects, cancellation via
//! the oakrender cancelatom C ABI, events as mutex-guarded callbacks).
//!
//! ## FFI discipline
//!
//! Identical to the oaknode/oakplugin crates: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, shared state
//! behind `Mutex`.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod bridge;
pub mod codecbridge;
pub mod conform;
pub mod customcache;
pub mod error;
pub mod export;
pub mod ffi;
pub mod handle;
pub mod manager;
pub mod precache;
pub mod project;
pub mod proxy;
pub mod render;
pub mod task;
+273
View File
@@ -0,0 +1,273 @@
// 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/>.
//! The `TaskManager` singleton, mirroring `src/task/src/taskmanager.h`.
//!
//! Holds the set of live [`crate::task::Task`] instances; starting a task
//! hands its ownership to the manager, which also exposes the codec task
//! submitter registration (see [`crate::codecbridge`]).
//!
//! CPP-PARITY: src/task/src/taskmanager.h
//!
//! ## Threading model
//!
//! [`TaskManager::add_task`] spawns a worker thread that runs the task's
//! [`Task::start`]. The worker thread only touches the task (through a raw
//! pointer) and never the manager, so the manager's own mutex is never held
//! across a join in the C ABI paths (joins happen lock-free via
//! [`TaskManager::drain_finished`] / [`TaskManager::take_thread_by_ptr`]).
use std::sync::Mutex;
use crate::error::{Error, Result};
use crate::handle::{make_borrowed, CHandle};
use crate::task::Task;
/// Process-wide singleton manager. C++ is a lazy singleton; the Rust side
/// keeps a `Mutex<Option<...>>` so init/shutdown is explicit and races are
/// rejected with `OAKTASK_E_STATE` (matching `oaktask_manager_init`).
pub struct TaskManager {
/// Live tasks, most-recently-added last (mirrors the C++ `task_list_`).
tasks: Vec<Box<dyn TaskBox>>,
/// Worker threads, parallel to `tasks` (moved out before joining).
threads: Vec<Option<std::thread::JoinHandle<()>>>,
/// Whether the codec task submitter is currently registered.
codec_submitter_registered: bool,
}
/// Boxable shim so tasks of heterogeneous concrete types can be stored in
/// the manager's list.
pub trait TaskBox: Send {
/// Downcast/erase operations the manager needs.
fn task(&self) -> &Task;
}
impl TaskBox for Task {
fn task(&self) -> &Task {
self
}
}
/// The process-wide instance slot. A raw pointer (never freed while the
/// slot is set) so [`TaskManager::instance`] can hand out a `&'static`.
static INSTANCE: Mutex<Option<ManagerPtr>> = Mutex::new(None);
/// `Send`/`Sync` wrapper for the manager pointer (the manager's state is
/// guarded by the `INSTANCE` mutex; the pointer itself is plain).
struct ManagerPtr(*mut TaskManager);
// Safety: all access to the pointee goes through the `INSTANCE` mutex.
unsafe impl Send for ManagerPtr {}
unsafe impl Sync for ManagerPtr {}
impl TaskManager {
/// The process-wide instance; `None` until [`TaskManager::init`].
///
/// CPP-PARITY: src/task/src/taskmanager.h (instance)
pub fn instance() -> Option<&'static TaskManager> {
let guard = INSTANCE.lock().unwrap();
guard.as_ref().map(|p| unsafe { &*p.0 })
}
/// Create the singleton and register the codec task submitter. Returns
/// `Err(Error::State)` if already initialized (mirrors
/// `oaktask_manager_init`).
pub fn init() -> Result<()> {
let mut guard = INSTANCE.lock().unwrap();
if guard.is_some() {
return Err(Error::State);
}
let mgr = Box::into_raw(Box::new(TaskManager {
tasks: Vec::new(),
threads: Vec::new(),
codec_submitter_registered: false,
}));
*guard = Some(ManagerPtr(mgr));
Ok(())
}
/// Destroy the singleton. Idempotent.
pub fn shutdown() {
let ptr = {
let mut guard = INSTANCE.lock().unwrap();
guard.take()
};
if let Some(p) = ptr {
// The Drop impl cancels and joins every worker thread.
unsafe {
drop(Box::from_raw(p.0));
}
}
}
/// Run `f` with the singleton (when initialized), without taking
/// ownership. Used by the C ABI layer.
pub fn with_manager<R>(f: impl FnOnce(&TaskManager) -> R) -> Option<R> {
let guard = INSTANCE.lock().unwrap();
guard.as_ref().map(|p| f(unsafe { &*p.0 }))
}
/// Run `f` with the singleton mutably (when initialized). Used by the C
/// ABI layer. The caller must not call [`TaskManager::with_manager`]
/// from a worker callback while this is held (same rule as the C++
/// `mutex_`).
pub fn with_manager_mut<R>(f: impl FnOnce(&mut TaskManager) -> R) -> Option<R> {
let mut guard = INSTANCE.lock().unwrap();
guard.as_mut().map(|p| f(unsafe { &mut *p.0 }))
}
/// Add a task, taking ownership (the manager now owns it). A worker
/// thread is spawned that runs [`Task::start`] on the task.
///
/// The worker runs through a raw pointer while the box lives in
/// `self.tasks`; the shared reference produced by `task()` ends before
/// the pointer is used, so the cast is safe in this exclusively-owned
/// setting.
#[allow(invalid_reference_casting)]
pub fn add_task(&mut self, task: Box<dyn TaskBox>) {
let ptr = task.task() as *const Task as usize as *mut Task;
self.tasks.push(task);
// The worker only touches the task through the raw pointer; it never
// reaches the manager, so the box stays alive in `self.tasks`.
let ptr_usize = ptr as usize;
let handle = std::thread::spawn(move || {
unsafe {
// The manager tracks success/failure through the task's own
// finished state; the Result is intentionally ignored.
let _ = (&mut *(ptr_usize as *mut Task)).start();
}
});
self.threads.push(Some(handle));
}
/// Cancel the task at `index`.
#[allow(invalid_reference_casting)]
pub fn cancel_task(&mut self, index: usize) -> Result<()> {
let task = self.tasks.get(index).ok_or(Error::NotFound)?;
let ptr = task.task() as *const Task as usize as *mut Task;
unsafe {
(&mut *ptr).cancel();
}
Ok(())
}
/// Cancel the task at `index` and block until it finishes.
pub fn cancel_task_and_wait(&mut self, index: usize) -> Result<()> {
self.cancel_task(index)?;
let handle = self.threads.get_mut(index).and_then(|h| h.take());
if let Some(h) = handle {
let _ = h.join();
}
Ok(())
}
/// Remove finished tasks from the list.
pub fn delete_finished(&mut self) {
for (_task, handle) in self.drain_finished() {
let _ = handle.join();
}
}
/// Number of live tasks.
pub fn get_task_count(&self) -> usize {
self.tasks.len()
}
/// Borrowed handle to the task at `index`; `Err(Error::NotFound)` if out
/// of range.
pub fn get_task_at(&self, index: usize) -> Result<CHandle> {
let task = self.tasks.get(index).ok_or(Error::NotFound)?;
let ptr = task.task() as *const Task as usize as *mut Task;
Ok(unsafe { make_borrowed::<Task>(ptr) })
}
/// Raw pointer to the task at `index` (stable while the manager owns
/// it). Used by `oaktask_manager_at` to build a borrowed task handle.
pub fn task_ptr_at(&self, index: usize) -> Result<*mut Task> {
let task = self.tasks.get(index).ok_or(Error::NotFound)?;
Ok(task.task() as *const Task as *mut Task)
}
/// Index of the task with the given address.
pub fn find_index(&self, ptr: *const Task) -> Option<usize> {
self.tasks
.iter()
.position(|t| t.task() as *const Task == ptr)
}
/// Cancel the task with the given address (no-op when absent).
pub fn cancel_task_by_ptr(&mut self, ptr: *const Task) {
if let Some(index) = self.find_index(ptr) {
let _ = self.cancel_task(index);
}
}
/// Move the worker thread of the task with the given address out of the
/// list so the caller can join it without holding the manager lock.
pub fn take_thread_by_ptr(&mut self, ptr: *const Task) -> Option<std::thread::JoinHandle<()>> {
let index = self.find_index(ptr)?;
self.threads.get_mut(index).and_then(|h| h.take())
}
/// Remove every finished task (and its worker thread) from the list,
/// returning the task boxes and thread handles so the caller can join
/// them lock-free (the boxes must stay alive until the threads exit).
pub fn drain_finished(&mut self) -> Vec<(Box<dyn TaskBox>, std::thread::JoinHandle<()>)> {
let mut out = Vec::new();
let mut i = 0;
while i < self.tasks.len() {
if self.tasks[i].task().is_finished() {
let task = self.tasks.remove(i);
let handle = self.threads.remove(i);
if let Some(handle) = handle {
out.push((task, handle));
}
} else {
i += 1;
}
}
out
}
/// Whether this manager registered the codec submitter itself.
pub fn codec_submitter_registered(&self) -> bool {
self.codec_submitter_registered
}
/// Mark whether this manager registered the codec submitter.
pub fn set_codec_submitter_registered(&mut self, registered: bool) {
self.codec_submitter_registered = registered;
}
}
impl Drop for TaskManager {
fn drop(&mut self) {
// Cancel every task, then join every worker (mirrors the C++
// destructor: cancel all first, then join all).
#[allow(invalid_reference_casting)]
for task in &self.tasks {
let ptr = task.task() as *const Task as usize as *mut Task;
unsafe {
(&mut *ptr).cancel();
}
}
for handle in self.threads.iter_mut() {
if let Some(h) = handle.take() {
let _ = h.join();
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
// 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/>.
//! `PreCacheTask`, mirroring `src/task/src/precache/precachetask.h`.
//!
//! Renders a footage node (or the whole sequence) through
//! [`crate::render::RenderTask`] to fill the playback cache. Owns a deep copy
//! of the project (`OakNodeProject`) and borrows the source footage
//! (`OakNodeFootage`).
//!
//! **Simplifications over the C++**: the deep project copy / viewer wiring
//! is not built (the render drives the given viewer directly) and the
//! timeline work-area intersection is replaced by the full footage length.
//!
//! CPP-PARITY: src/task/src/precache/precachetask.h
use crate::bridge;
use crate::error::Result;
use crate::handle::CHandle;
use crate::render::{RenderTask, RenderTaskBehavior};
use crate::task::{Task, TaskBehavior};
use oakcore_rs::{Rational, TimeRange};
/// A pre-cache task: renders frames and audio of a footage node into the
/// playback cache without any output file.
pub struct PreCacheTask {
/// The render base (itself a task).
pub render: RenderTask,
/// Owning deep copy of the project (borrowed `OakNodeProject`).
pub project: CHandle,
/// Borrowed source footage (borrowed `OakNodeFootage`).
pub footage: CHandle,
/// Frame index within the footage being cached.
pub index: i32,
/// The sequence node (borrowed `OakNodeSequence`).
pub sequence: CHandle,
}
impl PreCacheTask {
/// Create a pre-cache task for the given footage at `index` inside
/// `sequence`.
pub fn new(footage: CHandle, index: i32, sequence: CHandle) -> PreCacheTask {
// Video params from the sequence (empty when unavailable).
let mut video_params = CHandle::null();
unsafe {
bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut video_params);
}
let filename = footage_filename(footage);
let title = format!("Pre-caching {filename}:{index}");
let base = Task::new(&title, CHandle::null());
let render = RenderTask::new(base, video_params, CHandle::null(), sequence, Default::default(), None);
// A scratch project for the render color manager (simplified).
let project = unsafe { bridge::node::oaknode_project_init() };
PreCacheTask {
render,
project,
footage,
index,
sequence,
}
}
}
impl TaskBehavior for PreCacheTask {
fn run(&mut self, task: &mut Task) -> Result<()> {
// Share the caller's cancellation atom with the inner render base.
self.render.base.set_cancel_atom(task.get_cancel_atom());
// The full footage length is the cache range (simplified: no
// work-area intersection).
let mut len_num = 0i64;
let mut len_den = 1i64;
unsafe {
bridge::node::oaknode_footage_get_video_length(self.footage, &mut len_num, &mut len_den);
}
let range = TimeRange::new(Rational::new(0, 1), Rational::new(len_num, len_den));
let mut color_manager = unsafe { bridge::node::oaknode_colormanager_init(self.project) };
let mut cache = bridge::render::OakRenderCache::null();
unsafe {
bridge::node::oaknode_node_get_video_frame_cache(self.sequence, &mut cache);
}
self.render.set_render_inputs(color_manager, cache, 0 /* k_online */, false, range);
// Drive the render with `self` as the subclass behavior (the C++
// virtual dispatch receiver); the render is temporarily moved out to
// avoid a self-referential borrow and put back before the handles are
// released below.
let mut render = std::mem::replace(&mut self.render, crate::render::RenderTask::placeholder());
let result = render.render(task, self);
self.render = render;
if !cache.ctx.is_null() {
unsafe {
bridge::render::oakrender_cache_free(&mut cache);
}
}
if !color_manager.ctx.is_null() {
unsafe {
bridge::node::oaknode_colormanager_free(&mut color_manager);
}
}
result
}
}
impl RenderTaskBehavior for PreCacheTask {
fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()> {
// Do nothing: pre-cache just fills the frame cache.
let _ = (task, frame);
Ok(())
}
fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()> {
// Pre-cache doesn't cache any audio.
let _ = (task, buffer);
Ok(())
}
fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()> {
let _ = (task, text);
Ok(())
}
}
/// Two-stage read of the footage filename.
fn footage_filename(footage: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);
}
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
}
+28
View File
@@ -0,0 +1,28 @@
// 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/>.
//! Project load/save/import tasks, mirroring `src/task/src/project/*`.
//!
//! These tasks move data across the oaknode C ABI: they borrow node handles
//! while running and take ownership only on the `take_*` accessors
//! (architectural decision #5 in README.md).
pub mod format;
pub mod import;
pub mod load;
pub mod loadotio;
pub mod save;
pub mod saveotio;
+65
View File
@@ -0,0 +1,65 @@
// 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/>.
//! Interchange-format dispatch for the OTIO tasks.
//!
//! `LoadOTIOTask` / `SaveOTIOTask` accept both OpenTimelineIO JSON
//! (`.otio`) and FCPXML (`.fcpxml`) documents. The format is inferred from
//! the filename extension (case-insensitive), so the frozen C ABI
//! (`include/task/project.h`) needs no format parameter: the filename
//! passed to `oaktask_create_project_load_otio` /
//! `oaktask_create_project_save_otio` selects the codec. Format handling
//! ends at the `oakotio` parse/serialize call in each task — the
//! track/clip/footage building is shared between both formats.
//!
//! No OTIO or FCPXML type crosses the oaktask C ABI (README decision #6).
use std::path::Path;
use crate::error::{Error, Result};
/// The interchange formats the OTIO tasks can read and write.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InterchangeFormat {
/// OpenTimelineIO JSON (`.otio`).
OtioJson,
/// Final Cut Pro XML (`.fcpxml`).
Fcpxml,
}
/// The filename extensions each format dispatches on.
pub const SUPPORTED_EXTENSIONS: &str = ".otio, .fcpxml";
impl InterchangeFormat {
/// Dispatch on a filename's extension (case-insensitive).
///
/// `foo.OTIO` and `foo.FcpXml` select the same format as `foo.otio` /
/// `foo.fcpxml`; any other extension (or a filename without one) is
/// `Error::Failed` with a message naming the supported extensions.
pub fn of(filename: &str) -> Result<InterchangeFormat> {
let ext = Path::new(filename)
.extension()
.map(|e| e.to_string_lossy().into_owned())
.unwrap_or_default();
match ext.to_ascii_lowercase().as_str() {
"otio" => Ok(InterchangeFormat::OtioJson),
"fcpxml" => Ok(InterchangeFormat::Fcpxml),
_ => Err(Error::Failed(format!(
"Unknown project file format for \"{filename}\" (supported: {SUPPORTED_EXTENSIONS})"
))),
}
}
}
+546
View File
@@ -0,0 +1,546 @@
// 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/>.
//! `ProjectImportTask`, mirroring `src/task/src/project/import/import.h`.
//!
//! Imports media files into a folder of a project. Produces an undoable
//! `OakUndoCommand` (taken via `take_command()`), tracks per-file import
//! failures, and supports an optional image-sequence confirmation callback.
//!
//! CPP-PARITY: src/task/src/project/import/import.h
use std::path::Path;
use crate::bridge;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::task::{Task, TaskBehavior};
/// Callback used to confirm whether a detected image sequence should be
/// imported as a sequence rather than individual frames, mirroring
/// `ImageSequenceConfirmFn` in import.h.
pub type ImageSequenceConfirmFn = Box<dyn FnMut(&str, &str) -> bool + Send>;
/// A project-import task. Borrows the destination folder and project.
pub struct ProjectImportTask {
/// The shared task base.
pub base: Task,
/// Destination folder (borrowed `OakNodeFolder`).
pub folder: CHandle,
/// Destination project (borrowed `OakNodeProject`).
pub project: CHandle,
/// Media filenames to import.
pub filenames: Vec<String>,
/// The undo command produced by the task, taken via `take_command`.
command: Option<CHandle>,
/// Files that failed to import.
invalid_files: Vec<String>,
/// Imported footage, taken via `get_imported_footage`.
imported_footage: Vec<CHandle>,
/// Optional image-sequence confirmation callback.
image_sequence_confirm: Option<ImageSequenceConfirmFn>,
/// Total number of files to import (directories counted recursively).
file_count: usize,
/// Image-sequence files already decided not to be imported as sequences.
image_sequence_ignore_files: Vec<String>,
}
impl Drop for ProjectImportTask {
fn drop(&mut self) {
if let Some(command) = &mut self.command {
if !command.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_free(command);
}
}
}
// Borrowed footage handles: releasing them only frees the handle
// boxes (the footage nodes are owned by the project).
for footage in &self.imported_footage {
if !footage.ctx.is_null() {
if let Some(release) = footage.release {
unsafe {
release(footage.ctx);
}
}
}
}
}
}
impl ProjectImportTask {
/// Create an import task for the given folder/project and filenames.
pub fn new(
base: Task,
folder: CHandle,
project: CHandle,
filenames: Vec<String>,
image_sequence_confirm: Option<ImageSequenceConfirmFn>,
file_count: usize,
) -> ProjectImportTask {
ProjectImportTask {
base,
folder,
project,
filenames,
command: None,
invalid_files: Vec::new(),
imported_footage: Vec::new(),
image_sequence_confirm,
file_count,
image_sequence_ignore_files: Vec::new(),
}
}
/// Take ownership of the produced undo command; `Err(Error::State)` if
/// the task has not run yet.
pub fn take_command(&mut self) -> Result<CHandle> {
self.command.take().ok_or(Error::State)
}
/// Number of files that failed to import.
pub fn get_invalid_file_count(&self) -> usize {
self.invalid_files.len()
}
/// Whether any file failed to import.
pub fn has_invalid_files(&self) -> bool {
!self.invalid_files.is_empty()
}
/// Borrowed handle to the imported footage at `index` (addref'd by the C
/// ABI); `Err(Error::NotFound)` if out of range.
pub fn get_imported_footage(&self, index: usize) -> Result<CHandle> {
let footage = self.imported_footage.get(index).ok_or(Error::NotFound)?;
if !footage.ctx.is_null() {
if let Some(addref) = footage.addref {
unsafe {
addref(footage.ctx);
}
}
}
Ok(*footage)
}
/// Total number of imported footage entries.
pub fn get_file_count(&self) -> usize {
self.imported_footage.len()
}
/// Set the image-sequence confirmation callback.
pub fn set_image_sequence_confirm_callback(&mut self, cb: ImageSequenceConfirmFn) {
self.image_sequence_confirm = Some(cb);
}
/// The number of files counted at construction (progress denominator).
pub fn file_count(&self) -> usize {
self.file_count
}
/// The invalid filename at `index` (assumes the index is in range).
pub(crate) fn invalid_file_at(&self, index: usize) -> &str {
&self.invalid_files[index]
}
fn import(&mut self, task: &mut Task, folder: CHandle, entries: &mut Vec<String>, counter: &mut usize, parent_command: CHandle) {
let mut i = 0;
while i < entries.len() {
if task.is_cancelled() {
break;
}
let file_path = &entries[i];
if Path::new(file_path).is_dir() {
// Create a folder corresponding to the directory and recurse.
let entry_list: Vec<String> = match std::fs::read_dir(file_path) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.map(|e| e.path().to_string_lossy().into_owned())
.collect(),
Err(_) => Vec::new(),
};
if !entry_list.is_empty() {
let folder_handle = unsafe { bridge::node::oaknode_folder_create(self.project) };
if !folder_handle.ctx.is_null() {
unsafe {
bridge::node::oaknode_node_set_label(
bridge::node::oaknode_folder_as_node(folder_handle),
cstr(&basename_of(file_path)),
);
}
self.add_item_to_folder(folder, unsafe { bridge::node::oaknode_folder_as_node(folder_handle) }, parent_command);
let mut sub_entries = entry_list;
self.import(task, folder_handle, &mut sub_entries, counter, parent_command);
}
}
} else {
let footage = unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };
if footage.ctx.is_null() {
i += 1;
continue;
}
unsafe {
bridge::node::oaknode_footage_set_cancel_atom(footage, task.get_cancel_atom());
}
let ok = unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) } == 0;
unsafe {
bridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::null());
}
if ok && unsafe { bridge::node::oaknode_footage_is_valid(footage) } != 0 {
unsafe {
bridge::node::oaknode_node_set_label(bridge::node::oaknode_footage_as_node(footage), cstr(&basename_of(file_path)));
}
// See if this footage is an image sequence.
self.validate_image_sequence(task, footage, entries, i);
// Create the undoable command that adds the item.
self.add_item_to_folder(folder, unsafe { bridge::node::oaknode_footage_as_node(footage) }, parent_command);
self.imported_footage.push(footage);
} else {
self.invalid_files.push(file_path.clone());
// Remove the invalid footage from the graph; the remove
// command takes ownership on redo and deletes the node
// when the command is destroyed.
let mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(footage)) };
if !remove.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(remove);
bridge::undo::oakundo_command_free(&mut remove);
}
}
}
*counter += 1;
task.emit_progress(*counter as f64 / self.file_count.max(1) as f64);
}
i += 1;
}
}
fn add_item_to_folder(&self, folder: CHandle, item: CHandle, command: CHandle) {
let child = unsafe { bridge::node::oaknode_command_create_folder_add_child(folder, item) };
if !child.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_multi_add_child(command, child);
}
}
}
fn validate_image_sequence(&mut self, task: &mut Task, footage: CHandle, info_list: &mut Vec<String>, index: usize) {
let filename = footage_filename(footage);
if filename.is_empty() {
return;
}
let digit_count = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(&filename)) };
if digit_count <= 0 {
return;
}
if self.image_sequence_ignore_files.iter().any(|f| f == &filename) {
return;
}
if !self.item_is_still_image_footage_only(footage) {
return;
}
let mut video_stream = CHandle::null();
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut video_stream) } != 0 {
return;
}
let mut width = 0;
let mut height = 0;
unsafe {
bridge::common::oakcommon_videoparams_get_width(video_stream, &mut width);
bridge::common::oakcommon_videoparams_get_height(video_stream, &mut height);
}
let seq_index = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(&filename)) };
let prev_fn = transform_sequence_filename(&filename, seq_index - 1, digit_count);
let next_fn = transform_sequence_filename(&filename, seq_index + 1, digit_count);
let previous_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&prev_fn)) };
let next_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&next_fn)) };
let prev_matches = !previous_file.ctx.is_null()
&& unsafe { bridge::node::oaknode_footage_is_valid(previous_file) } != 0
&& self.compare_still_image_size(previous_file, width, height);
let next_matches = !next_file.ctx.is_null()
&& unsafe { bridge::node::oaknode_footage_is_valid(next_file) } != 0
&& self.compare_still_image_size(next_file, width, height);
if prev_matches || next_matches {
// Ask the user whether this is really a sequence (default: no).
let is_sequence = match self.image_sequence_confirm.as_mut() {
Some(cb) => cb(&filename, &filename),
None => false,
};
let start_index = get_image_sequence_limit(&filename, seq_index, false, digit_count);
let end_index = get_image_sequence_limit(&filename, seq_index, true, digit_count);
for j in start_index..=end_index {
let entry_fn = transform_sequence_filename(&filename, j, digit_count);
if is_sequence {
// Remove later entries of this sequence from the import
// list (they are imported as one footage). The C++
// `info_list.size()` is re-evaluated after each erase.
let mut k = index + 1;
while k < info_list.len() {
if info_list[k] == entry_fn {
info_list.remove(k);
break;
}
k += 1;
}
} else {
self.image_sequence_ignore_files.push(entry_fn);
}
}
if is_sequence {
unsafe {
bridge::common::oakcommon_videoparams_set_video_type(video_stream, bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE);
let mut rate_buf = [0i8; 64];
let needed = bridge::common::oakcommon_config_get(std::ptr::null(), cstr("DefaultSequenceFrameRate"), rate_buf.as_mut_ptr(), rate_buf.len() as i32);
if needed > 0 {
let rate = crate::project::load::buf_to_string(&rate_buf);
if let Some((num, den)) = parse_rational(&rate) {
if den != 0 {
bridge::common::oakcommon_videoparams_set_time_base(video_stream, num, den);
bridge::common::oakcommon_videoparams_set_frame_rate(video_stream, den, num);
}
}
}
bridge::common::oakcommon_videoparams_set_start_time(video_stream, start_index);
bridge::common::oakcommon_videoparams_set_duration(video_stream, end_index - start_index + 1);
bridge::node::oaknode_footage_set_video_params(footage, 0, &video_stream);
}
}
}
if !video_stream.ctx.is_null() {
unsafe {
bridge::common::oakcommon_videoparams_free(&mut video_stream);
}
}
// The probe footage was only created for comparison; remove it.
for probe in [previous_file, next_file] {
if !probe.ctx.is_null() {
let mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(probe)) };
if !remove.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(remove);
bridge::undo::oakundo_command_free(&mut remove);
}
}
}
}
let _ = task;
}
fn item_is_still_image_footage_only(&self, footage: CHandle) -> bool {
if unsafe { bridge::node::oaknode_footage_total_stream_count(footage) } != 1 {
return false;
}
let mut vp = CHandle::null();
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut vp) } != 0 {
return false;
}
let mut video_type = 0;
let mut valid = 0;
unsafe {
bridge::common::oakcommon_videoparams_get_video_type(vp, &mut video_type);
bridge::common::oakcommon_videoparams_get_is_valid(vp, &mut valid);
bridge::common::oakcommon_videoparams_free(&mut vp);
}
valid != 0 && video_type == bridge::common::OAKCOMMON_VIDEO_TYPE_STILL
}
fn compare_still_image_size(&self, footage: CHandle, width: i32, height: i32) -> bool {
if !self.item_is_still_image_footage_only(footage) {
return false;
}
let mut stream = CHandle::null();
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut stream) } != 0 {
return false;
}
let mut w = 0;
let mut h = 0;
unsafe {
bridge::common::oakcommon_videoparams_get_width(stream, &mut w);
bridge::common::oakcommon_videoparams_get_height(stream, &mut h);
bridge::common::oakcommon_videoparams_free(&mut stream);
}
w == width && h == height
}
}
impl TaskBehavior for ProjectImportTask {
/// Import each filename via the oaknode footage/folder C ABI
/// (`bridge::node`), collecting an undo command, imported footage, and
/// per-file failures.
fn run(&mut self, task: &mut Task) -> Result<()> {
let mut command = unsafe { bridge::undo::oakundo_command_init_multi() };
if command.ctx.is_null() {
task.set_error("Failed to create import command");
return Err(Error::Failed("Failed to create import command".to_string()));
}
self.command = Some(command);
let mut counter = 0;
let mut entries = self.filenames.clone();
self.import(task, self.folder, &mut entries, &mut counter, command);
if task.is_cancelled() {
if !command.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_free(&mut command);
}
}
self.command = None;
return Err(Error::Cancelled);
}
Ok(())
}
}
/// Count files recursively (directories recurse; anything else counts 1),
/// mirroring `count_files_recursive` in import.cpp.
fn count_files_recursive(paths: &[String]) -> usize {
paths
.iter()
.map(|p| {
let path = Path::new(p);
if path.is_dir() {
count_dir_files(path)
} else {
1
}
})
.sum()
}
fn count_dir_files(dir: &Path) -> usize {
match std::fs::read_dir(dir) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.map(|e| {
let p = e.path();
if p.is_dir() {
count_dir_files(&p)
} else {
1
}
})
.sum(),
Err(_) => 0,
}
}
/// `std::filesystem::path(filename).filename()`, as a String.
fn basename_of(path: &str) -> String {
Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
}
/// Two-stage read of the footage filename.
fn footage_filename(footage: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);
}
crate::project::load::buf_to_string(&buf)
}
/// Substitute `number` into the trailing-digit field of an image-sequence
/// filename (mirrors `oakcodec_decoder_transform_image_sequence_file_name`).
fn transform_sequence_filename(filename: &str, number: i64, digit_count: i32) -> String {
let width = digit_count.max(0) as usize;
match filename.rfind('.') {
Some(dot) => {
// The digits are the `width` chars immediately before the
// extension; they are replaced by `number`.
let digits_start = dot.saturating_sub(width);
let prefix = &filename[..digits_start];
format!("{prefix}{:0width$}{}", number, &filename[dot..], width = width)
}
None => format!("{filename}{:0width$}", number, width = width),
}
}
/// Walk the index from `start` in the given direction while the transformed
/// filename exists (mirrors `get_image_sequence_limit` in import.cpp).
fn get_image_sequence_limit(start_fn: &str, start: i64, up: bool, digit_count: i32) -> i64 {
let mut current = start;
loop {
let test_index = if up { current + 1 } else { current - 1 };
let test_filename = transform_sequence_filename(start_fn, test_index, digit_count);
if !Path::new(&test_filename).exists() {
break;
}
current = test_index;
}
current
}
/// Parse a "num/den" frame-rate string.
fn parse_rational(s: &str) -> Option<(i32, i32)> {
let mut parts = s.trim().split('/');
let num: i32 = parts.next()?.trim().parse().ok()?;
let den: i32 = parts.next()?.trim().parse().ok()?;
Some((num, den))
}
/// Convenience used by the C ABI factory to compute the title.
pub(crate) fn import_title(paths: &[String]) -> String {
let count = count_files_recursive(paths).max(1);
format!("Importing {count} file(s)")
}
/// Convenience used by the C ABI factory to compute the progress
/// denominator (mirrors the C++ `file_count_`).
pub(crate) fn import_file_count(paths: &[String]) -> usize {
count_files_recursive(paths).max(1)
}
+170
View File
@@ -0,0 +1,170 @@
// 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/>.
//! `ProjectLoadBaseTask` / `ProjectLoadTask`, mirroring
//! `src/task/src/project/load/load.h`.
//!
//! Loads an `.oakproj` file into a new `OakNodeProject`. The base task holds
//! the filename and produces the project on `take_project()`; `ProjectLoadTask`
//! is the concrete (OTIO-less) loader.
//!
//! CPP-PARITY: src/task/src/project/load/load.h
use crate::bridge;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::task::{Task, TaskBehavior};
/// Serializer result codes (`include/node/serializer.h`).
const OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;
const OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD: i32 = 1;
const OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW: i32 = 2;
const OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION: i32 = 3;
const OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;
const OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;
const OAKNODE_SERIALIZER_RESULT_NO_DATA: i32 = 7;
/// The base project-load task: owns a source filename and yields a loaded
/// project via [`ProjectLoadBaseTask::take_project`].
pub struct ProjectLoadBaseTask {
/// The shared task base.
pub base: Task,
/// Absolute project filename to load.
pub filename: String,
/// The loaded project, produced by the task and taken via `take_project`.
loaded_project: Option<CHandle>,
}
impl Drop for ProjectLoadBaseTask {
fn drop(&mut self) {
// Free the loaded project if it was never taken.
if let Some(project) = &mut self.loaded_project {
if !project.ctx.is_null() {
unsafe {
bridge::node::oaknode_project_free(project);
}
}
}
}
}
impl ProjectLoadBaseTask {
/// Create a base load task for the given filename.
pub fn new(base: Task, filename: String) -> ProjectLoadBaseTask {
ProjectLoadBaseTask {
base,
filename,
loaded_project: None,
}
}
/// Take ownership of the loaded project. Returns `Err(Error::State)` if
/// the task has not loaded a project yet.
pub fn take_project(&mut self) -> Result<CHandle> {
self.loaded_project.take().ok_or(Error::State)
}
/// Store the loaded project (called by `run`).
pub(crate) fn store_project(&mut self, project: CHandle) {
self.loaded_project = Some(project);
}
}
impl TaskBehavior for ProjectLoadBaseTask {
/// Load the project from `filename` via the oaknode serializer
/// (`bridge::node`), storing the resulting `OakNodeProject`.
fn run(&mut self, task: &mut Task) -> Result<()> {
let mut project = unsafe { bridge::node::oaknode_project_init() };
if project.ctx.is_null() {
task.set_error("Failed to create project");
return Err(Error::Failed("Failed to create project".to_string()));
}
unsafe {
bridge::node::oaknode_project_set_filename(project, cstr(&self.filename));
}
let mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;
let mut details = [0i8; 512];
let result = unsafe {
bridge::node::oaknode_serializer_load_from_file(
project,
cstr(&self.filename),
&mut code,
details.as_mut_ptr(),
details.len() as i32,
)
};
let mut success = false;
match code {
OAKNODE_SERIALIZER_RESULT_SUCCESS => success = true,
OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD => {
task.set_error("This project is from a version of Oak Video Editor that is no longer supported in this version.");
}
OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW => {
task.set_error("This project is from a newer version of Oak Video Editor and cannot be opened in this version.");
}
OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION => {
task.set_error("Failed to determine project version.");
}
OAKNODE_SERIALIZER_RESULT_FILE_ERROR => {
task.set_error(&format!("Failed to read file \"{}\" for reading.", self.filename));
}
OAKNODE_SERIALIZER_RESULT_XML_ERROR => {
task.set_error(&format!(
"Failed to read XML document. File may be corrupt. Error was: {}",
buf_to_string(&details)
));
}
OAKNODE_SERIALIZER_RESULT_NO_DATA => {
task.set_error("Failed to find any data to parse.");
}
_ => task.set_error("Unknown error."),
}
if result == 0 && success {
self.store_project(project);
return Ok(());
}
if !project.ctx.is_null() {
unsafe {
bridge::node::oaknode_project_free(&mut project);
}
}
Err(Error::Failed("Failed to load project".to_string()))
}
}
/// The concrete native project loader (no OTIO path).
pub struct ProjectLoadTask {
/// The shared task base.
pub base: ProjectLoadBaseTask,
}
impl TaskBehavior for ProjectLoadTask {
fn run(&mut self, task: &mut Task) -> Result<()> {
<ProjectLoadBaseTask as TaskBehavior>::run(&mut self.base, task)
}
}
/// Read a NUL-terminated char buffer into a String (lossy).
pub(crate) fn buf_to_string(buf: &[i8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
}
+558
View File
@@ -0,0 +1,558 @@
// 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/>.
//! `LoadOTIOTask`, mirroring `src/task/src/project/loadotio/loadotio.h`.
//!
//! Loads an OpenTimelineIO (`.otio`) or FCPXML (`.fcpxml`) file into a new
//! `OakNodeProject`, with a configurable import-confirmation callback. The
//! format is dispatched from the filename extension (see
//! [`crate::project::format`]); the document is parsed with the pure-Rust
//! `oakotio` binding (see `README` decision #6) and the project is built
//! through the oaknode / oaktimeline C ABIs exactly like the C++ task, so
//! no OTIO or FCPXML type crosses the oaktask C ABI.
//!
//! CPP-PARITY: src/task/src/project/loadotio/loadotio.cpp
use std::collections::HashMap;
use std::path::Path;
use std::sync::Mutex;
use oakotio::Serializable;
use crate::bridge;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::project::format::InterchangeFormat;
use crate::project::load::ProjectLoadBaseTask;
use crate::task::{Task, TaskBehavior};
/// Callback used to confirm whether the sequences of an OTIO document should
/// be imported, mirroring `ImportConfirmFn` in loadotio.h (receives the
/// sequence labels in order; return true to accept).
pub type ImportConfirmFn = Box<dyn FnMut(&[String]) -> bool + Send>;
/// Global import-confirmation callback, mirroring the C++ static
/// `LoadOTIOTask::confirm_callback_` (loadotio.h). When unset, everything is
/// imported (the headless default).
static CONFIRM_CALLBACK: Mutex<Option<ImportConfirmFn>> = Mutex::new(None);
/// Register (or clear, with `None`) the global import-confirmation callback.
///
/// CPP-PARITY: src/task/src/project/loadotio/loadotio.h
/// (`LoadOTIOTask::set_import_confirm_callback`)
pub fn set_import_confirm_callback(cb: Option<ImportConfirmFn>) {
*CONFIRM_CALLBACK.lock().unwrap() = cb;
}
/// An OTIO project loader.
pub struct LoadOTIOTask {
/// The base load task (owns the filename and yields the project).
pub base: ProjectLoadBaseTask,
}
impl LoadOTIOTask {
/// Create an OTIO loader for the given base load task.
pub fn new(base: ProjectLoadBaseTask) -> LoadOTIOTask {
LoadOTIOTask { base }
}
}
impl TaskBehavior for LoadOTIOTask {
/// Load and parse the document (`oakotio`), then convert it into the
/// project stored on the base task.
fn run(&mut self, task: &mut Task) -> Result<()> {
// Dispatch the interchange format from the filename extension.
// Format handling ends here: `parse_timelines` returns one
// `oakotio::Timeline` per sequence whatever the source format, so
// the project building below is shared between `.otio` and
// `.fcpxml` (C++ parity for the track/clip/footage code).
let format = match InterchangeFormat::of(&self.base.filename) {
Ok(f) => f,
Err(e) => {
if let Error::Failed(msg) = &e {
task.set_error(msg);
}
return Err(e);
}
};
let timelines = parse_timelines(task, &self.base.filename, format)?;
let mut project = unsafe { bridge::node::oaknode_project_init() };
if project.ctx.is_null() {
task.set_error("Failed to create project");
return Err(Error::Failed("Failed to create project".to_string()));
}
unsafe {
bridge::node::oaknode_project_initialize(project);
bridge::node::oaknode_project_set_modified(project, 1);
}
// Keep track of imported footage
let mut imported_footage: HashMap<String, CHandle> = HashMap::new();
// Generate a list of sequences with the same names as the timelines.
// Assumes each timeline has a unique name.
let mut unnamed_sequence_count = 0;
let mut sequences: Vec<CHandle> = Vec::new();
// Variables used for loading bar
let mut number_of_clips: f64 = 0.0;
for timeline in &timelines {
let sequence = unsafe { bridge::node::oaknode_sequence_create() };
if sequence.ctx.is_null() {
continue;
}
let label = if !timeline.name().is_empty() {
timeline.name().to_string()
} else {
// If the otio timeline does not provide a name, create a
// default one here.
unnamed_sequence_count += 1;
format!("Sequence {unnamed_sequence_count}")
};
unsafe {
bridge::node::oaknode_node_set_label(bridge::node::oaknode_sequence_as_node(sequence), cstr(&label));
}
// Set default params incase they aren't edited.
unsafe {
bridge::node::oaknode_sequence_set_default_parameters(sequence);
}
// Get number of clips for loading bar
for track in timeline.tracks().children() {
if let Some(otio_track) = track.as_track() {
number_of_clips += otio_track.children().len() as f64;
}
}
sequences.push(sequence);
}
if number_of_clips <= 0.0 {
number_of_clips = 1.0;
}
// Ask the user which sequences to import (facade callback; headless
// default accepts everything).
let sequence_names: Vec<String> = sequences
.iter()
.map(|s| node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*s) }))
.collect();
let mut confirm = CONFIRM_CALLBACK.lock().unwrap().take();
let accepted = match confirm.as_mut() {
Some(cb) => cb(&sequence_names),
None => true,
};
if !accepted {
// Cancel to indicate to caller that this task did not complete
// and to simply dispose of it. The project is never handed to the
// base task, so free it here (the C++ base-task destructor does
// the same).
task.cancel();
for sequence in &mut sequences {
unsafe {
bridge::node::oaknode_sequence_free(sequence);
}
}
if !project.ctx.is_null() {
unsafe {
bridge::node::oaknode_project_free(&mut project);
}
}
return Ok(());
}
let root_folder = unsafe { bridge::node::oaknode_project_root(project) };
let mut clips_done = 0.0f64;
for (timeline, sequence) in timelines.iter().zip(&sequences) {
let sequence_node = unsafe { bridge::node::oaknode_sequence_as_node(*sequence) };
unsafe {
bridge::node::oaknode_project_add_node(project, sequence_node);
}
let mut add_seq = unsafe { bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node) };
if !add_seq.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(add_seq);
bridge::undo::oakundo_command_free(&mut add_seq);
}
}
// Create a folder for this sequence's footage
let sequence_footage = unsafe { bridge::node::oaknode_folder_create(project) };
if !sequence_footage.ctx.is_null() {
unsafe {
bridge::node::oaknode_node_set_label(
bridge::node::oaknode_folder_as_node(sequence_footage),
cstr(timeline.name()),
);
}
let mut add_folder = unsafe {
bridge::node::oaknode_command_create_folder_add_child(
root_folder,
bridge::node::oaknode_folder_as_node(sequence_footage),
)
};
if !add_folder.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(add_folder);
bridge::undo::oakundo_command_free(&mut add_folder);
}
}
}
// Iterate through tracks
for track_composable in timeline.tracks().children() {
let Some(otio_track) = track_composable.as_track() else {
continue;
};
// Determine what kind of track it is
let track_type = match otio_track.kind() {
"Video" => bridge::node::OAKNODE_TRACK_TYPE_VIDEO,
"Audio" => bridge::node::OAKNODE_TRACK_TYPE_AUDIO,
other => {
eprintln!("Found unknown track type: {other}");
continue;
}
};
// Create a new track
let mut track_list = CHandle::null();
unsafe {
bridge::node::oaknode_sequence_get_track_list(*sequence, track_type, &mut track_list);
}
let mut add_track = unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) };
if !add_track.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(add_track);
bridge::undo::oakundo_command_free(&mut add_track);
}
}
let mut track = CHandle::null();
let mut count = 0;
unsafe {
bridge::node::oaknode_tracklist_get_track_count(track_list, &mut count);
}
if count > 0 {
unsafe {
bridge::node::oaknode_tracklist_get_track_at(track_list, count - 1, &mut track);
}
}
if track.ctx.is_null() {
continue;
}
// Get clips from track
let mut previous_block = CHandle::null();
let mut prev_block_transition = false;
for otio_block in otio_track.children() {
if task.is_cancelled() {
break;
}
let block = match otio_block.schema_name() {
"Clip" => unsafe { bridge::node::oaknode_block_clip_create() },
"Gap" => unsafe { bridge::node::oaknode_block_gap_create() },
"Transition" => {
// Todo: Look into OTIO supported transitions and add
// them to Oak.
unsafe {
bridge::node::oaknode_block_transition_create(bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE)
}
}
other => {
// We don't know what this is yet, just create a gap
// for now so that *something* is there.
eprintln!("Found unknown block type: {other}");
unsafe { bridge::node::oaknode_block_gap_create() }
}
};
if block.ctx.is_null() {
continue;
}
let block_node = unsafe { bridge::node::oaknode_block_as_node(block) };
unsafe {
bridge::node::oaknode_project_add_node(project, block_node);
bridge::node::oaknode_node_set_label(block_node, cstr(otio_block.name()));
bridge::node::oaknode_track_append_block(track, block);
}
if otio_block.schema_name() == "Clip" || otio_block.schema_name() == "Gap" {
if let Some(source_range) = otio_block.source_range() {
let start_seconds = source_range.start_time().to_seconds();
let duration_seconds = source_range.duration().to_seconds();
let start_time = oakcore_rs::Rational::from_double(start_seconds);
let duration = oakcore_rs::Rational::from_double(duration_seconds);
if otio_block.schema_name() == "Clip" {
unsafe {
bridge::node::oaknode_clip_set_media_in(
block,
start_time.numerator() as i32,
start_time.denominator() as i32,
);
}
}
unsafe {
bridge::node::oaknode_block_set_length_and_media_out(
block,
duration.numerator() as i32,
duration.denominator() as i32,
);
}
}
}
// If the previous block was a transition, connect the
// current block to it.
if prev_block_transition {
unsafe {
bridge::node::oaknode_node_connect(
block_node,
bridge::node::oaknode_block_as_node(previous_block),
cstr(bridge::node::OAKNODE_TRANSITION_IN_BLOCK_INPUT),
);
}
prev_block_transition = false;
}
if let Some(otio_transition) = otio_block.as_transition() {
// Set how far the transition eats into the previous
// clip.
let in_offset = otio_transition.in_offset().to_rational();
let out_offset = otio_transition.out_offset().to_rational();
unsafe {
bridge::node::oaknode_transition_set_offsets_and_length(
block,
in_offset.numerator() as i32,
in_offset.denominator() as i32,
out_offset.numerator() as i32,
out_offset.denominator() as i32,
);
}
if !previous_block.ctx.is_null() {
unsafe {
bridge::node::oaknode_node_connect(
bridge::node::oaknode_block_as_node(previous_block),
block_node,
cstr(bridge::node::OAKNODE_TRANSITION_OUT_BLOCK_INPUT),
);
}
}
prev_block_transition = true;
// Position transition in its own context.
unsafe {
set_own_context_position(block_node);
}
}
if otio_block.schema_name() == "Gap" {
// Position gap in its own context.
unsafe {
set_own_context_position(block_node);
}
}
// Update this after it's used but before any continue
// statements.
previous_block = block;
if otio_block.schema_name() == "Clip" {
let Some(otio_clip) = otio_block.as_clip() else {
continue;
};
let Some(media_reference) = otio_clip.media_reference() else {
continue;
};
if let Some(external) = media_reference.as_external_reference() {
// Link footage
let footage_url = external.target_url().to_string();
let probed_item = if let Some(existing) = imported_footage.get(&footage_url) {
*existing
} else {
let created = unsafe { bridge::node::oaknode_footage_create(project, cstr(&footage_url)) };
if !created.ctx.is_null() {
imported_footage.insert(footage_url.clone(), created);
let label = Path::new(&footage_url)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
unsafe {
bridge::node::oaknode_node_set_label(
bridge::node::oaknode_footage_as_node(created),
cstr(&label),
);
}
if !sequence_footage.ctx.is_null() {
let mut add_footage = unsafe {
bridge::node::oaknode_command_create_folder_add_child(
sequence_footage,
bridge::node::oaknode_footage_as_node(created),
)
};
if !add_footage.ctx.is_null() {
unsafe {
bridge::undo::oakundo_command_redo_now(add_footage);
bridge::undo::oakundo_command_free(&mut add_footage);
}
}
}
}
created
};
if !probed_item.ctx.is_null() {
unsafe {
// Position clip in its own context.
set_own_context_position(block_node);
// Position footage in its context.
bridge::node::oaknode_node_set_context_position(
block_node,
bridge::node::oaknode_footage_as_node(probed_item),
-2.0,
0.0,
0,
);
}
if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {
let transform = unsafe {
bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_TRANSFORM))
};
if !transform.ctx.is_null() {
unsafe {
bridge::node::oaknode_project_add_node(project, transform);
bridge::node::oaknode_node_connect(
bridge::node::oaknode_footage_as_node(probed_item),
transform,
cstr("tex_in"),
);
bridge::node::oaknode_node_connect(transform, block_node, cstr("buffer_in"));
bridge::node::oaknode_node_set_context_position(block_node, transform, -1.0, 0.0, 0);
}
}
} else {
let volume =
unsafe { bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_VOLUME)) };
if !volume.ctx.is_null() {
unsafe {
bridge::node::oaknode_project_add_node(project, volume);
bridge::node::oaknode_node_connect(
bridge::node::oaknode_footage_as_node(probed_item),
volume,
cstr("samples_in"),
);
bridge::node::oaknode_node_connect(volume, block_node, cstr("buffer_in"));
bridge::node::oaknode_node_set_context_position(block_node, volume, -1.0, 0.0, 0);
}
}
}
}
}
}
clips_done += 1.0;
task.emit_progress(clips_done / number_of_clips);
}
}
}
self.base.store_project(project);
Ok(())
}
}
/// Set the node's position in its own context (the C++
/// `set_own_context_position` helper in loadotio.cpp).
unsafe fn set_own_context_position(node: CHandle) {
unsafe {
bridge::node::oaknode_node_set_context_position(node, node, 0.0, 0.0, 0);
}
}
/// Parse the document into one `oakotio::Timeline` per sequence, dispatching
/// on the filename extension:
///
/// - `.otio`: OpenTimelineIO JSON. A `Timeline` root yields one timeline, a
/// `SerializableCollection` yields one timeline per `Timeline` child, and
/// any other root is the C++ "Unknown OpenTimelineIO root element" error.
/// - `.fcpxml`: FCPXML (`oakotio::fcpxml`), one timeline per `<sequence>`.
///
/// Format handling ends here — the caller builds the project from the
/// returned timelines regardless of the source format (C++ parity), so the
/// track/clip/footage code never forks.
fn parse_timelines(task: &mut Task, filename: &str, format: InterchangeFormat) -> Result<Vec<oakotio::Timeline>> {
match format {
InterchangeFormat::OtioJson => {
let root = oakotio::from_json_file(filename).map_err(|e| {
task.set_error(&format!(
"Failed to load OpenTimelineIO from file \"{}\" \n\nOpenTimelineIO Error:\n\n{}",
filename, e
));
Error::Failed("Failed to load OpenTimelineIO".to_string())
})?;
match &root {
Serializable::SerializableCollection(collection) => Ok(collection
.children()
.iter()
.filter_map(|child| child.as_timeline().cloned())
.collect()),
Serializable::Timeline(timeline) => Ok(vec![timeline.clone()]),
_ => {
task.set_error("Unknown OpenTimelineIO root element");
Err(Error::Failed("Unknown OpenTimelineIO root element".to_string()))
}
}
}
InterchangeFormat::Fcpxml => oakotio::from_fcpxml_file(filename).map_err(|e| {
task.set_error(&format!(
"Failed to load FCPXML from file \"{}\" \n\nFCPXML Error:\n\n{}",
filename, e
));
Error::Failed("Failed to load FCPXML".to_string())
}),
}
}
/// Two-stage read of a node's label (the C++ `oaknode_node_get_label` usage).
fn node_label_of(node: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);
}
crate::project::load::buf_to_string(&buf)
}
+124
View File
@@ -0,0 +1,124 @@
// 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/>.
//! `ProjectSaveTask`, mirroring `src/task/src/project/save/save.h`.
//!
//! Serializes a borrowed `OakNodeProject` to an `.oakproj` file via the
//! oaknode serializer. Optionally writes to an override filename and/or uses
//! compression.
//!
//! CPP-PARITY: src/task/src/project/save/save.h
use crate::bridge;
use crate::error::{Error, Result};
use crate::ffi::taskhandle::cstr;
use crate::handle::CHandle;
use crate::project::load::buf_to_string;
use crate::task::{Task, TaskBehavior};
/// Serializer result codes (`include/node/serializer.h`).
const OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;
const OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;
const OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;
const OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR: i32 = 6;
/// A project-save task. Borrows its project and never takes ownership.
pub struct ProjectSaveTask {
/// The shared task base.
pub base: Task,
/// Borrowed project to save (borrowed `OakNodeProject`).
pub project: CHandle,
/// Optional override filename; when empty the project's own filename is
/// used.
pub override_filename: Option<String>,
/// Whether to compress the serialized output.
pub use_compression: bool,
}
impl ProjectSaveTask {
/// Set the output filename override.
pub fn set_override_filename(&mut self, filename: &str) {
self.override_filename = Some(filename.to_string());
}
}
impl TaskBehavior for ProjectSaveTask {
/// Serialize the project via the oaknode serializer (`bridge::node`) to
/// the resolved filename.
fn run(&mut self, task: &mut Task) -> Result<()> {
let using_filename = match &self.override_filename {
Some(name) => name.clone(),
None => project_filename(self.project),
};
if using_filename.is_empty() {
task.set_error("Project has no filename to save to.");
return Err(Error::Failed("Project has no filename to save to.".to_string()));
}
let mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;
let mut details = [0i8; 512];
unsafe {
bridge::node::oaknode_serializer_save_to_file(
self.project,
cstr(&using_filename),
if self.use_compression { 1 } else { 0 },
&mut code,
details.as_mut_ptr(),
details.len() as i32,
);
}
let mut success = false;
match code {
OAKNODE_SERIALIZER_RESULT_SUCCESS => success = true,
OAKNODE_SERIALIZER_RESULT_XML_ERROR => {
task.set_error("Failed to write XML data.");
}
OAKNODE_SERIALIZER_RESULT_FILE_ERROR => {
task.set_error(&format!("Failed to open file for writing: {}", buf_to_string(&details)));
}
OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR => {
task.set_error(&format!(
"Failed to overwrite \"{}\". Project has been saved as \"{}\" instead.",
using_filename,
buf_to_string(&details)
));
success = true;
}
_ => task.set_error("Unknown error."),
}
if success {
Ok(())
} else {
Err(Error::Failed("Failed to save project".to_string()))
}
}
}
/// Two-stage read of the project's own filename (empty when unset).
pub(crate) fn project_filename(project: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_project_filename(project, buf.as_mut_ptr(), needed);
}
buf_to_string(&buf)
}
+498
View File
@@ -0,0 +1,498 @@
// 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/>.
//! `SaveOTIOTask`, mirroring `src/task/src/project/saveotio/saveotio.h`.
//!
//! Serializes a borrowed `OakNodeProject` to an OpenTimelineIO (`.otio`) or
//! FCPXML (`.fcpxml`) file through the pure-Rust `oakotio` binding (see
//! `README` decision #6): the project's sequences become `OTIO::Timeline`s
//! (`serialize_timeline` / `serialize_track` / `serialize_track_list`),
//! exactly like the C++ `serialize_*` helpers. The format is dispatched
//! from the filename extension (see [`crate::project::format`]) at the
//! final write only — the serialization itself is shared. No OTIO or
//! FCPXML type crosses the oaktask C ABI.
//!
//! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp
use oakcore_rs::Rational;
use oakotio::{
Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, SerializableCollection, TimeRange,
Timeline, Track, Transition,
};
use crate::bridge;
use crate::error::{Error, Result};
use crate::handle::CHandle;
use crate::project::format::InterchangeFormat;
use crate::project::load::buf_to_string;
use crate::task::{Task, TaskBehavior};
/// `Rational(INT_MIN)` — the initial "no track yet" maximum length
/// (`RATIONAL_MIN` in saveotio.cpp).
fn rational_min() -> Rational {
Rational::new(-2147483647, 1)
}
/// An OTIO project-save task. Borrows its project.
pub struct SaveOTIOTask {
/// The shared task base.
pub base: Task,
/// Borrowed project to save (borrowed `OakNodeProject`).
pub project: CHandle,
/// Output OTIO filename.
pub filename: String,
}
impl SaveOTIOTask {
/// Serialize one sequence into an `OTIO::Timeline`; `None` when the
/// sequence has no usable frame rate or a track fails to serialize.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline)
fn serialize_timeline(sequence: CHandle) -> Option<Timeline> {
let mut otio_timeline = Timeline::new(node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(sequence) }));
let mut rate = 0.0f64;
{
let mut num = 0;
let mut den = 1;
let mut vp = CHandle::null();
if unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, &mut vp) } == 0 {
unsafe {
bridge::common::oakcommon_videoparams_get_frame_rate(vp, &mut num, &mut den);
}
if den != 0 {
rate = num as f64 / den as f64;
}
if !vp.ctx.is_null() {
unsafe {
bridge::common::oakcommon_videoparams_free(&mut vp);
}
}
}
}
if rate.is_nan() || rate <= 0.0 {
return None;
}
let mut video_list = CHandle::null();
let mut audio_list = CHandle::null();
unsafe {
bridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_VIDEO, &mut video_list);
bridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_AUDIO, &mut audio_list);
}
if !Self::serialize_track_list(video_list, &mut otio_timeline, rate)
|| !Self::serialize_track_list(audio_list, &mut otio_timeline, rate)
{
return None;
}
Some(otio_timeline)
}
/// Serialize all tracks of one track list (video or audio) into the
/// timeline. Each track is padded to the list's maximum track length.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)
fn serialize_track_list(list: CHandle, otio_timeline: &mut Timeline, sequence_rate: f64) -> bool {
if list.ctx.is_null() {
return true;
}
let mut max_track_length = rational_min();
let mut track_count = 0;
unsafe {
bridge::node::oaknode_tracklist_get_track_count(list, &mut track_count);
}
for i in 0..track_count {
let mut track = CHandle::null();
unsafe {
bridge::node::oaknode_tracklist_get_track_at(list, i, &mut track);
}
if !track.ctx.is_null() {
let length = track_length_of(track);
if length > max_track_length {
max_track_length = length;
}
}
}
for i in 0..track_count {
let mut track = CHandle::null();
unsafe {
bridge::node::oaknode_tracklist_get_track_at(list, i, &mut track);
}
if track.ctx.is_null() {
continue;
}
let Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length) else {
return false;
};
otio_timeline.tracks_mut().append_child(Composable::Track(otio_track));
}
true
}
/// Serialize one track: a `Track` whose kind matches the native track
/// type, one OTIO block per native block, plus a trailing `Gap` when the
/// track is shorter than `max_track_length`.
///
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)
fn serialize_track(track: CHandle, sequence_rate: f64, max_track_length: Rational) -> Option<Track> {
let mut track_type = bridge::node::OAKNODE_TRACK_TYPE_NONE;
unsafe {
bridge::node::oaknode_track_get_type(track, &mut track_type);
}
let kind = match track_type {
bridge::node::OAKNODE_TRACK_TYPE_VIDEO => "Video",
bridge::node::OAKNODE_TRACK_TYPE_AUDIO => "Audio",
other => {
eprintln!("Don't know OTIO track kind for native type {other}");
return None;
}
};
let mut otio_track = Track::new(kind);
let mut block_count = 0;
unsafe {
bridge::node::oaknode_track_get_block_count(track, &mut block_count);
}
for i in 0..block_count {
let mut block = CHandle::null();
unsafe {
bridge::node::oaknode_track_get_block_at(track, i, &mut block);
}
if block.ctx.is_null() {
continue;
}
let mut kind = bridge::node::OAKNODE_BLOCK_OTHER;
unsafe {
bridge::node::oaknode_block_get_kind(block, &mut kind);
}
let otio_block: Option<Composable> = match kind {
bridge::node::OAKNODE_BLOCK_CLIP => {
let mut otio_clip = Clip::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));
otio_clip.set_source_range(TimeRange::new(
RationalTime::from_rational(block_in_of(block), sequence_rate),
RationalTime::from_rational(block_length_of(block), sequence_rate),
));
let mut media = CHandle::null();
unsafe {
bridge::node::oaknode_node_find_input_footage(bridge::node::oaknode_block_as_node(block), &mut media);
}
if !media.ctx.is_null() {
let available_range = if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {
// OTIO ExternalReference uses the source clips
// frame rate (or sample rate) as opposed to the
// sequences rate.
let mut source_frame_rate = 0.0f64;
let mut duration = 0.0f64;
let mut num = 0;
let mut den = 1;
let mut vp = CHandle::null();
if unsafe { bridge::node::oaknode_footage_get_video_params(media, 0, &mut vp) } == 0 {
unsafe {
bridge::common::oakcommon_videoparams_get_frame_rate(vp, &mut num, &mut den);
}
if den != 0 {
source_frame_rate = num as f64 / den as f64;
}
let mut dur = 0i64;
unsafe {
bridge::common::oakcommon_videoparams_get_duration(vp, &mut dur);
}
duration = dur as f64;
if !vp.ctx.is_null() {
unsafe {
bridge::common::oakcommon_videoparams_free(&mut vp);
}
}
}
TimeRange::new(
RationalTime::new(0.0, source_frame_rate),
RationalTime::new(duration, source_frame_rate),
)
} else {
TimeRange::new(RationalTime::new(0.0, 48000.0), RationalTime::new(0.0, 48000.0))
};
let media_url = footage_filename(media);
if !media_url.is_empty() {
otio_clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(
media_url,
Some(available_range),
)));
}
}
Some(Composable::Clip(otio_clip))
}
bridge::node::OAKNODE_BLOCK_GAP => Some(Composable::Gap(Gap::new(
TimeRange::new(
RationalTime::from_rational(block_in_of(block), 24.0),
RationalTime::from_rational(block_length_of(block), 24.0),
),
node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }),
))),
bridge::node::OAKNODE_BLOCK_TRANSITION => {
let mut otio_transition = Transition::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));
let (n, d) = transition_offset_of(block, true);
otio_transition.set_in_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));
let (n, d) = transition_offset_of(block, false);
otio_transition.set_out_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));
Some(Composable::Transition(otio_transition))
}
_ => None,
};
let Some(otio_block) = otio_block else {
// We shouldn't ever get here, but catch without crashing if
// we ever do.
return None;
};
otio_track.append_child(otio_block);
}
// All OTIO tracks must have the same duration so we add a Gap to
// fill the remaining time.
let duration = track_duration(&otio_track);
let duration_seconds = duration.clone().to_seconds();
if duration_seconds < max_track_length.to_f64() {
let time_left = max_track_length.to_f64() - duration_seconds;
let gap = Gap::new(TimeRange::new(duration, RationalTime::new(time_left, 1.0)), "");
otio_track.append_child(Composable::Gap(gap));
}
Some(otio_track)
}
}
impl TaskBehavior for SaveOTIOTask {
/// Serialize the project to OTIO/FCPXML via `oakotio` and write the file.
fn run(&mut self, task: &mut Task) -> Result<()> {
// Dispatch the interchange format from the filename extension.
// Format handling ends at the write below — the serialization
// produces `oakotio::Timeline`s and is shared between `.otio` and
// `.fcpxml` (C++ parity for the serialize_* helpers).
let format = match InterchangeFormat::of(&self.filename) {
Ok(f) => f,
Err(e) => {
if let Error::Failed(msg) = &e {
task.set_error(msg);
}
return Err(e);
}
};
// Collect sequences from the root folder (non-recursive, matching
// the original list_children_of_type behavior closely enough for
// OTIO).
let root = unsafe { bridge::node::oaknode_project_root(self.project) };
if root.ctx.is_null() {
task.set_error("Project contains no sequences to export.");
return Err(Error::Failed("Project contains no sequences to export.".to_string()));
}
let mut sequences: Vec<CHandle> = Vec::new();
let child_count = unsafe { bridge::node::oaknode_folder_child_count(root) };
for i in 0..child_count {
let child = unsafe { bridge::node::oaknode_folder_child_at(root, i) };
if child.ctx.is_null() {
continue;
}
if node_id_of(child) == bridge::node::OAKNODE_TYPE_SEQUENCE {
// Borrowed sequence alias of the child node handle (all
// oaknode handles share the same box layout).
sequences.push(child);
}
}
if sequences.is_empty() {
task.set_error("Project contains no sequences to export.");
return Err(Error::Failed("Project contains no sequences to export.".to_string()));
}
let mut serialized: Vec<Timeline> = Vec::with_capacity(sequences.len());
for sequence in &sequences {
match Self::serialize_timeline(*sequence) {
Some(timeline) => serialized.push(timeline),
None => {
task.set_error(&format!(
"Failed to serialize sequence \"{}\"",
node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*sequence) })
));
return Err(Error::Failed("Failed to serialize sequence".to_string()));
}
}
}
// Write the serialized timelines, dispatching on the format: OTIO
// JSON writes a `Timeline` root for one sequence or a
// `SerializableCollection` for several; FCPXML writes all timelines
// into one document (one `<project>` per timeline).
let result: std::result::Result<(), String> = match format {
InterchangeFormat::OtioJson => {
if serialized.len() == 1 {
serialized[0].to_json_file(&self.filename).map_err(|e| e.to_string())
} else {
// Serialize all into a SerializableCollection.
let collection = SerializableCollection::new(
"Sequences",
serialized.into_iter().map(Serializable::Timeline).collect(),
);
Serializable::SerializableCollection(collection)
.to_json_file(&self.filename)
.map_err(|e| e.to_string())
}
}
InterchangeFormat::Fcpxml => oakotio::to_fcpxml_file(&serialized, &self.filename).map_err(|e| e.to_string()),
};
result.map_err(|e| {
let (label, what) = match format {
InterchangeFormat::OtioJson => ("Failed to save OpenTimelineIO to file", "Failed to save OpenTimelineIO"),
InterchangeFormat::Fcpxml => ("Failed to save FCPXML to file", "Failed to save FCPXML"),
};
task.set_error(&format!("{label} \"{}\": {e}", self.filename));
Error::Failed(what.to_string())
})
}
}
/// Two-stage read of a node's label.
fn node_label_of(node: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);
}
buf_to_string(&buf)
}
/// Two-stage read of a node's type id.
fn node_id_of(node: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_node_get_id(node, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_node_get_id(node, buf.as_mut_ptr(), needed);
}
buf_to_string(&buf)
}
/// The block's in point as a `Rational` (default 0/1 when unset).
fn block_in_of(block: CHandle) -> Rational {
let mut n = 0;
let mut d = 1;
unsafe {
bridge::node::oaknode_block_get_in(block, &mut n, &mut d);
}
Rational::new(n as i64, d as i64)
}
/// The block's length as a `Rational` (default 0/1 when unset).
fn block_length_of(block: CHandle) -> Rational {
let mut n = 0;
let mut d = 1;
unsafe {
bridge::node::oaknode_block_get_length(block, &mut n, &mut d);
}
Rational::new(n as i64, d as i64)
}
/// The track's total length as a `Rational` (default 0/1 when unset).
fn track_length_of(track: CHandle) -> Rational {
let mut n = 0;
let mut d = 1;
unsafe {
bridge::node::oaknode_track_get_length(track, &mut n, &mut d);
}
Rational::new(n as i64, d as i64)
}
/// A transition's in (`in_offset == true`) or out offset as a `Rational`.
fn transition_offset_of(block: CHandle, in_offset: bool) -> (i32, i32) {
let mut n = 0;
let mut d = 1;
if in_offset {
unsafe {
bridge::node::oaknode_transition_get_in_offset(block, &mut n, &mut d);
}
} else {
unsafe {
bridge::node::oaknode_transition_get_out_offset(block, &mut n, &mut d);
}
}
(n, d)
}
/// Two-stage read of a footage's filename (empty when unset).
fn footage_filename(footage: CHandle) -> String {
let needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };
if needed <= 0 {
return String::new();
}
let mut buf = vec![0i8; needed as usize];
unsafe {
bridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);
}
buf_to_string(&buf)
}
/// The serialized track's duration as an `OTIO::RationalTime` — the sum of
/// its children's source-range durations, keeping the first child's rate
/// (the C++ `otio_track->duration(&es)` result for contiguous children).
fn track_duration(track: &Track) -> RationalTime {
let mut acc: Option<RationalTime> = None;
for child in track.children() {
let duration = match child.as_clip().and_then(|c| c.source_range()).map(|r| r.duration())
.or_else(|| child.as_gap().and_then(|g| g.source_range()).map(|r| r.duration()))
{
Some(d) => d,
// Transitions occupy no time.
None => RationalTime::new(0.0, 1.0),
};
match acc {
None => acc = Some(duration),
Some(a) => {
let rate = a.rate();
let value = a.value() + duration.rescaled_to(rate).value();
acc = Some(RationalTime::new(value, rate));
}
}
}
acc.unwrap_or_else(RationalTime::invalid_time)
}
+330
View File
@@ -0,0 +1,330 @@
// 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/>.
//! `ProxyTask`, mirroring `src/task/src/proxy/proxy.h`.
//!
//! Transcodes a video stream to a proxy via an external `ffmpeg` process
//! (oakcodec kind `OAKCODEC_TASK_PROXY`). The two pure functions
//! [`ProxyTask::build_arguments`] and [`ProxyTask::parse_progress`] are
//! parity/golden candidates (see tests/parity_test.rs).
//!
//! CPP-PARITY: src/task/src/proxy/proxy.h
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use crate::bridge;
use crate::error::{Error, Result};
use crate::handle::CHandle;
use crate::task::{Task, TaskBehavior};
/// The proxy parameters, mirroring `oakcodec_proxy_params` in
/// `include/codec/proxy.h` (kept as plain fields so no oakcodec handle is
/// needed to build arguments).
pub struct ProxyParams {
/// Target width (0 = unspecified/divider-based).
pub width: i32,
/// Target height (0 = unspecified/divider-based).
pub height: i32,
/// Resolution divider.
pub divider: i32,
/// Proxy format version.
pub version: i32,
/// CRF for the proxy encode.
pub crf: i32,
/// Whether audio is included in the proxy.
pub include_audio: bool,
/// Output container extension.
pub extension: String,
/// ffmpeg preset name.
pub preset: String,
}
/// A proxy transcode task: runs `ffmpeg` with arguments from
/// [`ProxyTask::build_arguments`], parsing progress lines via
/// [`ProxyTask::parse_progress`].
pub struct ProxyTask {
/// The shared task base.
pub base: Task,
/// Source media filename.
pub source_filename: String,
/// Target stream index.
pub stream_index: i32,
/// Proxy parameters.
pub params: ProxyParams,
/// Output proxy filename.
pub output_filename: String,
/// Total media duration in seconds (for progress scaling).
duration_seconds: f64,
}
impl ProxyTask {
/// Build a proxy task from an oakcodec request and proxy params,
/// mirroring the C++ constructor (divider-based requests take the source
/// fraction).
pub fn new(request: &bridge::codec::OakCodecTaskRequest, params: ProxyParams) -> ProxyTask {
let source = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };
let output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };
let mut params = params;
if request.proxy_width > 0 && request.proxy_height > 0 {
params.width = request.proxy_width;
params.height = request.proxy_height;
params.divider = 1;
}
let title = format!("Generating Proxy {}:{}", source, request.stream_index);
ProxyTask {
base: Task::new(&title, CHandle::null()),
source_filename: source,
stream_index: request.stream_index,
params,
output_filename: output,
duration_seconds: 0.0,
}
}
/// Build the `ffmpeg` command-line argument vector for the proxy
/// transcode. Mirrors `build_arguments` in proxy.cpp; the exact ordering
/// and flag spelling are parity-locked (golden test).
///
/// CPP-PARITY: src/task/src/proxy/proxy.cpp (build_arguments)
pub fn build_arguments(
source_filename: &str,
stream_index: i32,
params: &ProxyParams,
output_filename: &str,
) -> Vec<String> {
let scale_filter = if params.divider > 1 {
format!(
"scale=w=trunc(iw/{}/2)*2:h=trunc(ih/{}/2)*2",
params.divider, params.divider
)
} else {
format!(
"scale=w={}:h={}:force_original_aspect_ratio=decrease",
params.width, params.height
)
};
let container_format = if params.extension.is_empty() {
"mp4".to_string()
} else {
params.extension.clone()
};
let mut args: Vec<String> = vec![
"-y".to_string(),
"-nostats".to_string(),
"-progress".to_string(),
"pipe:1".to_string(),
"-i".to_string(),
source_filename.to_string(),
"-map".to_string(),
format!("0:{stream_index}"),
];
if params.include_audio {
args.extend([
"-map".to_string(),
"0:a?".to_string(),
"-c:a".to_string(),
"aac".to_string(),
"-b:a".to_string(),
"128k".to_string(),
]);
} else {
args.push("-an".to_string());
}
args.extend([
"-vf".to_string(),
scale_filter,
"-c:v".to_string(),
"libx264".to_string(),
"-preset".to_string(),
params.preset.clone(),
"-crf".to_string(),
params.crf.to_string(),
"-pix_fmt".to_string(),
"yuv420p".to_string(),
"-movflags".to_string(),
"+faststart".to_string(),
"-f".to_string(),
container_format,
output_filename.to_string(),
]);
args
}
/// Parse a single `ffmpeg -progress` output line into a progress value in
/// 0.0..=1.0 given the total duration. Mirrors `parse_progress` in
/// proxy.cpp (golden test).
///
/// CPP-PARITY: src/task/src/proxy/proxy.cpp (parse_progress)
pub fn parse_progress(line: &str, duration_seconds: f64) -> Option<f64> {
if duration_seconds <= 0.0 {
return None;
}
let out_time_us: i64 = if let Some(rest) = line.strip_prefix("out_time_us=") {
rest.trim().parse().unwrap_or(-1)
} else if let Some(rest) = line.strip_prefix("out_time_ms=") {
// Despite the name, ffmpeg reports this value in microseconds.
rest.trim().parse().unwrap_or(-1)
} else {
-1
};
if out_time_us < 0 {
return None;
}
let progress = out_time_us as f64 / 1_000_000.0 / duration_seconds;
Some(if progress < 0.0 {
0.0
} else if progress > 1.0 {
1.0
} else {
progress
})
}
}
impl TaskBehavior for ProxyTask {
/// Spawn `ffmpeg` with the built arguments, feed `-progress` lines to
/// [`ProxyTask::parse_progress`] and emit them as task progress.
fn run(&mut self, task: &mut Task) -> Result<()> {
let mut ffmpeg_buf = [0i8; 1024];
let found = unsafe {
bridge::codec::oakcodec_proxy_find_ffmpeg(std::ptr::null(), ffmpeg_buf.as_mut_ptr(), ffmpeg_buf.len() as i32)
};
if found <= 0 {
task.set_error(
"Failed to generate proxy: ffmpeg executable was not found. Set the ffmpeg path in Preferences > Disk > Proxy Settings.",
);
return Err(Error::Failed("ffmpeg executable was not found".to_string()));
}
let ffmpeg_path = buf_to_string(&ffmpeg_buf);
// Create the output directory if needed.
if let Some(parent) = std::path::Path::new(&self.output_filename).parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
if std::fs::create_dir_all(parent).is_err() {
task.set_error("Failed to create proxy output directory");
return Err(Error::Failed("Failed to create proxy output directory".to_string()));
}
}
}
let _ = std::fs::remove_file(&self.output_filename);
let working_filename = format!("{}.working.mp4", self.output_filename);
let _ = std::fs::remove_file(&working_filename);
let args = Self::build_arguments(&self.source_filename, self.stream_index, &self.params, &working_filename);
// Probe the source duration for progress scaling (0 when unknown).
self.duration_seconds = probe_source_duration_seconds(&ffmpeg_path, &self.source_filename);
let mut command = Command::new(&ffmpeg_path);
command.args(&args).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = match command.spawn() {
Ok(child) => child,
Err(_) => {
task.set_error("Failed to start ffmpeg for proxy generation");
return Err(Error::Failed("Failed to start ffmpeg".to_string()));
}
};
// Drain stderr in a thread so a chatty ffmpeg cannot block us.
let stderr = child.stderr.take();
if let Some(stderr) = stderr {
std::thread::spawn(move || {
let _ = BufReader::new(stderr).lines().count();
});
}
let stdout = child.stdout.take();
let mut last_progress = 0.0_f64;
if let Some(stdout) = stdout {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(line) => {
if let Some(progress) = Self::parse_progress(&line, self.duration_seconds) {
if progress >= 0.0 && progress - last_progress > 0.001 {
last_progress = progress;
task.emit_progress(progress);
}
}
}
Err(_) => break,
}
if task.is_cancelled() {
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(&working_filename);
task.set_error("Proxy generation was cancelled");
return Err(Error::Cancelled);
}
}
}
let exit_status = child.wait();
if !matches!(exit_status, Ok(status) if status.success()) {
let _ = std::fs::remove_file(&working_filename);
task.set_error("ffmpeg failed to generate proxy");
return Err(Error::Failed("ffmpeg failed to generate proxy".to_string()));
}
if !std::path::Path::new(&working_filename).exists() {
task.set_error("ffmpeg finished but proxy file was not created");
return Err(Error::Failed("ffmpeg finished but proxy file was not created".to_string()));
}
if std::fs::rename(&working_filename, &self.output_filename).is_err() {
task.set_error("Failed to move proxy into place");
return Err(Error::Failed("Failed to move proxy into place".to_string()));
}
task.emit_progress(1.0);
Ok(())
}
}
/// Read a NUL-terminated C char buffer into a String.
fn buf_to_string(buf: &[i8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
unsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }
}
/// Probe the source duration via `ffprobe` next to ffmpeg; 0.0 when
/// unavailable (in which case no intermediate progress is reported).
fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f64 {
let ffprobe = std::path::Path::new(ffmpeg_path).with_file_name("ffprobe");
if !ffprobe.exists() {
return 0.0;
}
let output = Command::new(&ffprobe)
.args(["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", source_filename])
.output();
match output {
Ok(out) if out.status.success() => {
let text = String::from_utf8_lossy(&out.stdout);
text.trim().parse().unwrap_or(0.0)
}
_ => 0.0,
}
}
+720
View File
@@ -0,0 +1,720 @@
// 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/>.
//! `RenderTask` abstract base, mirroring `src/task/src/render/render.h`.
//!
//! The abstract parent of [`crate::export::ExportTask`] and
//! [`crate::precache::PreCacheTask`]. It renders frames/audio through
//! oakrender tickets (`bridge::render`) and hands each result to a virtual
//! hook (frame/audio downloaded, subtitle encoded) that the subclass
//! implements via the [`RenderTaskBehavior`] trait.
//!
//! ## Concurrent render loop
//!
//! Like the C++ original (`render.cpp`), the loop keeps up to
//! `max_inflight` tickets running at once (default
//! `std::thread::available_parallelism()`, matching the C++
//! `std::max(1, hardware_concurrency)`). Tickets report completion through
//! the `oakrender_ticket_finished_fn` callback (fired on the ticket's own
//! finishing thread), which pushes the finished ticket into a shared queue
//! and wakes the render thread (queue + condvar, exactly the C++ design).
//! Tickets may complete out of order; a **reorder buffer** delivers the
//! results to the hooks in timestamp order — the audio range first (the C++
//! queue-audio-first order), then every frame in ascending time — so the
//! observable per-frame contract (`frame_downloaded`/`audio_downloaded` in
//! order, progress updates, cancellation between frames) is unchanged.
//!
//! The ticket's finished callback hands the loop a **borrowed copy** of the
//! submitter's handle (`include/render/ticket.h`: "the submitter keeps
//! ownership and releases it"). The loop therefore releases exactly the
//! handle value each submit function returns, once, and never releases the
//! queue's borrowed copies. Cancellation (or a hook error) cancels and
//! waits every in-flight ticket before returning, so every submitted
//! ticket's completion still fires exactly once.
//!
//! CPP-PARITY: src/task/src/render/render.h
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Condvar, Mutex};
use crate::bridge;
use crate::bridge::common::OakColorTransform;
use crate::bridge::node::{cstr, OAKNODE_SEQUENCE_SAMPLES_INPUT, OAKNODE_SEQUENCE_TEXTURE_INPUT};
use crate::bridge::render::{OakRenderTicket, OakRenderVideoTicketParams};
use crate::error::{Error, Result};
use crate::handle::CHandle;
use crate::task::Task;
use oakcore_rs::{Rational, TimeRange};
/// `OAKRENDER_TICKET_VIDEO` (`include/render/ticket.h`).
const TICKET_VIDEO: i32 = 0;
/// `OAKRENDER_TICKET_AUDIO` (`include/render/ticket.h`).
const TICKET_AUDIO: i32 = 1;
/// Overrides applied to a render ticket, mirroring the C++ `ForceParams`
/// struct in render.h (fields map onto `oakrender_video_ticket_params`).
///
/// CPP-PARITY: src/task/src/render/render.h (ForceParams)
#[derive(Clone, Debug, Default)]
pub struct ForceParams {
/// Forced output width (0 = off).
pub force_width: i32,
/// Forced output height (0 = off).
pub force_height: i32,
/// Forced color matrix (row-major 4x4); only used when
/// `has_force_matrix` is set.
pub force_matrix: [f64; 16],
/// Whether `force_matrix` is in effect.
pub has_force_matrix: bool,
/// Forced pixel format (`oakcore_rs::PixelFormat` as int; -1 = off).
pub force_format: i32,
/// Forced channel count (0 = off).
pub force_channel_count: i32,
/// Forced color output (borrowed `OakColorProcessor`; empty = none).
pub force_color_output: CHandle,
}
/// Subclass hooks, standing in for the C++ protected virtuals
/// `download_frame`/`frame_downloaded`/`audio_downloaded`/`encode_subtitle`.
pub trait RenderTaskBehavior {
/// Called for each rendered frame.
fn frame_downloaded(&mut self, task: &mut Task, frame: CHandle) -> Result<()>;
/// Called for each rendered audio buffer.
fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()>;
/// Called to encode a subtitle.
fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()>;
}
/// The abstract render task base. Holds the shared [`Task`], the sequence
/// output params, the viewer node, and the subclass behavior.
pub struct RenderTask {
/// The shared task base.
pub base: Task,
/// Output video params (borrowed `OakVideoParams`).
pub video_params: CHandle,
/// Output audio params (borrowed `OakAudioParams`; may be empty).
pub audio_params: CHandle,
/// The viewer node being rendered (borrowed `OakNodeNode`).
pub viewer: CHandle,
/// Force params applied to every ticket.
pub force_params: ForceParams,
/// The subclass behavior.
pub behavior: Option<Box<dyn RenderTaskBehavior + Send>>,
// --- private render inputs (set by the subclass before render()) ---
/// Borrowed color manager.
color_manager: CHandle,
/// Borrowed frame cache.
cache: CHandle,
/// `olive::RenderMode::Mode` as int.
mode: i32,
/// Whether audio ranges are rendered.
audio_enabled: bool,
/// The range rendered by [`RenderTask::render`].
export_range: TimeRange,
/// Whether the render loop emits progress itself (the export task
/// disables it and reports progress per written frame).
native_progress_signalling: bool,
/// Total number of frames in `export_range` (computed by `render`).
total_frames: i64,
/// Maximum render tickets kept in flight at once (the C++
/// `maximum_rendered_frames`; defaults to `available_parallelism`).
max_inflight: usize,
}
impl RenderTask {
/// Create a render task with default (empty) render inputs.
pub fn new(
base: Task,
video_params: CHandle,
audio_params: CHandle,
viewer: CHandle,
force_params: ForceParams,
behavior: Option<Box<dyn RenderTaskBehavior + Send>>,
) -> RenderTask {
RenderTask {
base,
video_params,
audio_params,
viewer,
force_params,
behavior,
color_manager: CHandle::null(),
cache: CHandle::null(),
mode: 0,
audio_enabled: false,
export_range: TimeRange::new(Rational::new(0, 1), Rational::new(0, 1)),
native_progress_signalling: true,
total_frames: 0,
max_inflight: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).max(1),
}
}
/// Configure the render inputs before [`RenderTask::render`] (called by
/// the concrete subclasses' `run`).
pub fn set_render_inputs(
&mut self,
color_manager: CHandle,
cache: CHandle,
mode: i32,
audio_enabled: bool,
export_range: TimeRange,
) {
self.color_manager = color_manager;
self.cache = cache;
self.mode = mode;
self.audio_enabled = audio_enabled;
self.export_range = export_range;
}
/// Enable/disable the render loop's own progress signalling.
pub fn set_native_progress_signalling(&mut self, enabled: bool) {
self.native_progress_signalling = enabled;
}
/// Override the number of render tickets kept in flight at once.
///
/// Defaults to `std::thread::available_parallelism()`, mirroring the
/// C++ `std::max(1, int(std::thread::hardware_concurrency()))`. Primarily
/// a test hook: a smaller window makes concurrent-completion tests
/// deterministic regardless of the host core count.
pub fn set_max_inflight(&mut self, max: usize) {
self.max_inflight = max.max(1);
}
/// The number of frames computed by the last [`RenderTask::render`] run.
pub fn total_frames(&self) -> i64 {
self.total_frames
}
/// Frame duration rational from the video params (falls back to 1/1
/// when the params are empty/invalid).
fn timebase(&self) -> Rational {
let mut num = 0;
let mut den = 1;
unsafe {
bridge::common::oakcommon_videoparams_frame_rate_as_time_base(
self.video_params,
&mut num,
&mut den,
);
}
if num <= 0 || den <= 0 {
Rational::new(1, 1)
} else {
Rational::new(num as i64, den as i64)
}
}
/// Submit one video frame ticket at `time` (mirrors the C++
/// `start_video_ticket`). `dispatch` is the shared completion channel
/// handed to the ticket's finished callback.
fn submit_video_ticket(
&self,
time: Rational,
dispatch: *mut RenderDispatch,
) -> Result<OakRenderTicket> {
let mut output_node = CHandle::null();
unsafe {
bridge::node::oaknode_node_input_get_connected_node(
self.viewer,
cstr(OAKNODE_SEQUENCE_TEXTURE_INPUT),
&mut output_node,
);
}
if output_node.ctx.is_null() {
return Err(Error::Failed("No node connected to the viewer output".to_string()));
}
let audio_params_ptr = if self.audio_params.ctx.is_null() {
std::ptr::null()
} else {
&self.audio_params as *const CHandle
};
let force = &self.force_params;
let params = OakRenderVideoTicketParams {
output_node,
video_params: self.video_params,
audio_params: audio_params_ptr,
time_num: time.numerator(),
time_den: time.denominator(),
color_manager: self.color_manager,
mode: self.mode,
force_width: force.force_width,
force_height: force.force_height,
force_matrix: force.force_matrix,
has_force_matrix: if force.has_force_matrix { 1 } else { 0 },
force_format: force.force_format,
force_channel_count: force.force_channel_count,
force_color_output: force.force_color_output,
force_color_transform: OakColorTransform {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: 0,
},
cache: self.cache,
};
let ticket = unsafe {
bridge::render::oakrender_ticket_render_frame(
&params,
Some(ticket_finished),
dispatch as *mut std::ffi::c_void,
)
};
// Per-frame call: release the borrowed handle box (the ticket keeps
// the native node, not the handle).
unsafe {
bridge::node::oaknode_node_free(&mut output_node);
}
if ticket.ctx.is_null() {
return Err(Error::Failed("Failed to start frame render ticket".to_string()));
}
Ok(ticket)
}
/// Submit one audio ticket for `range` (the Rust API renders a single
/// range; the C++ submits one ticket per audio range).
fn submit_audio_ticket(
&self,
range: TimeRange,
dispatch: *mut RenderDispatch,
) -> Result<OakRenderTicket> {
let mut output_node = CHandle::null();
unsafe {
bridge::node::oaknode_node_input_get_connected_node(
self.viewer,
cstr(OAKNODE_SEQUENCE_SAMPLES_INPUT),
&mut output_node,
);
}
if output_node.ctx.is_null() {
return Err(Error::Failed("No node connected to the viewer samples input".to_string()));
}
let audio_params_ptr = if self.audio_params.ctx.is_null() {
std::ptr::null()
} else {
&self.audio_params as *const CHandle
};
let ticket = unsafe {
bridge::render::oakrender_ticket_render_audio(
output_node,
range.in_().numerator(),
range.in_().denominator(),
range.out().numerator(),
range.out().denominator(),
audio_params_ptr,
self.mode,
Some(ticket_finished),
dispatch as *mut std::ffi::c_void,
)
};
unsafe {
bridge::node::oaknode_node_free(&mut output_node);
}
if ticket.ctx.is_null() {
return Err(Error::Failed("Failed to start audio render ticket".to_string()));
}
Ok(ticket)
}
/// Submit a video ticket and account it as in-flight.
///
/// The in-flight count is bumped **before** the submit: a ticket can
/// complete synchronously (its callback fires before the submit returns),
/// so the callback's decrement must always see its increment. A failed
/// submit (null ticket) never fires a callback, so the bump is rolled
/// back.
fn start_video_ticket(
&self,
time: Rational,
dispatch: *mut RenderDispatch,
in_flight: &mut Vec<OakRenderTicket>,
) -> Result<()> {
unsafe {
(&*dispatch).running.fetch_add(1, Ordering::SeqCst);
}
match self.submit_video_ticket(time, dispatch) {
Ok(ticket) => {
in_flight.push(ticket);
Ok(())
}
Err(e) => {
unsafe {
(&*dispatch).running.fetch_sub(1, Ordering::SeqCst);
}
Err(e)
}
}
}
/// Submit the audio ticket and account it as in-flight (see
/// [`RenderTask::start_video_ticket`] for the counting contract).
fn start_audio_ticket(
&self,
range: TimeRange,
dispatch: *mut RenderDispatch,
in_flight: &mut Vec<OakRenderTicket>,
) -> Result<()> {
unsafe {
(&*dispatch).running.fetch_add(1, Ordering::SeqCst);
}
match self.submit_audio_ticket(range, dispatch) {
Ok(ticket) => {
in_flight.push(ticket);
Ok(())
}
Err(e) => {
unsafe {
(&*dispatch).running.fetch_sub(1, Ordering::SeqCst);
}
Err(e)
}
}
}
/// Map a finished ticket back to its delivery slot: the audio slot for
/// audio tickets, the matching frame slot for video tickets.
fn classify_ticket(
&self,
ticket: &OakRenderTicket,
slot_by_key: &HashMap<(i32, i64, i64), usize>,
) -> Option<usize> {
let kind = unsafe { bridge::render::oakrender_ticket_get_type(*ticket) };
if kind == TICKET_AUDIO {
let mut a = 0i64;
let mut b = 1i64;
let mut c = 0i64;
let mut d = 1i64;
unsafe {
bridge::render::oakrender_ticket_get_range(*ticket, &mut a, &mut b, &mut c, &mut d);
}
slot_by_key.get(&(TICKET_AUDIO, a, b)).copied()
} else if kind == TICKET_VIDEO {
let mut n = 0i64;
let mut d = 1i64;
unsafe {
bridge::render::oakrender_ticket_get_time(*ticket, &mut n, &mut d);
}
slot_by_key.get(&(TICKET_VIDEO, n, d)).copied()
} else {
None
}
}
/// Drive the whole render: keep up to `max_inflight` frame tickets in
/// flight (audio first, then frames in timestamp order), deliver each
/// finished ticket's result to the behavior hooks in timestamp order via
/// the reorder buffer, and stop on cancellation or a hook error —
/// cancelling and waiting every still-running ticket so their
/// completions fire exactly once.
///
/// `task` is the live driving task (cancellation/progress/error
/// reporting); `behavior` is the concrete subclass receiving the
/// `frame_downloaded`/`audio_downloaded` hooks.
pub fn render(&mut self, task: &mut Task, behavior: &mut dyn RenderTaskBehavior) -> Result<()> {
let timebase = self.timebase();
// Compute the frame timestamps (progress denominator mirrors the
// C++ `total_length <= 0 -> 1` guard).
let mut frame_times: Vec<Rational> = Vec::new();
let mut t = self.export_range.in_();
while t < self.export_range.out() {
frame_times.push(t);
t = t + timebase;
}
self.total_frames = frame_times.len() as i64;
let total_length = if frame_times.is_empty() { 1.0 } else { frame_times.len() as f64 };
// Delivery order: the audio range first (mirrors the C++ queue
// order), then every frame in ascending timestamp order. The reorder
// buffer delivers to the hooks in exactly this order no matter the
// completion order.
let mut slots: Vec<DeliverySlot> = Vec::new();
if self.audio_enabled {
slots.push(DeliverySlot {
kind: TICKET_AUDIO,
time: self.export_range.in_(),
});
}
for &ft in &frame_times {
slots.push(DeliverySlot {
kind: TICKET_VIDEO,
time: ft,
});
}
let mut slot_by_key: HashMap<(i32, i64, i64), usize> = HashMap::with_capacity(slots.len());
for (i, slot) in slots.iter().enumerate() {
slot_by_key.insert((slot.kind, slot.time.numerator(), slot.time.denominator()), i);
}
let total_slots = slots.len();
// The shared completion channel. Heap-allocated: the C callback
// reaches it through its `userdata` pointer. Freed only once every
// in-flight ticket has fired its callback (`running == 0`).
let dispatch = Box::into_raw(Box::new(RenderDispatch::new()));
// Shared view of the same state for the render thread (the raw
// pointer stays valid until the box is freed below).
let dispatch_ref = unsafe { &*dispatch };
let window = self.max_inflight.max(1);
// Tickets we hold (the submitter's copy of each handle, released
// exactly once at the end); `consumed` counts tickets whose finished
// queue copy has been popped, so `in_flight.len() - consumed` is the
// live window.
let mut in_flight: Vec<OakRenderTicket> = Vec::new();
let mut consumed = 0usize;
// Next frame timestamp to submit and next delivery slot.
let mut next_frame_index = 0usize;
let mut next_slot = 0usize;
// Reorder buffer: finished tickets not yet deliverable.
let mut pending: HashMap<usize, OakRenderTicket> = HashMap::new();
let mut progress_counter = 0.0;
let mut result: Result<()> = Ok(());
// Queue audio first (mirrors the C++ order).
if self.audio_enabled && result.is_ok() {
if let Err(e) = self.start_audio_ticket(self.export_range, dispatch, &mut in_flight) {
result = Err(e);
}
}
// Start the initial frame window.
if result.is_ok() {
while in_flight.len() < window && next_frame_index < frame_times.len() {
if let Err(e) =
self.start_video_ticket(frame_times[next_frame_index], dispatch, &mut in_flight)
{
result = Err(e);
break;
}
next_frame_index += 1;
}
}
while result.is_ok() && !task.is_cancelled() && next_slot < total_slots {
// Drain the completion queue into the reorder buffer.
while let Some(ticket) = dispatch_ref.pop_finished() {
consumed += 1;
match self.classify_ticket(&ticket, &slot_by_key) {
Some(slot_index) => {
pending.insert(slot_index, ticket);
}
None => {
result = Err(Error::Failed(
"Render ticket reported an unexpected timestamp".to_string(),
));
break;
}
}
}
if result.is_err() {
break;
}
// Deliver contiguous results in the observable (timestamp) order.
while let Some(ticket) = pending.remove(&next_slot) {
let slot = &slots[next_slot];
if slot.kind == TICKET_AUDIO {
unsafe {
bridge::render::oakrender_ticket_get_samples(ticket, std::ptr::null_mut());
}
if let Err(e) = behavior.audio_downloaded(task, CHandle::null()) {
result = Err(e);
break;
}
} else {
let mut frame = CHandle::null();
unsafe {
bridge::render::oakrender_ticket_get_frame(ticket, &mut frame);
}
if let Err(e) = behavior.frame_downloaded(task, frame) {
result = Err(e);
break;
}
if !frame.ctx.is_null() {
unsafe {
bridge::render::oakrender_codec_frame_free(&mut frame);
}
}
if self.native_progress_signalling {
progress_counter += 1.0;
task.emit_progress(progress_counter / total_length);
}
}
next_slot += 1;
}
if result.is_err() || task.is_cancelled() {
break;
}
// Refill the window: one new ticket per delivered frame.
while in_flight.len().saturating_sub(consumed) < window
&& next_frame_index < frame_times.len()
{
if let Err(e) =
self.start_video_ticket(frame_times[next_frame_index], dispatch, &mut in_flight)
{
result = Err(e);
break;
}
next_frame_index += 1;
}
if result.is_err() {
break;
}
if next_slot >= total_slots {
break;
}
// Wait for the next completion (a cancellation or a hook error
// aborts the wait; in-flight tickets then finish, waking us).
let mut guard = dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner());
while guard.is_empty()
&& dispatch_ref.running.load(Ordering::SeqCst) > 0
&& !task.is_cancelled()
{
guard = dispatch_ref.cv.wait(guard).unwrap_or_else(|e| e.into_inner());
}
drop(guard);
if dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner()).is_empty()
&& dispatch_ref.running.load(Ordering::SeqCst) == 0
{
// Every ticket finished and its queue copy was consumed.
break;
}
}
// Cancellation that aborted the loop before every slot was delivered
// maps to the cancellation error (the sync loop returned
// `Error::Cancelled` from its between-frames check; a render that
// finished every frame despite a late cancel stays successful).
if result.is_ok() && task.is_cancelled() && next_slot < total_slots {
result = Err(Error::Cancelled);
}
// Tear down. On cancellation or error, cancel and wait every ticket
// still in flight (the C++ abort path), so their completions still
// fire exactly once. Then wait until every callback has returned,
// release the submitter's handles, and free the completion channel.
if result.is_err() || task.is_cancelled() {
for &ticket in &in_flight {
unsafe {
bridge::render::oakrender_ticket_cancel(ticket);
bridge::render::oakrender_ticket_wait(ticket);
}
}
}
dispatch_ref.wait_idle();
for ticket in in_flight.iter_mut() {
unsafe {
bridge::render::oakrender_ticket_free(ticket);
}
}
unsafe {
drop(Box::from_raw(dispatch));
}
result
}
/// A detached render state used while a concrete task temporarily moves
/// its render out of itself to drive it with itself as the behavior
/// (avoids a self-referential borrow). Never used for actual rendering.
pub(crate) fn placeholder() -> RenderTask {
RenderTask::new(
Task::new("", CHandle::null()),
CHandle::null(),
CHandle::null(),
CHandle::null(),
ForceParams::default(),
None,
)
}
}
/// One deliverable unit of the render: an audio range or a frame time, in
/// the observable delivery order (audio first, then frames in time order).
struct DeliverySlot {
/// `TICKET_AUDIO` or `TICKET_VIDEO`.
kind: i32,
/// Frame time (video) or range start (audio).
time: Rational,
}
/// Shared state between the render thread and the ticket-finished
/// callbacks (the ticket's async return channel, mirroring the C++
/// `finished_mutex_`/`finished_tickets_`/`finished_wait_cond_`). One
/// instance per [`RenderTask::render`] run, reached from the C callback
/// through its `userdata` pointer; heap-allocated for the run and freed
/// only after every in-flight ticket has fired (`running == 0`).
struct RenderDispatch {
/// Finished tickets in completion order (callbacks push here).
finished: Mutex<VecDeque<OakRenderTicket>>,
/// Tickets whose completion callback has not fired yet.
running: AtomicUsize,
/// Wakes the render thread when a ticket finishes.
cv: Condvar,
}
impl RenderDispatch {
fn new() -> RenderDispatch {
RenderDispatch {
finished: Mutex::new(VecDeque::new()),
running: AtomicUsize::new(0),
cv: Condvar::new(),
}
}
/// Pop the next finished ticket (the callback's borrowed copy; `None`
/// when the queue is empty).
fn pop_finished(&self) -> Option<OakRenderTicket> {
self.finished
.lock()
.unwrap_or_else(|e| e.into_inner())
.pop_front()
}
/// Block until every submitted ticket has fired its callback. Called
/// before freeing `self`, so no callback can touch the state afterwards.
fn wait_idle(&self) {
let mut guard = self.finished.lock().unwrap_or_else(|e| e.into_inner());
while self.running.load(Ordering::SeqCst) > 0 {
guard = self.cv.wait(guard).unwrap_or_else(|e| e.into_inner());
}
}
}
/// Ticket-finished callback, mirroring the C++ `on_ticket_finished`. Fires
/// on the ticket's finishing thread; pushes the ticket's borrowed handle
/// copy into the completion queue and wakes the render thread. The push
/// precedes the in-flight decrement, so a waiter never observes
/// `running == 0` with a missing queue entry.
///
/// # Safety
///
/// `userdata` must point to a live `RenderDispatch` for the duration of
/// every in-flight ticket (guaranteed by [`RenderTask::render`]'s
/// `wait_idle` before freeing it).
unsafe extern "C" fn ticket_finished(ticket: OakRenderTicket, userdata: *mut std::ffi::c_void) {
let dispatch = unsafe { &*(userdata as *const RenderDispatch) };
let mut queue = dispatch.finished.lock().unwrap_or_else(|e| e.into_inner());
queue.push_back(ticket);
dispatch.running.fetch_sub(1, Ordering::SeqCst);
dispatch.cv.notify_all();
}
+359
View File
@@ -0,0 +1,359 @@
// 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/>.
//! The `Task` base class, mirroring `src/task/src/task.h` (`olive::Task`).
//!
//! The C++ class is abstract with a protected virtual `run()`. In Rust each
//! concrete task (`ConformTask`, `ProxyTask`, …) is its own struct and the
//! shared lifecycle lives here; the per-task work is supplied through
//! [`TaskBehavior`] (a trait object, per architectural decision #1 in
//! README.md). Cancellation rides on a borrowed oakrender `OakCancelAtom`
//! reached through `crate::bridge::render`.
//!
//! CPP-PARITY: src/task/src/task.h
//!
//! ## Concurrency contract
//!
//! Like the C++ original, the task's `title`/`error` strings are mutated on
//! the task's own thread (`run()`). The C ABI layer must therefore only read
//! them while the task is not concurrently running — i.e. before
//! [`Task::start`] or after the task finished (the `finished` flag is
//! synchronized through the `done` condvar, so `is_finished()`/
//! `succeeded()` are always race-free). This mirrors the C++ semantics
//! exactly.
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use crate::bridge;
use crate::error::{Error, Result};
use crate::handle::CHandle;
/// Event type emitted through a task's [`EventListener`], mirroring the
/// C++ `EventType` enum (`k_event_started`/`k_event_progress`/`k_event_finished`).
///
/// CPP-PARITY: src/task/src/task.h (EventType)
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TaskEvent {
/// Task began executing.
Started,
/// Progress updated; the payload is in 0.0..=1.0.
Progress(f64),
/// Task finished (success or failure).
Finished,
}
/// A listener notified of [`TaskEvent`]s, mirroring the C++
/// `std::function<void(EventType,double)>` listener. Delivered under the
/// task's lock; kept as the single async return channel (decision #3).
///
/// CPP-PARITY: src/task/src/task.h (EventListener)
pub type EventListener = Box<dyn FnMut(TaskEvent) + Send>;
/// Trait supplying the per-task work, standing in for the C++ protected
/// virtual `run()`. Concrete tasks implement this; [`Task::start`] drives it.
///
/// CPP-PARITY: src/task/src/task.h (run)
pub trait TaskBehavior {
/// Perform the task's work. Returning `Err(Error::Cancelled)` is treated
/// as a cancellation; any other `Err` marks the task failed.
fn run(&mut self, task: &mut Task) -> Result<()>;
}
/// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the
/// C ABI callback signature `(event_id, value, userdata)`. The C++ emits
/// the start timestamp with `k_event_started` and 1.0/0.0 with
/// `k_event_finished`; `TaskEvent` carries neither, so the task publishes
/// them into this shared state before emitting (decision #1 in README.md).
#[derive(Debug, Default)]
pub struct SubscriberState {
/// Start timestamp (ms since epoch) published before `Started` is emitted.
pub start_ms: AtomicI64,
/// 1.0/0.0 published before `Finished` is emitted.
pub finished_value: AtomicI64,
}
/// Finish state, protected by the `done` condvar pair so `is_finished`/
/// `succeeded` are readable from any thread while the task thread mutates
/// them.
#[derive(Default)]
struct TaskDone {
/// Whether the task has finished (success or failure).
finished: bool,
/// Whether the task finished successfully.
succeeded: bool,
}
/// The base task. Owns lifecycle state plus a borrowed oakrender cancel
/// atom; the concrete behavior lives in a [`TaskBehavior`] trait object.
pub struct Task {
title: String,
error: Option<String>,
start_time: Option<std::time::Instant>,
cancel_atom: CHandle,
owns_atom: bool,
event_listener: Option<EventListener>,
cancel_event: Option<Box<dyn FnMut() + Send>>,
started: bool,
finished: bool,
succeeded: bool,
behavior: Option<Box<dyn TaskBehavior + Send>>,
/// Finish/success flag pair + wakeup condvar (race-free readers).
done: Arc<(Mutex<TaskDone>, Condvar)>,
/// Values published for the C ABI subscribe wrapper.
subscriber: Option<Arc<SubscriberState>>,
}
impl Drop for Task {
fn drop(&mut self) {
// Free the cancellation atom only when we created it ourselves;
// borrowed atoms are released by their owner.
if self.owns_atom && !self.cancel_atom.is_null() {
let mut atom = self.cancel_atom;
unsafe {
bridge::render::oakrender_cancelatom_free(&mut atom);
}
}
}
}
impl Task {
/// Create a new task with the given title and a borrowed oakrender
/// cancel atom. When `cancel_atom` is empty a fresh atom is created and
/// owned by the task (mirroring the C++ constructor).
pub fn new(title: &str, cancel_atom: CHandle) -> Task {
let (cancel_atom, owns_atom) = if cancel_atom.is_null() {
let atom = unsafe { bridge::render::oakrender_cancelatom_init() };
(atom, true)
} else {
(cancel_atom, false)
};
Task {
title: title.to_string(),
error: None,
start_time: None,
cancel_atom,
owns_atom,
event_listener: None,
cancel_event: None,
started: false,
finished: false,
succeeded: false,
behavior: None,
done: Arc::new((Mutex::new(TaskDone::default()), Condvar::new())),
subscriber: None,
}
}
/// Attach the concrete behavior (defaults to a no-op).
pub fn set_behavior(&mut self, behavior: Box<dyn TaskBehavior + Send>) {
self.behavior = Some(behavior);
}
/// Start the task: set `started`, emit [`TaskEvent::Started`], run the
/// behavior, then mark finished and emit [`TaskEvent::Finished`].
///
/// The returned `Result` mirrors the C++ `start()` bool: `Ok(())` when
/// the behavior succeeded, `Err(..)` otherwise.
pub fn start(&mut self) -> Result<()> {
let start_ms = system_time_ms();
self.start_time = Some(std::time::Instant::now());
self.started = true;
if let Some(s) = &self.subscriber {
s.start_ms.store(start_ms, Ordering::SeqCst);
}
self.emit_event(TaskEvent::Started);
// Take the behavior out so it can receive `self` (avoids a
// self-referential borrow); put it back afterwards.
let behavior = self.behavior.take();
let ret = if let Some(mut b) = behavior {
let r = b.run(self);
self.behavior = Some(b);
r
} else {
Ok(())
};
let succeeded = ret.is_ok();
{
let mut done = self.done.0.lock().unwrap();
done.finished = true;
done.succeeded = succeeded;
self.done.1.notify_all();
}
if let Some(s) = &self.subscriber {
s.finished_value.store(if succeeded { 1 } else { 0 }, Ordering::SeqCst);
}
self.emit_event(TaskEvent::Finished);
// One-shot subscription: drop the listener after the final event.
self.event_listener = None;
ret
}
/// Request cancellation through the borrowed oakrender cancel atom, then
/// invoke the cancel event callback if one is registered.
pub fn cancel(&mut self) {
if !self.cancel_atom.is_null() {
let atom = self.cancel_atom;
unsafe {
bridge::render::oakrender_cancelatom_cancel(atom);
}
}
if let Some(cb) = self.cancel_event.as_mut() {
cb();
}
}
/// Whether cancellation was requested (queries the oakrender atom).
pub fn is_cancelled(&self) -> bool {
if self.cancel_atom.is_null() {
return false;
}
let atom = self.cancel_atom;
let mut cancelled = 0;
unsafe {
bridge::render::oakrender_cancelatom_is_cancelled(atom, &mut cancelled);
}
cancelled != 0
}
/// The borrowed cancel atom handle (empty for tasks without one).
pub fn get_cancel_atom(&self) -> CHandle {
self.cancel_atom
}
/// Replace the cancel atom. A previously owned atom is freed; the new
/// atom is borrowed (never freed by this task). Used to share one atom
/// between a task and its behavior's inner base task.
pub fn set_cancel_atom(&mut self, atom: CHandle) {
if self.owns_atom && !self.cancel_atom.is_null() {
let mut old = self.cancel_atom;
unsafe {
bridge::render::oakrender_cancelatom_free(&mut old);
}
}
self.cancel_atom = atom;
self.owns_atom = false;
}
/// Register the event listener. Replaces any previous listener.
pub fn set_event_listener(&mut self, listener: EventListener) {
self.event_listener = Some(listener);
}
/// Register a hook invoked when [`Task::cancel`] is called.
pub fn set_cancel_event(&mut self, cb: Box<dyn FnMut() + Send>) {
self.cancel_event = Some(cb);
}
/// Publish the shared values used by the C ABI subscribe wrapper.
pub fn set_subscriber(&mut self, state: Arc<SubscriberState>) {
self.subscriber = Some(state);
}
/// Emit a progress update, clamping to 0.0..=1.0 and delivering it to the
/// listener as [`TaskEvent::Progress`].
pub fn emit_progress(&mut self, progress: f64) {
self.emit_event(TaskEvent::Progress(progress.clamp(0.0, 1.0)));
}
/// Reset lifecycle state so the task can be run again.
pub fn reset(&mut self) {
self.started = false;
self.finished = false;
self.succeeded = false;
self.error = None;
self.start_time = None;
{
let mut done = self.done.0.lock().unwrap();
done.finished = false;
done.succeeded = false;
}
}
/// Record a failure message and mark the task failed.
pub fn set_error(&mut self, message: &str) {
self.error = Some(message.to_string());
}
/// Change the task title.
pub fn set_title(&mut self, title: &str) {
self.title = title.to_string();
}
/// The task title.
pub fn title(&self) -> &str {
&self.title
}
/// The failure message, if the task failed.
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
/// Whether the task has finished (success or failure).
pub fn is_finished(&self) -> bool {
self.done.0.lock().unwrap().finished
}
/// Whether the task finished successfully.
pub fn succeeded(&self) -> bool {
self.done.0.lock().unwrap().succeeded
}
/// Block until the task finishes. Mirrors the C++ finished-wait condvar;
/// the manager joins worker threads instead, but this is useful for
/// direct (non-manager) runs. Returns immediately when the task has not
/// been started yet (nothing to wait for) or is already finished.
pub fn wait_finished(&self) {
if !self.started {
return;
}
let mut done = self.done.0.lock().unwrap();
while !done.finished {
done = self.done.1.wait(done).unwrap();
}
}
/// The elapsed time since [`Task::start`], for duration reporting.
pub fn elapsed(&self) -> Option<std::time::Duration> {
self.start_time.map(|t| t.elapsed())
}
fn emit_event(&mut self, ev: TaskEvent) {
if let Some(listener) = self.event_listener.as_mut() {
listener(ev);
}
}
}
/// Current wall-clock time in milliseconds since the Unix epoch, matching
/// the C++ `start_time_` convention (std::chrono::system_clock ms).
pub fn system_time_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
/// Marker error returned when a task is cancelled, so the C ABI can map it to
/// `OAKTASK_E_CANCELLED` (distinct from a generic failure).
pub fn cancelled() -> Error {
Error::Cancelled
}