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:
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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(¶ms.extension) },
|
||||
preset: unsafe { cstr_buf_to_string(¶ms.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 }
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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})"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
¶ms,
|
||||
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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user