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:
2026-08-16 00:33:45 +08:00
parent 2248be8567
commit ab1a2e9c7b
293 changed files with 27826 additions and 90128 deletions
+75 -7
View File
@@ -43,9 +43,9 @@ use std::ffi::{c_char, c_int, c_void};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
use crate::bridge::node as n;
use crate::bridge::task as t;
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
use crate::stubs::node as n;
use crate::stubs::task as t;
use crate::codec::OakEngineEncodingParams;
use crate::common::OakVideoParamsPod;
use crate::error::{Error, Result};
@@ -654,9 +654,9 @@ pub unsafe extern "C" fn oakengine_task_create_export(
///
/// The oaktask crate's `convert_encoding_params` consumes exactly these
/// fields (filename, format, video/audio/subtitle enables, codecs,
/// dimensions, time base, pixel format, export length), so a POD carrying
/// them is behaviorally identical to the original for the export task; all
/// other POD fields stay zeroed.
/// dimensions, time base, pixel format, audio rate/layout, export length),
/// so a POD carrying them is behaviorally identical to the original for
/// the export task; all other POD fields stay zeroed.
fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingParamsPOD> {
let mut pod = zeroed_encoding_params();
@@ -682,6 +682,24 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
pod.video_codec = unsafe { crate::codec::oakengine_encoding_params_video_codec(params) };
pod.audio_enabled = unsafe { crate::codec::oakengine_encoding_params_audio_enabled(params) };
pod.audio_codec = unsafe { crate::codec::oakengine_encoding_params_audio_codec(params) };
// Audio rate/layout flow through so the encoder opens with the
// requested rate (the module export reads them from the POD). The
// sample format is NOT carried: the FFmpeg encoder always runs in the
// codec's native format and resamples (see `FFmpegEncoder::open`).
if pod.audio_enabled != 0 {
let mut sample_rate: c_int = 0;
let mut channel_layout: u64 = 0;
unsafe {
crate::codec::oakengine_encoding_params_get_audio_params(
params,
&mut sample_rate,
&mut channel_layout,
std::ptr::null_mut(),
);
}
pod.audio_sample_rate = sample_rate;
pod.audio_channel_layout = channel_layout;
}
pod.subtitles_enabled =
unsafe { crate::codec::oakengine_encoding_params_subtitles_enabled(params) };
unsafe {
@@ -703,7 +721,7 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
pod.video_height = v.height;
pod.video_time_base_num = v.time_base_num;
pod.video_time_base_den = v.time_base_den;
pod.video_pixel_format = v.format;
pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
}
}
Ok(pod)
@@ -865,3 +883,53 @@ pub unsafe extern "C" fn oakengine_task_save_get_project(
}
})
}
// ---------------------------------------------------------------------------
// Load task results / event subscription
// ---------------------------------------------------------------------------
/// `oakengine_task_load_take_project` — take the project an interchange
/// (OVE/OTIO) load task produced after a successful run; ownership moves to
/// the caller (release with `oakengine_project_free`). NULL for a NULL
/// task, a task that never ran, or a task that is not a load task.
///
/// The module's `oaktask_load_take_project` is the load-result getter the
/// engine facade has no export for (the app's interchange-open path); it is
/// wrapped here so the app can stay on the `oakengine_*` surface.
#[no_mangle]
pub unsafe extern "C" fn oakengine_task_load_take_project(
task: *mut OakEngineTask,
) -> *mut OakEngineProject {
guard_ptr(|| unsafe {
let h = unbox(task)?;
let project = t::oaktask_load_take_project(h);
if project.is_null() {
return Ok(std::ptr::null_mut());
}
Ok(box_handle::<OakEngineProject>(project))
})
}
/// `oakengine_task_subscribe` — register the task event callback invoked on
/// the task's own thread (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1,
/// `OAKTASK_EVENT_FINISHED`=2); one subscription replaces the previous one.
///
/// Returns 0 on success; facade `OAKENGINE_E_INVALID` (-1) for a NULL task
/// or NULL callback; module error codes pass through untranslated. The
/// callback and `userdata` follow the module's `oaktask_event_fn` contract
/// (the engine facade has no subscription export of its own; the app's
/// export-progress path uses this wrapper).
#[no_mangle]
pub unsafe extern "C" fn oakengine_task_subscribe(
task: *mut OakEngineTask,
cb: Option<t::OakTaskEventFn>,
userdata: *mut c_void,
) -> i64 {
guard_i64(|| unsafe {
let h = unbox(task)?;
if cb.is_none() {
return Err(Error::Invalid);
}
Ok(t::oaktask_task_subscribe(h, cb, userdata))
})
}