refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
This commit is contained in:
@@ -1,293 +0,0 @@
|
||||
// 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) }
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// 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) }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// 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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,290 +0,0 @@
|
||||
// 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,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) }
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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) }
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// 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) }
|
||||
}
|
||||
@@ -17,23 +17,26 @@
|
||||
//! 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.
|
||||
//! Wires oakcodec's task-submit callback (`oakcodec::task::set_task_submit_cb`)
|
||||
//! so conform/proxy requests from the codec side land back in the task
|
||||
//! module. The registration is a direct Rust closure (single-lib
|
||||
//! unification: the old extern-C submit callback is gone).
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/codecbridge.h
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use oakcodec::error::Error as CodecError;
|
||||
use oakcodec::proxymanager::ProxyManager;
|
||||
use oakcodec::task::{
|
||||
set_task_submit_cb, task_submit_is_registered, TaskKind, TaskRequest, TaskSubmitFn,
|
||||
};
|
||||
|
||||
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 {
|
||||
/// Convert oakcodec proxy params into the Rust [`ProxyParams`].
|
||||
fn proxy_params_from_codec(params: &oakcodec::proxymanager::ProxyParams) -> ProxyParams {
|
||||
ProxyParams {
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
@@ -41,72 +44,48 @@ fn proxy_params_from_codec(params: &bridge::codec::OakCodecProxyParams) -> Proxy
|
||||
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) },
|
||||
extension: cstr_buf_to_string(¶ms.extension),
|
||||
preset: 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 {
|
||||
/// Read a NUL-terminated byte array into a String (lossy).
|
||||
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()
|
||||
String::from_utf8_lossy(&buf[..len]).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 };
|
||||
|
||||
fn submit_codec_task(
|
||||
req: &TaskRequest,
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
) -> oakcodec::error::Result<()> {
|
||||
// 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);
|
||||
match req.kind {
|
||||
TaskKind::Conform => {
|
||||
let task = ConformTask::new(req);
|
||||
let atom = task.base.get_cancel_atom();
|
||||
let title = task.base.title().to_string();
|
||||
let mut outer = Task::new(&title, atom);
|
||||
let mut outer = Task::new(&title, Some(atom));
|
||||
outer.set_behavior(Box::new(task));
|
||||
match outer.start() {
|
||||
Ok(()) => 0,
|
||||
Err(_) => bridge::codec::OAKCODEC_E_FAILED,
|
||||
}
|
||||
outer
|
||||
.start()
|
||||
.map_err(|_| CodecError::Failed("conform task failed".to_string()))
|
||||
}
|
||||
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);
|
||||
}
|
||||
TaskKind::Proxy => {
|
||||
let codec_params = ProxyManager::proxy_params_from_config();
|
||||
let params = proxy_params_from_codec(&codec_params);
|
||||
let task = ProxyTask::new(request, params);
|
||||
let task = ProxyTask::new(req, params);
|
||||
let atom = task.base.get_cancel_atom();
|
||||
let title = task.base.title().to_string();
|
||||
let mut outer = Task::new(&title, atom);
|
||||
let mut outer = Task::new(&title, Some(atom));
|
||||
outer.set_behavior(Box::new(task));
|
||||
match outer.start() {
|
||||
Ok(()) => 0,
|
||||
Err(_) => bridge::codec::OAKCODEC_E_FAILED,
|
||||
}
|
||||
outer
|
||||
.start()
|
||||
.map_err(|_| CodecError::Failed("proxy task failed".to_string()))
|
||||
}
|
||||
_ => bridge::codec::OAKCODEC_E_INVALID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,21 +95,18 @@ 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());
|
||||
}
|
||||
let cb: &'static TaskSubmitFn = Box::leak(Box::new(submit_codec_task));
|
||||
set_task_submit_cb(Some(cb), 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());
|
||||
}
|
||||
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 }
|
||||
task_submit_is_registered()
|
||||
}
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
//! `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.
|
||||
//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes. The
|
||||
//! decoder is reached through the direct Rust API
|
||||
//! (`oakcodec::decoder::{receive_list_of_all_decoders, Decoder}`) —
|
||||
//! single-lib unification; the old oakcodec decoder C ABI is gone.
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/conform/conform.h
|
||||
|
||||
use crate::bridge;
|
||||
use oakcodec::decoder::CodecStream;
|
||||
use oakcodec::task::TaskRequest;
|
||||
|
||||
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
|
||||
@@ -53,14 +56,15 @@ pub struct ConformTask {
|
||||
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);
|
||||
pub fn new(request: &TaskRequest) -> ConformTask {
|
||||
let title = format!(
|
||||
"Conforming Audio {}:{}",
|
||||
request.input_filename, request.stream_index
|
||||
);
|
||||
ConformTask {
|
||||
base: Task::new(&title, CHandle::null()),
|
||||
input_filename: input,
|
||||
output_filename: output,
|
||||
base: Task::new(&title, None),
|
||||
input_filename: request.input_filename.to_string(),
|
||||
output_filename: request.output_filename.to_string(),
|
||||
stream_index: request.stream_index,
|
||||
sample_rate: request.sample_rate,
|
||||
channel_layout: request.channel_layout,
|
||||
@@ -106,7 +110,7 @@ impl ConformTask {
|
||||
}
|
||||
|
||||
impl TaskBehavior for ConformTask {
|
||||
/// Run the conform via the oakcodec decoder (`bridge::codec`), emitting
|
||||
/// Run the conform via the direct oakcodec decoder API, emitting
|
||||
/// progress as audio is decoded and written to the working files, then
|
||||
/// rename working → final.
|
||||
fn run(&mut self, task: &mut Task) -> Result<()> {
|
||||
@@ -119,72 +123,63 @@ impl TaskBehavior for ConformTask {
|
||||
self.final_names = final_names;
|
||||
self.working_names = working_names;
|
||||
|
||||
let decoder = unsafe { bridge::codec::oakcodec_decoder_init() };
|
||||
if decoder.ctx.is_null() {
|
||||
let atom = task.get_cancel_atom();
|
||||
|
||||
// Pick the first registered decoder that can probe the file (the
|
||||
// C++ picks the decoder the footage was created with; probing in
|
||||
// registry order is the direct-Rust equivalent).
|
||||
let decoder = oakcodec::decoder::receive_list_of_all_decoders()
|
||||
.into_iter()
|
||||
.find(|d| d.probe(&self.input_filename, Some(&atom)).is_some());
|
||||
let Some(decoder) = decoder else {
|
||||
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}"));
|
||||
let stream =
|
||||
CodecStream::with_block(self.input_filename.clone(), self.stream_index, None);
|
||||
if let Err(e) = decoder.open(&stream) {
|
||||
task.set_error(&format!("Failed to open decoder for audio conform: {e}"));
|
||||
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);
|
||||
}
|
||||
let conform_result = decoder.conform_audio(
|
||||
&self.working_names,
|
||||
self.sample_rate,
|
||||
self.channel_layout,
|
||||
self.sample_format,
|
||||
Some(&atom),
|
||||
);
|
||||
let _ = decoder.close();
|
||||
|
||||
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;
|
||||
match conform_result {
|
||||
Ok(()) => {
|
||||
// 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
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// Clean up any partial working files.
|
||||
for name in &self.working_names {
|
||||
let _ = std::fs::remove_file(name);
|
||||
Err(_) => {
|
||||
// Clean up any partial working files.
|
||||
for name in &self.working_names {
|
||||
let _ = std::fs::remove_file(name);
|
||||
}
|
||||
if atom.is_cancelled() {
|
||||
task.set_error("Audio conform was cancelled");
|
||||
} else {
|
||||
task.set_error("Audio conform failed");
|
||||
}
|
||||
false
|
||||
}
|
||||
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 {
|
||||
@@ -192,19 +187,3 @@ impl TaskBehavior for ConformTask {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use oakcommon::cancelatom::CancelAtom;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::CHandle;
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
|
||||
/// Shared state between the parked task thread, the owning cache
|
||||
@@ -41,19 +42,14 @@ pub struct CustomCacheState {
|
||||
/// 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,
|
||||
atom: Arc<CancelAtom>,
|
||||
}
|
||||
|
||||
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.atom.cancel();
|
||||
self.wait.notify_one();
|
||||
}
|
||||
|
||||
@@ -71,12 +67,7 @@ impl CustomCacheState {
|
||||
/// 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.atom.cancel();
|
||||
self.cancel_event();
|
||||
}
|
||||
|
||||
@@ -104,7 +95,7 @@ impl CustomCacheTask {
|
||||
pub fn new(sequence_name: &str) -> CustomCacheTask {
|
||||
let base = Task::new(
|
||||
&format!("Caching custom range for \"{sequence_name}\""),
|
||||
CHandle::null(),
|
||||
None,
|
||||
);
|
||||
let state = Arc::new(CustomCacheState {
|
||||
cancelled_through_finish: Mutex::new(false),
|
||||
@@ -145,7 +136,7 @@ impl TaskBehavior for CustomCacheTask {
|
||||
/// `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
|
||||
// under (the outer task driven by the manager). 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.
|
||||
|
||||
+161
-188
@@ -16,10 +16,19 @@
|
||||
|
||||
//! `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.
|
||||
//! Renders the viewer output through [`crate::render::RenderTask`] and
|
||||
//! writes it to a file via the direct oakcodec encoder API
|
||||
//! (`oakcodec::encoder::{create_from_params, Encoder}` — single-lib
|
||||
//! unification; the old encoder C ABI is gone), mapping each rendered
|
||||
//! frame to an `Encoder::write_video` call and each rendered audio buffer
|
||||
//! to `Encoder::write_audio`.
|
||||
//!
|
||||
//! The viewer and color-manager arguments of the deleted C ABI path are
|
||||
//! replaced by a single [`crate::nodeops::NodeRef`] viewer (the color
|
||||
//! manager no longer crosses into the render tickets — the direct ticket
|
||||
//! arena performs no color management). Rendered frames arrive as
|
||||
//! `oakrender::texture::Texture` values and are converted to
|
||||
//! `oakcodec::frame::Frame` values for the encoder.
|
||||
//!
|
||||
//! **Simplifications over the C++**: no temporary-file rename dance (the
|
||||
//! encoder writes straight to the requested filename), no sidecar subtitle
|
||||
@@ -29,45 +38,36 @@
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/export/export.h
|
||||
|
||||
use crate::bridge;
|
||||
use crate::bridge::codec::OakCodecEncodingParams;
|
||||
use std::sync::Arc;
|
||||
|
||||
use oakcodec::encoder::{create_from_params, Encoder};
|
||||
use oakcodec::encodingparams::EncodingParams as CodecEncodingParams;
|
||||
use oakcommon::videoparams::VideoParams as CommonVideoParams;
|
||||
use oakrender::texture::Texture;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ffi::taskhandle::cstr;
|
||||
use crate::handle::CHandle;
|
||||
use crate::nodeops::{self, NodeRef};
|
||||
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.
|
||||
/// An export task. Owns the encoder; 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`).
|
||||
/// The node being exported (footage or sequence).
|
||||
pub viewer_node: NodeRef,
|
||||
/// The encoding parameters (mirror of the codec-side 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,
|
||||
/// The opened encoder (direct oakcodec trait object).
|
||||
encoder: Option<Arc<dyn Encoder>>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Mirror of the codec encoding params, kept as plain fields so the export
|
||||
/// task can be configured without a codec-side handle. Only the fields the
|
||||
/// task reads are declared; see `oakcodec::encodingparams::EncodingParams`
|
||||
/// for the full inventory.
|
||||
pub struct EncodingParams {
|
||||
/// Output filename.
|
||||
pub filename: String,
|
||||
@@ -91,6 +91,10 @@ pub struct EncodingParams {
|
||||
pub audio_enabled: bool,
|
||||
/// Audio codec id.
|
||||
pub audio_codec: i32,
|
||||
/// Audio sample rate (Hz) the encoder opens with.
|
||||
pub audio_sample_rate: i32,
|
||||
/// ffmpeg-style channel layout mask the encoder opens with.
|
||||
pub audio_channel_layout: u64,
|
||||
/// Whether subtitles are exported.
|
||||
pub subtitles_enabled: bool,
|
||||
/// Export length numerator (seconds rational).
|
||||
@@ -100,76 +104,35 @@ pub struct EncodingParams {
|
||||
}
|
||||
|
||||
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);
|
||||
/// Build a new export task from the viewer node and the encoding
|
||||
/// params. The old `viewer: CHandle` / `color_manager: CHandle`
|
||||
/// signature is replaced by the domain viewer [`NodeRef`] (single-lib
|
||||
/// unification; the color manager is dropped with the C ABI — see the
|
||||
/// module docs).
|
||||
pub fn new(viewer: NodeRef, params: EncodingParams) -> ExportTask {
|
||||
let label = nodeops::node_label(&viewer.0, viewer.1);
|
||||
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,
|
||||
);
|
||||
let base = Task::new(&title, None);
|
||||
// The render base needs the viewer's frame rate to step one frame
|
||||
// per video frame (the export range is in sequence/footage time);
|
||||
// without it the frame loop falls back to a 1/1 timebase and
|
||||
// exports one frame per second (observed in the facade's
|
||||
// `it_export` run).
|
||||
let video_params = nodeops::sequence_video_params(&viewer.0, viewer.1, 0)
|
||||
.or_else(|| nodeops::footage_video_params(&viewer.0, viewer.1, 0));
|
||||
let render = RenderTask::new(base, video_params, viewer.clone(), 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,
|
||||
encoder: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the C-ABI encoding-params POD for the encoder from the Rust
|
||||
/// Build the codec `EncodingParams` 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,
|
||||
};
|
||||
fn build_codec_params(&self) -> CodecEncodingParams {
|
||||
let mut params = CodecEncodingParams::default();
|
||||
let bytes = self.encoding_params.filename.as_bytes();
|
||||
let n = bytes.len().min(1023);
|
||||
params.filename[..n].copy_from_slice(&bytes[..n]);
|
||||
@@ -180,9 +143,12 @@ impl ExportTask {
|
||||
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.video_pixel_format =
|
||||
crate::nodeops::pixel_format_from_code(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.audio_sample_rate = self.encoding_params.audio_sample_rate;
|
||||
params.audio_channel_layout = self.encoding_params.audio_channel_layout;
|
||||
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;
|
||||
@@ -190,17 +156,63 @@ impl ExportTask {
|
||||
}
|
||||
|
||||
/// Resolve the export range: the custom range when set, otherwise the
|
||||
/// whole sequence length.
|
||||
/// whole viewer length (direct `oaknode` domain query; the deleted
|
||||
/// `oaknode_sequence_get_length` stub is gone).
|
||||
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);
|
||||
let length = nodeops::node_length(&self.viewer_node.0, self.viewer_node.1);
|
||||
TimeRange::new(Rational::new(0, 1), length)
|
||||
}
|
||||
|
||||
/// Copy a rendered `oakrender` CPU texture into an `oakcodec` frame
|
||||
/// with the matching video params (row-wise copy — line sizes may
|
||||
/// differ between the render and codec frame layouts).
|
||||
fn to_codec_frame(texture: &Texture) -> Result<oakcodec::frame::Frame> {
|
||||
let Texture::Cpu(frame) = texture else {
|
||||
return Err(Error::Failed(
|
||||
"Render produced a GPU texture; the CPU encoder path cannot consume it"
|
||||
.to_string(),
|
||||
));
|
||||
};
|
||||
let params = CommonVideoParams::new_basic(
|
||||
frame.width,
|
||||
frame.height,
|
||||
oakcommon::ocioutils::PixelFormat::from_code(frame.format as i32),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
let mut out = oakcodec::frame::Frame::with_params(params);
|
||||
out.set_timestamp(frame.timestamp);
|
||||
out.allocate().map_err(|e| {
|
||||
Error::Failed(format!("Failed to allocate encoder frame: {e:?}"))
|
||||
})?;
|
||||
let dst_stride = out.linesize_bytes() as usize;
|
||||
let Some(dst) = out.data_mut() else {
|
||||
return Err(Error::Failed(
|
||||
"Encoder frame allocation produced no buffer".to_string(),
|
||||
));
|
||||
};
|
||||
let src = frame.data.as_slice();
|
||||
let src_stride = frame.linesize_bytes();
|
||||
let row_bytes = std::cmp::min(src_stride, dst_stride)
|
||||
.min(src.len())
|
||||
.min(dst.len());
|
||||
if src_stride == dst_stride && src.len() == dst.len() {
|
||||
dst.copy_from_slice(src);
|
||||
} else {
|
||||
for y in 0..frame.height as usize {
|
||||
let src_start = y * src_stride;
|
||||
let dst_start = y * dst_stride;
|
||||
if src_start + row_bytes > src.len() || dst_start + row_bytes > dst.len() {
|
||||
break;
|
||||
}
|
||||
dst[dst_start..dst_start + row_bytes]
|
||||
.copy_from_slice(&src[src_start..src_start + row_bytes]);
|
||||
}
|
||||
}
|
||||
TimeRange::new(
|
||||
Rational::new(0, 1),
|
||||
Rational::new(len_num as i64, len_den as i64),
|
||||
)
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,19 +222,18 @@ impl TaskBehavior for ExportTask {
|
||||
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() {
|
||||
let Some(encoder) = create_from_params(&codec_params) else {
|
||||
task.set_error("Failed to create encoder");
|
||||
return Err(Error::Failed("Failed to create encoder".to_string()));
|
||||
}
|
||||
self.encoder = encoder;
|
||||
};
|
||||
self.encoder = Some(encoder.clone());
|
||||
|
||||
if unsafe { bridge::codec::oakcodec_encoder_open(encoder) } != 0 {
|
||||
let err = encoder_error(encoder);
|
||||
if let Err(e) = encoder.open() {
|
||||
let err = encoder.get_error();
|
||||
task.set_error(&format!("Failed to open file: {err}"));
|
||||
let _ = e;
|
||||
return Err(Error::Failed("Failed to open file".to_string()));
|
||||
}
|
||||
self.subtitle_encoder = encoder;
|
||||
|
||||
let export_range = self.export_range();
|
||||
|
||||
@@ -232,14 +243,14 @@ impl TaskBehavior for ExportTask {
|
||||
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_format = encoder
|
||||
.desired_pixel_format()
|
||||
.map(|f| f as i32)
|
||||
.unwrap_or(-1);
|
||||
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,
|
||||
@@ -256,10 +267,11 @@ impl TaskBehavior for ExportTask {
|
||||
result?;
|
||||
|
||||
// Flush the encoder and surface any trailing error.
|
||||
unsafe {
|
||||
bridge::codec::oakcodec_encoder_flush(self.encoder);
|
||||
if let Err(_) = encoder.flush() {
|
||||
// Fall through to the error read below (the flush error is
|
||||
// surfaced through `get_error()` like the C ABI did).
|
||||
}
|
||||
let err = encoder_error(self.encoder);
|
||||
let err = encoder.get_error();
|
||||
if !err.is_empty() {
|
||||
task.set_error(&err);
|
||||
return Err(Error::Failed("Encoder flush failed".to_string()));
|
||||
@@ -270,94 +282,55 @@ impl TaskBehavior for ExportTask {
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
fn frame_downloaded(&mut self, task: &mut Task, frame: &Texture) -> Result<()> {
|
||||
let Some(encoder) = &self.encoder else {
|
||||
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);
|
||||
};
|
||||
let codec_frame = Self::to_codec_frame(frame)?;
|
||||
if let Err(_) = encoder.write_video(&codec_frame) {
|
||||
let err = encoder.get_error();
|
||||
task.set_error(&err);
|
||||
return Err(Error::Failed("Failed to write video frame".to_string()));
|
||||
return Err(Error::Failed("Failed to write 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.
|
||||
// Progress is reported by the render loop itself (native signalling).
|
||||
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);
|
||||
fn audio_downloaded(
|
||||
&mut self,
|
||||
task: &mut Task,
|
||||
samples: &oakrender::ticket::AudioSamples,
|
||||
) -> Result<()> {
|
||||
let Some(encoder) = &self.encoder else {
|
||||
return Ok(());
|
||||
};
|
||||
let frame_count = if samples.channel_count > 0 {
|
||||
samples.samples.len() / samples.channel_count as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Err(_) = encoder.write_audio(&samples.samples, frame_count as i32) {
|
||||
let err = encoder.get_error();
|
||||
task.set_error(&err);
|
||||
return Err(Error::Failed("Failed to write audio".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()> {
|
||||
if !self.encoding_params.subtitles_enabled || self.subtitle_encoder.ctx.is_null() {
|
||||
if !self.encoding_params.subtitles_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(encoder) = &self.encoder else {
|
||||
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);
|
||||
if let Err(_) = encoder.write_subtitle(text, 0.0, 0.0) {
|
||||
let err = encoder.get_error();
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,94 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -40,10 +40,6 @@ pub struct RefBox<T: ?Sized> {
|
||||
/// 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)
|
||||
|
||||
@@ -16,29 +16,24 @@
|
||||
|
||||
//! # 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`.
|
||||
//! Reimplements the C++ task module (`src/task/src`) as direct Rust
|
||||
//! (single-lib unification): inheritance → one module per class + trait
|
||||
//! objects, cancellation via `oakcommon::cancelatom::CancelAtom`, events
|
||||
//! as boxed callbacks. All node-graph, timeline and render work goes
|
||||
//! through the direct `oaknode` / `oakrender` Rust APIs
|
||||
//! ([`nodeops`] holds the graph operations the tasks share).
|
||||
|
||||
#![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 nodeops;
|
||||
pub mod precache;
|
||||
pub mod project;
|
||||
pub mod proxy;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,67 +16,68 @@
|
||||
|
||||
//! `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`).
|
||||
//! Renders a footage node through [`crate::render::RenderTask`] to fill
|
||||
//! the playback cache. The footage and its sequence are now
|
||||
//! [`crate::nodeops::NodeRef`] domain references (the deleted oaknode C
|
||||
//! ABI stubs are gone); video params come from the sequence's parameter
|
||||
//! streams and the cache range is the full footage video length.
|
||||
//!
|
||||
//! **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.
|
||||
//! **Simplifications over the C++**: the deep project copy / viewer
|
||||
//! wiring is not built (the render drives the given footage directly —
|
||||
//! the ticket carries the footage filename and the footage node's
|
||||
//! identity as the cache key) and the timeline work-area intersection is
|
||||
//! replaced by the full footage length. The direct ticket arena's eval
|
||||
//! producer does not persist frames to the frame cache yet, so this
|
||||
//! render pass warms nothing on disk; the plumbing is in place.
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/precache/precachetask.h
|
||||
|
||||
use crate::bridge;
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::handle::CHandle;
|
||||
use crate::nodeops::{self, NodeRef};
|
||||
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.
|
||||
/// A pre-cache task: renders frames 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,
|
||||
/// The footage being cached.
|
||||
pub footage: NodeRef,
|
||||
/// Frame index within the footage being cached.
|
||||
pub index: i32,
|
||||
/// The sequence node (borrowed `OakNodeSequence`).
|
||||
pub sequence: CHandle,
|
||||
/// The sequence context the footage lives in.
|
||||
pub sequence: NodeRef,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/// `sequence`. The old `footage: CHandle` / `sequence: CHandle`
|
||||
/// signature is replaced by domain [`NodeRef`]s (single-lib
|
||||
/// unification).
|
||||
pub fn new(footage: NodeRef, index: i32, sequence: NodeRef) -> PreCacheTask {
|
||||
// The sequence's video params drive the render timebase (the
|
||||
// deleted `oaknode_sequence_get_video_params` stub is replaced by
|
||||
// the direct behavior query).
|
||||
let video_params: Option<VideoParams> =
|
||||
nodeops::sequence_video_params(&sequence.0, sequence.1, 0);
|
||||
|
||||
let filename = footage_filename(footage);
|
||||
// The footage filename labels the task (the deleted
|
||||
// `oaknode_footage_filename` stub is replaced by the direct
|
||||
// behavior query).
|
||||
let filename = nodeops::footage_filename(&footage.0, footage.1);
|
||||
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,
|
||||
);
|
||||
let base = Task::new(&title, None);
|
||||
|
||||
// A scratch project for the render color manager (simplified).
|
||||
let project = unsafe { bridge::node::oaknode_project_init() };
|
||||
// The render target is the footage node itself (pre-cache fills
|
||||
// that footage's frame cache).
|
||||
let render = RenderTask::new(base, video_params, footage.clone(), Default::default(), None);
|
||||
|
||||
PreCacheTask {
|
||||
render,
|
||||
project,
|
||||
footage,
|
||||
index,
|
||||
sequence,
|
||||
@@ -90,59 +91,42 @@ impl TaskBehavior for PreCacheTask {
|
||||
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));
|
||||
// work-area intersection). The deleted `oaknode_footage_get_video_length`
|
||||
// stub is replaced by the direct behavior query.
|
||||
let length = nodeops::node_length(&self.footage.0, self.footage.1);
|
||||
let range = TimeRange::new(Rational::new(0, 1), length);
|
||||
|
||||
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);
|
||||
// No color manager / frame-cache handles exist on the direct
|
||||
// ticket path (the arena keys the cache by the footage node
|
||||
// identity — see `RenderTask::build_video_ticket`).
|
||||
self.render.set_render_inputs(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.
|
||||
// avoid a self-referential borrow and put back before returning.
|
||||
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.
|
||||
fn frame_downloaded(&mut self, task: &mut Task, frame: &oakrender::texture::Texture) -> Result<()> {
|
||||
// Do nothing: pre-cache just fills the frame cache (the direct
|
||||
// ticket arena records the render through the ticket's cache
|
||||
// identity; see the module docs).
|
||||
let _ = (task, frame);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn audio_downloaded(&mut self, task: &mut Task, buffer: CHandle) -> Result<()> {
|
||||
fn audio_downloaded(
|
||||
&mut self,
|
||||
task: &mut Task,
|
||||
buffer: &oakrender::ticket::AudioSamples,
|
||||
) -> Result<()> {
|
||||
// Pre-cache doesn't cache any audio.
|
||||
let _ = (task, buffer);
|
||||
Ok(())
|
||||
@@ -153,21 +137,3 @@ impl RenderTaskBehavior for PreCacheTask {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
//! 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).
|
||||
//! These tasks move data through the direct oaknode domain model
|
||||
//! (`Arc<Mutex<oaknode::project::Project>>` + `oaknode::id::NodeId`);
|
||||
//! they borrow the project while running and hand ownership over on the
|
||||
//! `take_*` accessors (architectural decision #5 in README.md).
|
||||
|
||||
pub mod format;
|
||||
pub mod import;
|
||||
|
||||
@@ -17,17 +17,27 @@
|
||||
//! `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.
|
||||
//! [`oakundo::undocommand::UndoCommand`] (taken via `take_command()`),
|
||||
//! tracks per-file import failures, and supports an optional image-sequence
|
||||
//! confirmation callback.
|
||||
//!
|
||||
//! **Single-lib note**: the node-graph manipulation went through the
|
||||
//! deleted oaknode C ABI; it now goes through the direct oaknode domain
|
||||
//! operations in [`crate::nodeops`] (folder/footage creation, probing via
|
||||
//! the oakcodec decoder registry, undo command construction). The folder
|
||||
//! and project are domain references (`Arc<Mutex<Project>>` + `NodeId`)
|
||||
//! instead of borrowed `CHandle`s.
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/project/import/import.h
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::bridge;
|
||||
use oakcommon::configstore::ConfigStore;
|
||||
use oakcommon::videoparams::VideoType;
|
||||
use oakundo::undocommand::UndoCommand;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ffi::taskhandle::cstr;
|
||||
use crate::handle::CHandle;
|
||||
use crate::nodeops::{self, NodeRef, ProjectRef};
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
|
||||
/// Callback used to confirm whether a detected image sequence should be
|
||||
@@ -39,18 +49,18 @@ pub type ImageSequenceConfirmFn = Box<dyn FnMut(&str, &str) -> bool + Send>;
|
||||
pub struct ProjectImportTask {
|
||||
/// The shared task base.
|
||||
pub base: Task,
|
||||
/// Destination folder (borrowed `OakNodeFolder`).
|
||||
pub folder: CHandle,
|
||||
/// Destination project (borrowed `OakNodeProject`).
|
||||
pub project: CHandle,
|
||||
/// Destination folder (project + folder node id).
|
||||
pub folder: NodeRef,
|
||||
/// Destination project.
|
||||
pub project: ProjectRef,
|
||||
/// Media filenames to import.
|
||||
pub filenames: Vec<String>,
|
||||
/// The undo command produced by the task, taken via `take_command`.
|
||||
command: Option<CHandle>,
|
||||
command: Option<UndoCommand>,
|
||||
/// Files that failed to import.
|
||||
invalid_files: Vec<String>,
|
||||
/// Imported footage, taken via `get_imported_footage`.
|
||||
imported_footage: Vec<CHandle>,
|
||||
imported_footage: Vec<NodeRef>,
|
||||
/// Optional image-sequence confirmation callback.
|
||||
image_sequence_confirm: Option<ImageSequenceConfirmFn>,
|
||||
/// Total number of files to import (directories counted recursively).
|
||||
@@ -59,35 +69,15 @@ pub struct ProjectImportTask {
|
||||
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.
|
||||
/// The old `folder: CHandle` / `project: CHandle` signature is replaced
|
||||
/// by the domain [`NodeRef`] folder and [`ProjectRef`] project
|
||||
/// (single-lib unification).
|
||||
pub fn new(
|
||||
base: Task,
|
||||
folder: CHandle,
|
||||
project: CHandle,
|
||||
folder: NodeRef,
|
||||
project: ProjectRef,
|
||||
filenames: Vec<String>,
|
||||
image_sequence_confirm: Option<ImageSequenceConfirmFn>,
|
||||
file_count: usize,
|
||||
@@ -108,7 +98,7 @@ impl ProjectImportTask {
|
||||
|
||||
/// 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> {
|
||||
pub fn take_command(&mut self) -> Result<UndoCommand> {
|
||||
self.command.take().ok_or(Error::State)
|
||||
}
|
||||
|
||||
@@ -122,18 +112,11 @@ impl ProjectImportTask {
|
||||
!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)
|
||||
/// The imported footage at `index` as a domain [`NodeRef`];
|
||||
/// `Err(Error::NotFound)` if out of range. (The deleted C ABI handed
|
||||
/// out addref'd handles; the domain reference is a plain clone.)
|
||||
pub fn get_imported_footage(&self, index: usize) -> Result<NodeRef> {
|
||||
self.imported_footage.get(index).cloned().ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
/// Total number of imported footage entries.
|
||||
@@ -151,19 +134,15 @@ impl ProjectImportTask {
|
||||
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,
|
||||
folder: NodeRef,
|
||||
entries: &mut Vec<String>,
|
||||
counter: &mut usize,
|
||||
parent_command: CHandle,
|
||||
parent_command: &mut UndoCommand,
|
||||
) {
|
||||
let atom = task.get_cancel_atom();
|
||||
let mut i = 0;
|
||||
while i < entries.len() {
|
||||
if task.is_cancelled() {
|
||||
@@ -183,24 +162,17 @@ impl ProjectImportTask {
|
||||
};
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
if let Some(folder_id) = nodeops::folder_create(&self.project) {
|
||||
nodeops::set_node_label(&self.project, folder_id, &basename_of(file_path));
|
||||
self.add_item_to_folder(
|
||||
folder,
|
||||
unsafe { bridge::node::oaknode_folder_as_node(folder_handle) },
|
||||
folder.clone(),
|
||||
(self.project.clone(), folder_id),
|
||||
parent_command,
|
||||
);
|
||||
let mut sub_entries = entry_list;
|
||||
self.import(
|
||||
task,
|
||||
folder_handle,
|
||||
(self.project.clone(), folder_id),
|
||||
&mut sub_entries,
|
||||
counter,
|
||||
parent_command,
|
||||
@@ -208,62 +180,47 @@ impl ProjectImportTask {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let footage =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };
|
||||
if footage.ctx.is_null() {
|
||||
let Some(footage) = nodeops::footage_create(&self.project, None) else {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
// Mirror the C++ cancel-atom dance around the probe: the
|
||||
// footage behavior's own cancellation flag tracks the
|
||||
// task's atom during the probe and is cleared afterwards.
|
||||
nodeops::footage_set_cancelled(&self.project, footage, true);
|
||||
nodeops::footage_set_cancelled(&self.project, footage, atom.is_cancelled());
|
||||
let ok = nodeops::footage_set_filename(&self.project, footage, file_path);
|
||||
nodeops::footage_set_cancelled(&self.project, footage, false);
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
if ok && nodeops::footage_is_valid(&self.project, footage) {
|
||||
nodeops::set_node_label(&self.project, footage, &basename_of(file_path));
|
||||
|
||||
// See if this footage is an image sequence.
|
||||
self.validate_image_sequence(task, footage, entries, i);
|
||||
self.validate_image_sequence(
|
||||
task,
|
||||
(self.project.clone(), 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) },
|
||||
folder.clone(),
|
||||
(self.project.clone(), footage),
|
||||
parent_command,
|
||||
);
|
||||
|
||||
self.imported_footage.push(footage);
|
||||
self.imported_footage.push((self.project.clone(), 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);
|
||||
}
|
||||
}
|
||||
let mut remove =
|
||||
nodeops::remove_node_command(self.project.clone(), footage);
|
||||
remove.redo_now();
|
||||
}
|
||||
|
||||
*counter += 1;
|
||||
@@ -273,30 +230,25 @@ impl ProjectImportTask {
|
||||
}
|
||||
}
|
||||
|
||||
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 add_item_to_folder(&self, folder: NodeRef, item: NodeRef, command: &mut UndoCommand) {
|
||||
let child = nodeops::folder_add_child_command(folder, item);
|
||||
command.multi_add_child(child);
|
||||
}
|
||||
|
||||
fn validate_image_sequence(
|
||||
&mut self,
|
||||
task: &mut Task,
|
||||
footage: CHandle,
|
||||
footage: NodeRef,
|
||||
info_list: &mut Vec<String>,
|
||||
index: usize,
|
||||
) {
|
||||
let filename = footage_filename(footage);
|
||||
let filename = nodeops::footage_filename(&footage.0, footage.1);
|
||||
if filename.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let digit_count = unsafe {
|
||||
bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(&filename))
|
||||
};
|
||||
// Direct oakcodec calls (single-lib unification).
|
||||
let digit_count = oakcodec::decoder::get_image_sequence_digit_count(&filename);
|
||||
if digit_count <= 0 {
|
||||
return;
|
||||
}
|
||||
@@ -309,41 +261,35 @@ impl ProjectImportTask {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.item_is_still_image_footage_only(footage) {
|
||||
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
|
||||
{
|
||||
let Some(mut video_stream) = nodeops::footage_video_params(&footage.0, footage.1, 0)
|
||||
else {
|
||||
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 width = video_stream.width();
|
||||
let height = video_stream.height();
|
||||
|
||||
let seq_index =
|
||||
unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(&filename)) };
|
||||
// Direct oakcodec call (single-lib unification).
|
||||
let seq_index = oakcodec::decoder::get_image_sequence_index(&filename);
|
||||
|
||||
let prev_fn = transform_sequence_filename(&filename, seq_index - 1, digit_count);
|
||||
let next_fn = transform_sequence_filename(&filename, seq_index + 1, digit_count);
|
||||
|
||||
let previous_file =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&prev_fn)) };
|
||||
let next_file =
|
||||
unsafe { bridge::node::oaknode_footage_create(self.project, cstr(&next_fn)) };
|
||||
let previous_file = nodeops::footage_create(&self.project, Some(&prev_fn));
|
||||
let next_file = nodeops::footage_create(&self.project, Some(&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);
|
||||
let prev_matches = previous_file.is_some_and(|f| {
|
||||
nodeops::footage_set_filename(&self.project, f, &prev_fn)
|
||||
&& self.compare_still_image_size(&(self.project.clone(), f), width, height)
|
||||
});
|
||||
let next_matches = next_file.is_some_and(|f| {
|
||||
nodeops::footage_set_filename(&self.project, f, &next_fn)
|
||||
&& self.compare_still_image_size(&(self.project.clone(), f), width, height)
|
||||
});
|
||||
|
||||
if prev_matches || next_matches {
|
||||
// Ask the user whether this is really a sequence (default: no).
|
||||
@@ -376,179 +322,86 @@ impl ProjectImportTask {
|
||||
}
|
||||
|
||||
if is_sequence {
|
||||
unsafe {
|
||||
bridge::common::oakcommon_videoparams_set_video_type(
|
||||
video_stream,
|
||||
bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE,
|
||||
);
|
||||
video_stream.set_video_type(VideoType::ImageSequence);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
// Direct config read (single-lib unification).
|
||||
if let Ok(rate) = ConfigStore::instance().get(None, "DefaultSequenceFrameRate") {
|
||||
if let Some((num, den)) = parse_rational(&rate) {
|
||||
if den != 0 {
|
||||
video_stream.set_time_base(num, den);
|
||||
video_stream.set_frame_rate(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);
|
||||
video_stream.set_start_time(start_index);
|
||||
video_stream.set_duration(end_index - start_index + 1);
|
||||
nodeops::footage_set_video_params(&footage.0, footage.1, 0, &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);
|
||||
}
|
||||
}
|
||||
}
|
||||
for probe in [previous_file, next_file].into_iter().flatten() {
|
||||
let mut remove = nodeops::remove_node_command(self.project.clone(), probe);
|
||||
remove.redo_now();
|
||||
}
|
||||
|
||||
let _ = task;
|
||||
}
|
||||
|
||||
fn item_is_still_image_footage_only(&self, footage: CHandle) -> bool {
|
||||
if unsafe { bridge::node::oaknode_footage_total_stream_count(footage) } != 1 {
|
||||
fn item_is_still_image_footage_only(&self, footage: &NodeRef) -> bool {
|
||||
// The oaknode domain footage records probed streams; a single
|
||||
// stream is the closest domain equivalent of the C++
|
||||
// `total_stream_count == 1` check. The oaknode video params carry
|
||||
// no `video_type`, so the `kVideoTypeStill` check collapses to
|
||||
// "one stream with valid dimensions".
|
||||
if nodeops::footage_total_stream_count(&footage.0, footage.1) != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut vp = CHandle::null();
|
||||
if unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, &mut vp) } != 0 {
|
||||
let Some(vp) = nodeops::footage_video_params(&footage.0, footage.1, 0) else {
|
||||
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
|
||||
vp.is_valid() && vp.video_type() == VideoType::Video
|
||||
}
|
||||
|
||||
fn compare_still_image_size(&self, footage: CHandle, width: i32, height: i32) -> bool {
|
||||
fn compare_still_image_size(&self, footage: &NodeRef, 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 {
|
||||
let Some(stream) = nodeops::footage_video_params(&footage.0, footage.1, 0) else {
|
||||
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
|
||||
stream.width() == width && stream.height() == 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.
|
||||
/// Import each filename via the direct oaknode domain operations
|
||||
/// ([`crate::nodeops`]), 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 command = UndoCommand::multi();
|
||||
|
||||
let mut counter = 0;
|
||||
let mut entries = self.filenames.clone();
|
||||
self.import(task, self.folder, &mut entries, &mut counter, command);
|
||||
let folder = (self.folder.0.clone(), self.folder.1);
|
||||
self.import(task, folder, &mut entries, &mut counter, &mut 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);
|
||||
}
|
||||
self.command = Some(command);
|
||||
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()
|
||||
@@ -556,20 +409,6 @@ fn basename_of(path: &str) -> String {
|
||||
.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 {
|
||||
@@ -613,15 +452,3 @@ fn parse_rational(s: &str) -> Option<(i32, i32)> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -17,26 +17,23 @@
|
||||
//! `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.
|
||||
//! Loads an `.oakproj` file into a new `oaknode::project::Project`. The
|
||||
//! base task holds the filename and produces the project on
|
||||
//! `take_project()`; `ProjectLoadTask` is the concrete (OTIO-less) loader.
|
||||
//! The project itself is loaded through the direct Rust serializer
|
||||
//! (`oaknode::serializer::load` — single-lib unification; the old oaknode
|
||||
//! serializer C ABI with its per-code result mapping is gone, so the
|
||||
//! version/result-code ladder collapses into the error message the XML
|
||||
//! path used).
|
||||
//!
|
||||
//! 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};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// 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;
|
||||
use oaknode::project::Project;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
|
||||
/// The base project-load task: owns a source filename and yields a loaded
|
||||
/// project via [`ProjectLoadBaseTask::take_project`].
|
||||
@@ -46,20 +43,7 @@ pub struct ProjectLoadBaseTask {
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loaded_project: Option<Arc<Mutex<Project>>>,
|
||||
}
|
||||
|
||||
impl ProjectLoadBaseTask {
|
||||
@@ -74,83 +58,49 @@ impl ProjectLoadBaseTask {
|
||||
|
||||
/// 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> {
|
||||
pub fn take_project(&mut self) -> Result<Arc<Mutex<Project>>> {
|
||||
self.loaded_project.take().ok_or(Error::State)
|
||||
}
|
||||
|
||||
/// Store the loaded project (called by `run`).
|
||||
pub(crate) fn store_project(&mut self, project: CHandle) {
|
||||
pub(crate) fn store_project(&mut self, project: Arc<Mutex<Project>>) {
|
||||
self.loaded_project = Some(project);
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskBehavior for ProjectLoadBaseTask {
|
||||
/// Load the project from `filename` via the oaknode serializer
|
||||
/// (`bridge::node`), storing the resulting `OakNodeProject`.
|
||||
/// Load the project from `filename` via the direct oaknode serializer
|
||||
/// (`oaknode::serializer::load`), storing the resulting project.
|
||||
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 => {
|
||||
let xml = match std::fs::read_to_string(&self.filename) {
|
||||
Ok(xml) => xml,
|
||||
Err(e) => {
|
||||
task.set_error(&format!(
|
||||
"Failed to read file \"{}\" for reading.",
|
||||
self.filename
|
||||
));
|
||||
let _ = e;
|
||||
return Err(Error::Failed("Failed to load project".to_string()));
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_XML_ERROR => {
|
||||
};
|
||||
|
||||
let project = match oaknode::serializer::load(&xml) {
|
||||
Ok(project) => project,
|
||||
Err(e) => {
|
||||
task.set_error(&format!(
|
||||
"Failed to read XML document. File may be corrupt. Error was: {}",
|
||||
buf_to_string(&details)
|
||||
"Failed to read XML document. File may be corrupt. Error was: {e}"
|
||||
));
|
||||
return Err(Error::Failed("Failed to load project".to_string()));
|
||||
}
|
||||
OAKNODE_SERIALIZER_RESULT_NO_DATA => {
|
||||
task.set_error("Failed to find any data to parse.");
|
||||
}
|
||||
_ => task.set_error("Unknown error."),
|
||||
};
|
||||
|
||||
{
|
||||
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.set_filename(&self.filename);
|
||||
}
|
||||
|
||||
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()))
|
||||
self.store_project(project);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +115,3 @@ impl TaskBehavior for ProjectLoadTask {
|
||||
<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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@
|
||||
//! `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
|
||||
//! project, 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.
|
||||
//! `oakotio` binding (see `README` decision #6).
|
||||
//!
|
||||
//! **Single-lib note**: the project is built through the direct oaknode
|
||||
//! domain operations in [`crate::nodeops`] (`Project::new()` +
|
||||
//! `Project::initialize()`, factory-created sequence/folder/footage/
|
||||
//! block/track nodes, graph connections) instead of the deleted oaknode /
|
||||
//! oaktimeline C ABIs. Track creation uses the task-local
|
||||
//! [`crate::nodeops::add_track_command`] (see its docs for the
|
||||
//! oaktimeline-migration note). The loaded project is an
|
||||
//! `Arc<Mutex<oaknode::project::Project>>` stored on the base task
|
||||
//! (`take_project()`).
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/project/loadotio/loadotio.cpp
|
||||
|
||||
@@ -30,12 +38,12 @@ use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oaknode::id::NodeId;
|
||||
use oaknode::track::TrackType;
|
||||
use oakotio::Serializable;
|
||||
|
||||
use crate::bridge;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ffi::taskhandle::cstr;
|
||||
use crate::handle::CHandle;
|
||||
use crate::nodeops::{self, NodeRef, ProjectRef};
|
||||
use crate::project::format::InterchangeFormat;
|
||||
use crate::project::load::ProjectLoadBaseTask;
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
@@ -92,32 +100,34 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
|
||||
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);
|
||||
// Build the project directly through the oaknode domain model
|
||||
// (the deleted `oaknode_project_init` stub is gone).
|
||||
let project: ProjectRef = oaknode::project::Project::new();
|
||||
{
|
||||
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Err(e) = guard.initialize() {
|
||||
let _ = e;
|
||||
task.set_error("Failed to create project");
|
||||
return Err(Error::Failed("Failed to create project".to_string()));
|
||||
}
|
||||
guard.set_modified(true);
|
||||
}
|
||||
|
||||
// Keep track of imported footage
|
||||
let mut imported_footage: HashMap<String, CHandle> = HashMap::new();
|
||||
let mut imported_footage: HashMap<String, NodeRef> = 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();
|
||||
let mut sequences: Vec<NodeRef> = 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() {
|
||||
let Some(sequence) = nodeops::sequence_create(&project) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let label = if !timeline.name().is_empty() {
|
||||
timeline.name().to_string()
|
||||
@@ -127,17 +137,7 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
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);
|
||||
}
|
||||
nodeops::set_node_label(&project, sequence, &label);
|
||||
|
||||
// Get number of clips for loading bar
|
||||
for track in timeline.tracks().children() {
|
||||
@@ -146,7 +146,7 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
}
|
||||
}
|
||||
|
||||
sequences.push(sequence);
|
||||
sequences.push((project.clone(), sequence));
|
||||
}
|
||||
if number_of_clips <= 0.0 {
|
||||
number_of_clips = 1.0;
|
||||
@@ -156,7 +156,7 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// default accepts everything).
|
||||
let sequence_names: Vec<String> = sequences
|
||||
.iter()
|
||||
.map(|s| node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*s) }))
|
||||
.map(|(p, s)| nodeops::node_label(p, *s))
|
||||
.collect();
|
||||
let mut confirm = CONFIRM_CALLBACK.lock().unwrap().take();
|
||||
let accepted = match confirm.as_mut() {
|
||||
@@ -167,62 +167,35 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
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).
|
||||
// base task (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 root_folder = {
|
||||
let guard = project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.root
|
||||
};
|
||||
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) };
|
||||
for (timeline, (_, sequence)) in timelines.iter().zip(&sequences) {
|
||||
let sequence_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);
|
||||
}
|
||||
}
|
||||
let mut add_seq = nodeops::folder_add_child_command(
|
||||
(project.clone(), root_folder),
|
||||
(project.clone(), sequence_node),
|
||||
);
|
||||
add_seq.redo_now();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
let sequence_footage = nodeops::folder_create(&project);
|
||||
if let Some(folder_id) = sequence_footage {
|
||||
nodeops::set_node_label(&project, folder_id, timeline.name());
|
||||
let mut add_folder = nodeops::folder_add_child_command(
|
||||
(project.clone(), root_folder),
|
||||
(project.clone(), folder_id),
|
||||
);
|
||||
add_folder.redo_now();
|
||||
}
|
||||
|
||||
// Iterate through tracks
|
||||
@@ -233,8 +206,8 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
|
||||
// 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,
|
||||
"Video" => TrackType::Video,
|
||||
"Audio" => TrackType::Audio,
|
||||
other => {
|
||||
eprintln!("Found unknown track type: {other}");
|
||||
continue;
|
||||
@@ -242,43 +215,27 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
};
|
||||
|
||||
// Create a new track
|
||||
let mut track_list = CHandle::null();
|
||||
unsafe {
|
||||
bridge::node::oaknode_sequence_get_track_list(
|
||||
*sequence,
|
||||
track_type,
|
||||
&mut track_list,
|
||||
);
|
||||
}
|
||||
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() {
|
||||
let Some(track_list) =
|
||||
nodeops::sequence_track_list(&project, sequence_node, track_type)
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut add_track =
|
||||
nodeops::add_track_command(project.clone(), track_list);
|
||||
add_track.redo_now();
|
||||
|
||||
let track_count = nodeops::tracklist_track_count(&project, track_list);
|
||||
let track = if track_count > 0 {
|
||||
nodeops::tracklist_track_at(&project, track_list, track_count - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(track) = track else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get clips from track
|
||||
let mut previous_block = CHandle::null();
|
||||
let mut previous_block: Option<NodeId> = None;
|
||||
let mut prev_block_transition = false;
|
||||
|
||||
for otio_block in otio_track.children() {
|
||||
@@ -286,35 +243,27 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
break;
|
||||
}
|
||||
|
||||
let block = match otio_block.schema_name() {
|
||||
"Clip" => unsafe { bridge::node::oaknode_block_clip_create() },
|
||||
"Gap" => unsafe { bridge::node::oaknode_block_gap_create() },
|
||||
let block_kind = match otio_block.schema_name() {
|
||||
"Clip" => nodeops::BlockKind::Clip,
|
||||
"Gap" => nodeops::BlockKind::Gap,
|
||||
"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,
|
||||
)
|
||||
}
|
||||
nodeops::BlockKind::Transition
|
||||
}
|
||||
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() }
|
||||
nodeops::BlockKind::Gap
|
||||
}
|
||||
};
|
||||
if block.ctx.is_null() {
|
||||
let Some(block) = nodeops::block_create(&project, block_kind) else {
|
||||
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);
|
||||
}
|
||||
};
|
||||
let block_node = block;
|
||||
nodeops::set_node_label(&project, block_node, otio_block.name());
|
||||
nodeops::track_append_block(&project, track, block_node);
|
||||
|
||||
if otio_block.schema_name() == "Clip" || otio_block.schema_name() == "Gap" {
|
||||
if let Some(source_range) = otio_block.source_range() {
|
||||
@@ -325,32 +274,31 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
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,
|
||||
nodeops::clip_set_media_in(
|
||||
&project,
|
||||
block_node,
|
||||
start_time.numerator(),
|
||||
start_time.denominator(),
|
||||
);
|
||||
}
|
||||
nodeops::block_set_length_and_media_out(
|
||||
&project,
|
||||
block_node,
|
||||
duration.numerator(),
|
||||
duration.denominator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the previous block was a transition, connect the
|
||||
// current block to it.
|
||||
if prev_block_transition {
|
||||
unsafe {
|
||||
bridge::node::oaknode_node_connect(
|
||||
if let Some(prev) = previous_block {
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
block_node,
|
||||
bridge::node::oaknode_block_as_node(previous_block),
|
||||
cstr(bridge::node::OAKNODE_TRANSITION_IN_BLOCK_INPUT),
|
||||
prev,
|
||||
nodeops::TRANSITION_IN_BLOCK_INPUT,
|
||||
);
|
||||
}
|
||||
prev_block_transition = false;
|
||||
@@ -361,43 +309,37 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
nodeops::transition_set_offsets_and_length(
|
||||
&project,
|
||||
block_node,
|
||||
in_offset.numerator(),
|
||||
in_offset.denominator(),
|
||||
out_offset.numerator(),
|
||||
out_offset.denominator(),
|
||||
);
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
if let Some(prev) = previous_block {
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
prev,
|
||||
block_node,
|
||||
nodeops::TRANSITION_OUT_BLOCK_INPUT,
|
||||
);
|
||||
}
|
||||
prev_block_transition = true;
|
||||
|
||||
// Position transition in its own context.
|
||||
unsafe {
|
||||
set_own_context_position(block_node);
|
||||
}
|
||||
set_own_context_position(&project, block_node);
|
||||
}
|
||||
|
||||
if otio_block.schema_name() == "Gap" {
|
||||
// Position gap in its own context.
|
||||
unsafe {
|
||||
set_own_context_position(block_node);
|
||||
}
|
||||
set_own_context_position(&project, block_node);
|
||||
}
|
||||
|
||||
// Update this after it's used but before any continue
|
||||
// statements.
|
||||
previous_block = block;
|
||||
previous_block = Some(block_node);
|
||||
|
||||
if otio_block.schema_name() == "Clip" {
|
||||
let Some(otio_clip) = otio_block.as_clip() else {
|
||||
@@ -410,115 +352,90 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
// Link footage
|
||||
let footage_url = external.target_url().to_string();
|
||||
|
||||
let probed_item = if let Some(existing) =
|
||||
imported_footage.get(&footage_url)
|
||||
{
|
||||
*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 probed_item: Option<NodeRef> =
|
||||
if let Some(existing) = imported_footage.get(&footage_url) {
|
||||
Some(existing.clone())
|
||||
} else {
|
||||
let created =
|
||||
nodeops::footage_create(&project, Some(&footage_url));
|
||||
if let Some(created) = created {
|
||||
imported_footage.insert(
|
||||
footage_url.clone(),
|
||||
(project.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),
|
||||
let label = Path::new(&footage_url)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
nodeops::set_node_label(&project, created, &label);
|
||||
|
||||
if let Some(folder_id) = sequence_footage {
|
||||
let mut add_footage =
|
||||
nodeops::folder_add_child_command(
|
||||
(project.clone(), folder_id),
|
||||
(project.clone(), created),
|
||||
);
|
||||
add_footage.redo_now();
|
||||
}
|
||||
}
|
||||
created.map(|id| (project.clone(), id))
|
||||
};
|
||||
|
||||
if let Some((_, probed_id)) = probed_item {
|
||||
// Position clip in its own context.
|
||||
set_own_context_position(&project, block_node);
|
||||
|
||||
// Position footage in its context.
|
||||
nodeops::node_set_context_position(
|
||||
&project, block_node, probed_id, -2.0, 0.0, false,
|
||||
);
|
||||
|
||||
// Record the clip-footage link in the domain
|
||||
// model (the C++ finds footage through the
|
||||
// input chain; the Rust clip records it).
|
||||
nodeops::clip_set_footage(&project, block_node, probed_id);
|
||||
|
||||
if track_type == TrackType::Video {
|
||||
if let Some(transform) = factory_create(
|
||||
&project,
|
||||
nodeops::TRANSFORM_TYPE_ID,
|
||||
) {
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
probed_id,
|
||||
transform,
|
||||
"tex_in",
|
||||
);
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
transform,
|
||||
block_node,
|
||||
nodeops::CLIP_TEXTURE_INPUT,
|
||||
);
|
||||
nodeops::node_set_context_position(
|
||||
&project, block_node, transform, -1.0, 0.0, false,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
if let Some(volume) =
|
||||
factory_create(&project, nodeops::VOLUME_TYPE_ID)
|
||||
{
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
probed_id,
|
||||
volume,
|
||||
"samples_in",
|
||||
);
|
||||
nodeops::node_connect(
|
||||
&project,
|
||||
volume,
|
||||
block_node,
|
||||
nodeops::CLIP_TEXTURE_INPUT,
|
||||
);
|
||||
nodeops::node_set_context_position(
|
||||
&project, block_node, volume, -1.0, 0.0, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,12 +453,18 @@ impl TaskBehavior for LoadOTIOTask {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a node from the factory registry (`oaknode_factory_create_from_id`).
|
||||
fn factory_create(project: &ProjectRef, type_id: &str) -> Option<NodeId> {
|
||||
let meta = oaknode::factory::Factory::global().find(type_id)?;
|
||||
let (core, behavior) = (meta.create)();
|
||||
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
Some(guard.graph.add_node(core, behavior))
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
fn set_own_context_position(project: &ProjectRef, node: NodeId) {
|
||||
nodeops::node_set_context_position(project, node, node, 0.0, 0.0, false);
|
||||
}
|
||||
|
||||
/// Parse the document into one `oakotio::Timeline` per sequence, dispatching
|
||||
@@ -593,16 +516,3 @@ fn parse_timelines(
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@@ -16,35 +16,34 @@
|
||||
|
||||
//! `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.
|
||||
//! Serializes a borrowed `oaknode::project::Project` to an `.oakproj` file
|
||||
//! via the direct Rust serializer (`oaknode::serializer::save` —
|
||||
//! single-lib unification; the old oaknode serializer C ABI with its
|
||||
//! overwrite/compression result ladder is gone, so the overwrite-as-else
|
||||
//! path collapses into a plain write). Optionally writes to an override
|
||||
//! filename.
|
||||
//!
|
||||
//! 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};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// 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;
|
||||
use oaknode::project::Project;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
|
||||
/// 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,
|
||||
/// Borrowed project to save (`Arc<Mutex<Project>>`).
|
||||
pub project: Arc<Mutex<Project>>,
|
||||
/// Optional override filename; when empty the project's own filename is
|
||||
/// used.
|
||||
pub override_filename: Option<String>,
|
||||
/// Whether to compress the serialized output.
|
||||
/// Whether to compress the serialized output. The direct serializer has
|
||||
/// no compression mode (the old C ABI compression path is gone), so
|
||||
/// this flag is retained for API parity but ignored.
|
||||
pub use_compression: bool,
|
||||
}
|
||||
|
||||
@@ -56,12 +55,17 @@ impl ProjectSaveTask {
|
||||
}
|
||||
|
||||
impl TaskBehavior for ProjectSaveTask {
|
||||
/// Serialize the project via the oaknode serializer (`bridge::node`) to
|
||||
/// the resolved filename.
|
||||
/// Serialize the project via the direct oaknode serializer
|
||||
/// (`oaknode::serializer::save`) 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),
|
||||
None => self
|
||||
.project
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.filename()
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
if using_filename.is_empty() {
|
||||
@@ -71,60 +75,22 @@ impl TaskBehavior for ProjectSaveTask {
|
||||
));
|
||||
}
|
||||
|
||||
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 => {
|
||||
let xml = match oaknode::serializer::save(
|
||||
&self.project.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
) {
|
||||
Ok(xml) => xml,
|
||||
Err(e) => {
|
||||
let _ = e;
|
||||
task.set_error("Failed to write XML data.");
|
||||
return Err(Error::Failed("Failed to save project".to_string()));
|
||||
}
|
||||
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 let Err(e) = std::fs::write(&using_filename, xml) {
|
||||
task.set_error(&format!("Failed to open file for writing: {e}"));
|
||||
return Err(Error::Failed("Failed to save project".to_string()));
|
||||
}
|
||||
|
||||
if success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed("Failed to save project".to_string()))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@@ -16,28 +16,33 @@
|
||||
|
||||
//! `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
|
||||
//! Serializes a borrowed project 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.
|
||||
//! final write only — the serialization itself is shared.
|
||||
//!
|
||||
//! **Single-lib note**: the project graph is read through the direct
|
||||
//! oaknode domain operations in [`crate::nodeops`] (folder children,
|
||||
//! sequence track lists, track blocks, footage parameters) instead of the
|
||||
//! deleted oaknode C ABI stubs; the project is an
|
||||
//! `Arc<Mutex<oaknode::project::Project>>` instead of a borrowed `CHandle`.
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
use oaknode::id::NodeId;
|
||||
use oaknode::track::TrackType;
|
||||
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::nodeops::{self, ProjectRef};
|
||||
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
|
||||
@@ -50,8 +55,8 @@ fn rational_min() -> Rational {
|
||||
pub struct SaveOTIOTask {
|
||||
/// The shared task base.
|
||||
pub base: Task,
|
||||
/// Borrowed project to save (borrowed `OakNodeProject`).
|
||||
pub project: CHandle,
|
||||
/// Borrowed project to save (domain project).
|
||||
pub project: ProjectRef,
|
||||
/// Output OTIO filename.
|
||||
pub filename: String,
|
||||
}
|
||||
@@ -61,54 +66,35 @@ impl SaveOTIOTask {
|
||||
/// 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)
|
||||
}));
|
||||
fn serialize_timeline(project: &ProjectRef, sequence: NodeId) -> Option<Timeline> {
|
||||
let mut otio_timeline =
|
||||
Timeline::new(nodeops::node_label(project, sequence));
|
||||
|
||||
// Direct video-params value type (single-lib unification); absent
|
||||
// params leave rate at 0.0 → the documented `None` path.
|
||||
let (num, den) = nodeops::sequence_video_params(project, sequence, 0)
|
||||
.map(|vp| vp.frame_rate())
|
||||
.unwrap_or((0, 1));
|
||||
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 den != 0 {
|
||||
rate = num as f64 / den as f64;
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
let video_list = nodeops::sequence_track_list(project, sequence, TrackType::Video);
|
||||
let audio_list = nodeops::sequence_track_list(project, sequence, TrackType::Audio);
|
||||
|
||||
if !Self::serialize_track_list(video_list, &mut otio_timeline, rate)
|
||||
|| !Self::serialize_track_list(audio_list, &mut otio_timeline, rate)
|
||||
{
|
||||
return None;
|
||||
if let Some(list) = video_list {
|
||||
if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if let Some(list) = audio_list {
|
||||
if !Self::serialize_track_list(project, list, &mut otio_timeline, rate) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(otio_timeline)
|
||||
@@ -119,28 +105,18 @@ impl SaveOTIOTask {
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)
|
||||
fn serialize_track_list(
|
||||
list: CHandle,
|
||||
project: &ProjectRef,
|
||||
list: NodeId,
|
||||
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);
|
||||
}
|
||||
let track_count = nodeops::tracklist_track_count(project, list);
|
||||
|
||||
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 let Some(track) = nodeops::tracklist_track_at(project, list, i) {
|
||||
let length = nodeops::track_length(project, track);
|
||||
if length > max_track_length {
|
||||
max_track_length = length;
|
||||
}
|
||||
@@ -148,15 +124,12 @@ impl SaveOTIOTask {
|
||||
}
|
||||
|
||||
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 Some(track) = nodeops::tracklist_track_at(project, list, i) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length)
|
||||
let Some(otio_track) =
|
||||
Self::serialize_track(project, track, sequence_rate, max_track_length)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -175,99 +148,62 @@ impl SaveOTIOTask {
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)
|
||||
fn serialize_track(
|
||||
track: CHandle,
|
||||
project: &ProjectRef,
|
||||
track: NodeId,
|
||||
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 track_type = nodeops::track_type(project, track)?;
|
||||
|
||||
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}");
|
||||
TrackType::Video => "Video",
|
||||
TrackType::Audio => "Audio",
|
||||
TrackType::Subtitle => {
|
||||
eprintln!(
|
||||
"Don't know OTIO track kind for native type {}",
|
||||
track_type.to_c()
|
||||
);
|
||||
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);
|
||||
}
|
||||
let block_count = nodeops::track_block_count(project, track);
|
||||
|
||||
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() {
|
||||
let Some(block) = nodeops::track_block_at(project, track, i) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut kind = bridge::node::OAKNODE_BLOCK_OTHER;
|
||||
unsafe {
|
||||
bridge::node::oaknode_block_get_kind(block, &mut kind);
|
||||
}
|
||||
let kind = nodeops::block_kind(project, block);
|
||||
|
||||
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)
|
||||
}));
|
||||
nodeops::BlockKind::Clip => {
|
||||
let mut otio_clip = Clip::new(nodeops::node_label(project, 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),
|
||||
RationalTime::from_rational(block_in_of(project, block), sequence_rate),
|
||||
RationalTime::from_rational(block_length_of(project, 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
|
||||
{
|
||||
let media = nodeops::node_find_input_footage(project, block);
|
||||
if let Some(media) = media {
|
||||
let available_range = if track_type == TrackType::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (source_frame_rate, duration) =
|
||||
nodeops::footage_video_params(project, media, 0)
|
||||
.map(|vp| {
|
||||
let (num, den) = vp.frame_rate();
|
||||
let rate = if den != 0 {
|
||||
num as f64 / den as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(rate, vp.duration() as f64)
|
||||
})
|
||||
.unwrap_or((0.0, 0.0));
|
||||
TimeRange::new(
|
||||
RationalTime::new(0.0, source_frame_rate),
|
||||
RationalTime::new(duration, source_frame_rate),
|
||||
@@ -279,7 +215,7 @@ impl SaveOTIOTask {
|
||||
)
|
||||
};
|
||||
|
||||
let media_url = footage_filename(media);
|
||||
let media_url = nodeops::footage_filename(project, media);
|
||||
if !media_url.is_empty() {
|
||||
otio_clip.set_media_reference(MediaReference::ExternalReference(
|
||||
ExternalReference::new(media_url, Some(available_range)),
|
||||
@@ -289,32 +225,25 @@ impl SaveOTIOTask {
|
||||
|
||||
Some(Composable::Clip(otio_clip))
|
||||
}
|
||||
bridge::node::OAKNODE_BLOCK_GAP => Some(Composable::Gap(Gap::new(
|
||||
nodeops::BlockKind::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),
|
||||
RationalTime::from_rational(block_in_of(project, block), 24.0),
|
||||
RationalTime::from_rational(block_length_of(project, block), 24.0),
|
||||
),
|
||||
node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }),
|
||||
nodeops::node_label(project, block),
|
||||
))),
|
||||
bridge::node::OAKNODE_BLOCK_TRANSITION => {
|
||||
let mut otio_transition = Transition::new(node_label_of(unsafe {
|
||||
bridge::node::oaknode_block_as_node(block)
|
||||
}));
|
||||
nodeops::BlockKind::Transition => {
|
||||
let mut otio_transition =
|
||||
Transition::new(nodeops::node_label(project, 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,
|
||||
));
|
||||
let n = transition_offset_of(project, block, true);
|
||||
otio_transition.set_in_offset(RationalTime::from_rational(n, 24.0));
|
||||
let n = transition_offset_of(project, block, false);
|
||||
otio_transition.set_out_offset(RationalTime::from_rational(n, 24.0));
|
||||
|
||||
Some(Composable::Transition(otio_transition))
|
||||
}
|
||||
_ => None,
|
||||
nodeops::BlockKind::Other => None,
|
||||
};
|
||||
|
||||
let Some(otio_block) = otio_block else {
|
||||
@@ -364,25 +293,42 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
// 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() {
|
||||
let root = {
|
||||
let guard = self.project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.root
|
||||
};
|
||||
if !root.valid() {
|
||||
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);
|
||||
let mut sequences: Vec<NodeId> = Vec::new();
|
||||
{
|
||||
let guard = self.project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let Some(entry) = guard.graph.get(root) else {
|
||||
task.set_error("Project contains no sequences to export.");
|
||||
return Err(Error::Failed(
|
||||
"Project contains no sequences to export.".to_string(),
|
||||
));
|
||||
};
|
||||
let Some(folder) = entry
|
||||
.behavior
|
||||
.as_any()
|
||||
.and_then(|a| a.downcast_ref::<oaknode::folder::FolderBehavior>())
|
||||
else {
|
||||
task.set_error("Project contains no sequences to export.");
|
||||
return Err(Error::Failed(
|
||||
"Project contains no sequences to export.".to_string(),
|
||||
));
|
||||
};
|
||||
for child in &folder.children {
|
||||
if let Some(child_entry) = guard.graph.get(*child) {
|
||||
if child_entry.behavior.type_id() == nodeops::SEQUENCE_TYPE_ID {
|
||||
sequences.push(*child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,12 +341,12 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
|
||||
let mut serialized: Vec<Timeline> = Vec::with_capacity(sequences.len());
|
||||
for sequence in &sequences {
|
||||
match Self::serialize_timeline(*sequence) {
|
||||
match Self::serialize_timeline(&self.project, *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) })
|
||||
nodeops::node_label(&self.project, *sequence)
|
||||
));
|
||||
return Err(Error::Failed("Failed to serialize sequence".to_string()));
|
||||
}
|
||||
@@ -449,90 +395,23 @@ impl TaskBehavior for SaveOTIOTask {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
fn block_in_of(project: &ProjectRef, block: NodeId) -> Rational {
|
||||
nodeops::block_in(project, block)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
fn block_length_of(project: &ProjectRef, block: NodeId) -> Rational {
|
||||
nodeops::block_length(project, block)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
fn transition_offset_of(project: &ProjectRef, block: NodeId, in_offset: bool) -> Rational {
|
||||
if in_offset {
|
||||
unsafe {
|
||||
bridge::node::oaknode_transition_get_in_offset(block, &mut n, &mut d);
|
||||
}
|
||||
nodeops::transition_in_offset(project, block)
|
||||
} else {
|
||||
unsafe {
|
||||
bridge::node::oaknode_transition_get_out_offset(block, &mut n, &mut d);
|
||||
}
|
||||
nodeops::transition_out_offset(project, block)
|
||||
}
|
||||
(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
|
||||
|
||||
+16
-28
@@ -26,9 +26,10 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::bridge;
|
||||
use oakcodec::proxymanager::ProxyManager;
|
||||
use oakcodec::task::TaskRequest;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::CHandle;
|
||||
use crate::task::{Task, TaskBehavior};
|
||||
|
||||
/// The proxy parameters, mirroring `oakcodec_proxy_params` in
|
||||
@@ -75,22 +76,23 @@ 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) };
|
||||
pub fn new(request: &TaskRequest, params: ProxyParams) -> ProxyTask {
|
||||
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);
|
||||
let title = format!(
|
||||
"Generating Proxy {}:{}",
|
||||
request.input_filename, request.stream_index
|
||||
);
|
||||
ProxyTask {
|
||||
base: Task::new(&title, CHandle::null()),
|
||||
source_filename: source,
|
||||
base: Task::new(&title, None),
|
||||
source_filename: request.input_filename.to_string(),
|
||||
stream_index: request.stream_index,
|
||||
params,
|
||||
output_filename: output,
|
||||
output_filename: request.output_filename.to_string(),
|
||||
duration_seconds: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -207,21 +209,16 @@ 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 {
|
||||
// Direct call into oakcodec's proxy manager (single-lib
|
||||
// unification: the old two-stage C ABI getter is gone; an empty
|
||||
// string means "not found").
|
||||
let ffmpeg_path = ProxyManager::find_ffmpeg("");
|
||||
if ffmpeg_path.is_empty() {
|
||||
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() {
|
||||
@@ -320,15 +317,6 @@ impl TaskBehavior for ProxyTask {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
+507
-291
File diff suppressed because it is too large
Load Diff
+30
-70
@@ -20,8 +20,9 @@
|
||||
//! 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`.
|
||||
//! README.md). Cancellation rides on a shared
|
||||
//! `oakcommon::cancelatom::CancelAtom` (single-lib unification: the old
|
||||
//! oakrender cancelatom C ABI is gone).
|
||||
//!
|
||||
//! CPP-PARITY: src/task/src/task.h
|
||||
//!
|
||||
@@ -38,9 +39,9 @@
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use crate::bridge;
|
||||
use oakcommon::cancelatom::CancelAtom;
|
||||
|
||||
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`).
|
||||
@@ -74,7 +75,7 @@ pub trait TaskBehavior {
|
||||
}
|
||||
|
||||
/// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the
|
||||
/// C ABI callback signature `(event_id, value, userdata)`. The C++ emits
|
||||
/// legacy `(event_id, value, userdata)` callback signature. 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).
|
||||
@@ -97,14 +98,13 @@ struct TaskDone {
|
||||
succeeded: bool,
|
||||
}
|
||||
|
||||
/// The base task. Owns lifecycle state plus a borrowed oakrender cancel
|
||||
/// atom; the concrete behavior lives in a [`TaskBehavior`] trait object.
|
||||
/// The base task. Owns lifecycle state plus a shared `CancelAtom`; 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,
|
||||
cancel_atom: Arc<CancelAtom>,
|
||||
event_listener: Option<EventListener>,
|
||||
cancel_event: Option<Box<dyn FnMut() + Send>>,
|
||||
started: bool,
|
||||
@@ -113,40 +113,21 @@ pub struct Task {
|
||||
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.
|
||||
/// Values published for the legacy 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)
|
||||
};
|
||||
/// Create a new task with the given title. When `cancel_atom` is `None`
|
||||
/// a fresh atom is created and owned by the task (mirroring the C++
|
||||
/// constructor); `Some` shares the caller's atom (mirrors the old
|
||||
/// borrowed-oakrender-atom constructor).
|
||||
pub fn new(title: &str, cancel_atom: Option<Arc<CancelAtom>>) -> Task {
|
||||
Task {
|
||||
title: title.to_string(),
|
||||
error: None,
|
||||
start_time: None,
|
||||
cancel_atom,
|
||||
owns_atom,
|
||||
cancel_atom: cancel_atom.unwrap_or_else(|| Arc::new(CancelAtom::new())),
|
||||
event_listener: None,
|
||||
cancel_event: None,
|
||||
started: false,
|
||||
@@ -207,50 +188,29 @@ impl Task {
|
||||
ret
|
||||
}
|
||||
|
||||
/// Request cancellation through the borrowed oakrender cancel atom, then
|
||||
/// invoke the cancel event callback if one is registered.
|
||||
/// Request cancellation through the shared 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);
|
||||
}
|
||||
}
|
||||
self.cancel_atom.cancel();
|
||||
if let Some(cb) = self.cancel_event.as_mut() {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether cancellation was requested (queries the oakrender atom).
|
||||
/// Whether cancellation was requested (queries the shared 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
|
||||
self.cancel_atom.is_cancelled()
|
||||
}
|
||||
|
||||
/// The borrowed cancel atom handle (empty for tasks without one).
|
||||
pub fn get_cancel_atom(&self) -> CHandle {
|
||||
self.cancel_atom
|
||||
/// The shared cancel atom (a clone of the task's `Arc`).
|
||||
pub fn get_cancel_atom(&self) -> Arc<CancelAtom> {
|
||||
self.cancel_atom.clone()
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
/// Replace the cancel atom. Used to share one atom between a task and
|
||||
/// its behavior's inner base task.
|
||||
pub fn set_cancel_atom(&mut self, atom: Arc<CancelAtom>) {
|
||||
self.cancel_atom = atom;
|
||||
self.owns_atom = false;
|
||||
}
|
||||
|
||||
/// Register the event listener. Replaces any previous listener.
|
||||
@@ -263,7 +223,7 @@ impl Task {
|
||||
self.cancel_event = Some(cb);
|
||||
}
|
||||
|
||||
/// Publish the shared values used by the C ABI subscribe wrapper.
|
||||
/// Publish the shared values used by the legacy subscribe wrapper.
|
||||
pub fn set_subscriber(&mut self, state: Arc<SubscriberState>) {
|
||||
self.subscriber = Some(state);
|
||||
}
|
||||
@@ -353,8 +313,8 @@ pub fn system_time_ms() -> 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).
|
||||
/// Marker error returned when a task is cancelled, so the legacy 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