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
+60
View File
@@ -0,0 +1,60 @@
// 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/>.
//! `olive::AudioParams` — the audio stream parameter value type.
//!
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the
//! codec crate used to create audio parameter sets through the oakcore C
//! ABI (`oakcore_audioparams_*`, host-provided symbols) and store them
//! behind refcounted handles. The parameters are plain values now — the
//! FFmpeg probe fills them in directly and the footage description stores
//! them by value.
/// `olive::AudioParams` — the audio stream description recorded at probe
/// time.
///
/// Mirrors `core/include/olive/core/oakcore/audioparams.h`: sample rate,
/// ffmpeg channel-layout mask, sample format, stream index, duration and
/// time base.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AudioParams {
/// Sample rate in Hz.
pub sample_rate: i32,
/// ffmpeg-style channel layout mask (e.g. 0x3 = stereo).
pub channel_layout: u64,
/// `olive::core::SampleFormat::Format` value.
pub format: i32,
/// Stream index within the source container.
pub stream_index: i32,
/// Stream length in time-base units.
pub duration: i64,
/// Time base (num/den seconds per tick).
pub time_base: (i32, i32),
}
impl AudioParams {
/// Channel count derived from the layout mask, mirroring the C++
/// `AudioParams::channel_count()`.
pub fn channel_count(&self) -> i32 {
self.channel_layout.count_ones() as i32
}
/// Whether the parameter set describes a usable stream, mirroring the
/// C++ `AudioParams::is_valid()` (positive rate and a non-zero layout).
pub fn is_valid(&self) -> bool {
self.sample_rate > 0 && self.channel_layout != 0
}
}
-686
View File
@@ -1,686 +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/>.
//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,
//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,
//! colortransform).
//!
//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,
//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,
//! release, abi_version}` layout from `include/common/handle.h`, so the
//! codec module can store them by value and pass them straight across
//! the FFI boundary.
//!
//! The oakcore audio parameters use a pointer-based C ABI instead of the
//! by-value handle convention: `oakcore_audioparams_*` take and return
//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/
//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw
//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`
//! returns a newly allocated rational the caller must release with
//! `oakcore_rational_free`.
//!
//! # ABI discipline (single-lib era)
//!
//! The frozen `oakcommon` C ABI (see `crates/oakengine/include/common/`)
//! uses **out-pointer getters** and **instance handles** for the module
//! families. Earlier codec-era bridges declared value-style signatures
//! (C++-era headers); those declarations were wrong against the frozen
//! contract and are fixed here. To keep the crate's call sites
//! unchanged, each true ABI symbol is declared under `#[link_name]` with
//! its real signature and wrapped in an adapter of the old value-style
//! shape (an M12 P0 decode-path fix: the mismatch made every decoded
//! frame invalid).
use std::ffi::{c_char, c_int, c_void};
use crate::handle::CHandle;
/// `OakVideoParams` — refcounted video-parameter handle.
pub type OakVideoParams = CHandle;
/// `OakAudioParams` — refcounted audio-parameter handle.
pub type OakAudioParams = CHandle;
/// `OakSubtitleParams` — refcounted subtitle-parameter handle.
pub type OakSubtitleParams = CHandle;
/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec
/// only stores and forwards it).
pub type OakNodeBlock = CHandle;
/// Module instance handles (filefunctions / oiioutils take `self_`).
pub type OakFileFunctions = CHandle;
pub type OakOIIOUtils = CHandle;
// The handle structs are opaque refcounted handles pointing into a C
// library; the boxed objects are independently synchronized there, so
// moving a handle between threads is sound.
extern "C" {
/// `oakcommon_videoparams_init`.
pub fn oakcommon_videoparams_init() -> OakVideoParams;
/// `oakcommon_videoparams_init_basic`.
#[link_name = "oakcommon_videoparams_init_basic"]
fn videoparams_init_basic_abi(
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;
/// `oakcommon_videoparams_init_with_time_base`.
#[link_name = "oakcommon_videoparams_init_with_time_base"]
fn videoparams_init_with_time_base_abi(
width: c_int,
height: c_int,
time_base_num: c_int,
time_base_den: 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;
/// `oakcommon_videoparams_free` (NULL/empty no-op).
pub fn oakcommon_videoparams_free(params: *mut OakVideoParams);
/// `oakcommon_videoparams_get_width`.
#[link_name = "oakcommon_videoparams_get_width"]
fn videoparams_get_width_abi(params: OakVideoParams, width: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_height`.
#[link_name = "oakcommon_videoparams_get_height"]
fn videoparams_get_height_abi(params: OakVideoParams, height: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_format`.
#[link_name = "oakcommon_videoparams_get_format"]
fn videoparams_get_format_abi(params: OakVideoParams, format: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_time_base` (num/den out).
#[link_name = "oakcommon_videoparams_get_time_base"]
fn videoparams_get_time_base_abi(
params: OakVideoParams,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int;
/// `oakcommon_videoparams_set_width`.
pub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);
/// `oakcommon_videoparams_set_height`.
pub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);
/// `oakcommon_videoparams_set_format`.
pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);
/// `oakcommon_videoparams_get_is_valid`.
#[link_name = "oakcommon_videoparams_get_is_valid"]
fn videoparams_get_is_valid_abi(params: OakVideoParams, is_valid: *mut c_int) -> c_int;
/// `oakcommon_videoparams_equals`.
#[link_name = "oakcommon_videoparams_equals"]
fn videoparams_equals_abi(
params: OakVideoParams,
other: OakVideoParams,
equal: *mut c_int,
) -> c_int;
/// `oakcommon_videoparams_set_time_base`.
pub fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64);
/// `oakcommon_videoparams_set_frame_rate`.
pub fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64);
/// `oakcommon_videoparams_set_pixel_aspect_ratio`.
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(params: OakVideoParams, num: i64, den: i64);
/// `oakcommon_videoparams_set_interlacing`.
pub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);
/// `oakcommon_videoparams_set_duration`.
pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);
/// `oakcommon_videoparams_set_start_time`.
pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);
/// `oakcommon_videoparams_set_color_range`.
pub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);
/// `oakcommon_videoparams_set_video_type`.
pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);
/// `oakcommon_videoparams_set_channel_count`.
pub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);
/// `oakcommon_videoparams_set_color_primaries`.
pub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);
/// `oakcommon_videoparams_set_color_transfer`.
pub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);
/// `oakcommon_videoparams_set_premultiplied_alpha`.
pub fn oakcommon_videoparams_set_premultiplied_alpha(
params: OakVideoParams,
premultiplied: c_int,
);
/// `oakcommon_videoparams_set_enabled`.
pub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);
/// `oakcommon_videoparams_static_get_bytes_per_pixel`.
#[link_name = "oakcommon_videoparams_static_get_bytes_per_pixel"]
fn videoparams_static_get_bytes_per_pixel_abi(format: c_int, channels: c_int) -> c_int;
/// `oakcommon_videoparams_frame_rate_as_time_base`.
pub fn oakcommon_videoparams_frame_rate_as_time_base(
frame_rate_num: i64,
frame_rate_den: i64,
out_num: *mut i64,
out_den: *mut i64,
);
/// `oakcommon_videoparams_get_stream_index`.
#[link_name = "oakcommon_videoparams_get_stream_index"]
fn videoparams_get_stream_index_abi(params: OakVideoParams, index: *mut c_int) -> c_int;
/// `oakcommon_videoparams_set_stream_index`.
pub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);
/// `oakcommon_videoparams_get_divider`.
#[link_name = "oakcommon_videoparams_get_divider"]
fn videoparams_get_divider_abi(params: OakVideoParams, divider: *mut c_int) -> c_int;
/// `oakcommon_videoparams_set_divider`.
pub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);
/// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).
#[link_name = "oakcommon_videoparams_get_frame_rate"]
fn videoparams_get_frame_rate_abi(
params: OakVideoParams,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int;
/// `oakcommon_videoparams_get_duration` (time-base units).
#[link_name = "oakcommon_videoparams_get_duration"]
fn videoparams_get_duration_abi(params: OakVideoParams, duration: *mut i64) -> c_int;
/// `oakcommon_videoparams_get_channel_count`.
#[link_name = "oakcommon_videoparams_get_channel_count"]
fn videoparams_get_channel_count_abi(params: OakVideoParams, count: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_color_primaries`.
#[link_name = "oakcommon_videoparams_get_color_primaries"]
fn videoparams_get_color_primaries_abi(params: OakVideoParams, primaries: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_color_transfer`.
#[link_name = "oakcommon_videoparams_get_color_transfer"]
fn videoparams_get_color_transfer_abi(params: OakVideoParams, transfer: *mut c_int) -> c_int;
/// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).
#[link_name = "oakcommon_videoparams_get_interlacing"]
fn videoparams_get_interlacing_abi(params: OakVideoParams, interlacing: *mut c_int) -> c_int;
/// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).
pub fn oakcore_audioparams_create(
sample_rate: c_int,
channel_layout: u64,
format: c_int,
) -> *mut OakAudioParams;
/// `oakcore_audioparams_free` (NULL no-op).
pub fn oakcore_audioparams_free(params: *mut OakAudioParams);
/// `oakcore_audioparams_sample_rate`.
pub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int;
/// `oakcore_audioparams_set_sample_rate`.
pub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);
/// `oakcore_audioparams_channel_layout`.
pub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64;
/// `oakcore_audioparams_set_channel_layout`.
pub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);
/// `oakcore_audioparams_set_time_base`.
pub fn oakcore_audioparams_set_time_base(params: *mut OakAudioParams, num: c_int, den: c_int);
/// `oakcore_audioparams_set_format`.
pub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);
/// `oakcore_audioparams_set_stream_index`.
pub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);
/// `oakcore_audioparams_set_duration`.
pub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);
/// `oakcore_audioparams_channel_count`.
pub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int;
/// `oakcore_audioparams_format`.
pub fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int;
/// `oakcore_audioparams_stream_index`.
pub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int;
/// `oakcore_audioparams_duration`.
pub fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64;
/// `oakcore_audioparams_is_valid`.
pub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int;
/// `oakcore_audioparams_time_base` (newly allocated rational; caller
/// releases with `oakcore_rational_free`).
pub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void;
/// `oakcore_rational_numerator`.
pub fn oakcore_rational_numerator(rational: *const c_void) -> c_int;
/// `oakcore_rational_denominator`.
pub fn oakcore_rational_denominator(rational: *const c_void) -> c_int;
/// `oakcore_rational_free` (NULL no-op).
pub fn oakcore_rational_free(rational: *mut c_void);
/// `oakcommon_subtitleparams_get_stream_index`.
#[link_name = "oakcommon_subtitleparams_get_stream_index"]
fn subtitleparams_get_stream_index_abi(
params: OakSubtitleParams,
index: *mut c_int,
) -> c_int;
/// `oakcommon_subtitleparams_generate_ass_header`.
pub fn oakcommon_subtitleparams_generate_ass_header(
params: OakSubtitleParams,
width: c_int,
height: c_int,
);
/// `oakcommon_subtitleparams_add_subtitle`.
pub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);
/// `oakcommon_config_get_int`.
pub fn oakcommon_config_get_int(
group: *const c_char,
key: *const c_char,
fallback: c_int,
) -> c_int;
/// `oakcommon_config_get_bool`.
pub fn oakcommon_config_get_bool(
group: *const c_char,
key: *const c_char,
fallback: c_int,
) -> c_int;
/// `oakcommon_config_get` (two-stage string access).
pub fn oakcommon_config_get(
group: *const c_char,
key: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakcommon_filefunctions_init`.
pub fn oakcommon_filefunctions_init() -> OakFileFunctions;
/// `oakcommon_filefunctions_free`.
pub fn oakcommon_filefunctions_free(self_: *mut OakFileFunctions);
/// `oakcommon_filefunctions_get_configuration_location` (two-stage).
#[link_name = "oakcommon_filefunctions_get_configuration_location"]
fn filefunctions_get_configuration_location_abi(
self_: OakFileFunctions,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakcommon_filefunctions_get_unique_file_identifier`.
#[link_name = "oakcommon_filefunctions_get_unique_file_identifier"]
fn filefunctions_get_unique_file_identifier_abi(
self_: OakFileFunctions,
filename: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakcommon_filefunctions_get_application_path` (two-stage).
#[link_name = "oakcommon_filefunctions_get_application_path"]
fn filefunctions_get_application_path_abi(
self_: OakFileFunctions,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakcommon_colortransform_init_output`.
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakVideoParams;
/// `oakcommon_colortransform_get_output` (two-stage string).
pub fn oakcommon_colortransform_get_output(
transform: OakVideoParams,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakcommon_colortransform_free`.
pub fn oakcommon_colortransform_free(params: *mut OakVideoParams);
/// `oakcommon_ffmpegutils_get_native_sample_format`.
#[link_name = "oakcommon_ffmpegutils_get_native_sample_format"]
fn ffmpegutils_get_native_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
/// `oakcommon_ffmpegutils_get_compatible_pixel_format`.
#[link_name = "oakcommon_ffmpegutils_get_compatible_pixel_format"]
fn ffmpegutils_get_compatible_pixel_format_abi(pix_fmt: c_int, out: *mut c_int) -> c_int;
/// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.
#[link_name = "oakcommon_ffmpegutils_get_ffmpeg_pixel_format"]
fn ffmpegutils_get_ffmpeg_pixel_format_abi(
pix_fmt: c_int,
channel_count: c_int,
out: *mut c_int,
) -> c_int;
/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
#[link_name = "oakcommon_ffmpegutils_get_ffmpeg_sample_format"]
fn ffmpegutils_get_ffmpeg_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
/// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.
#[link_name = "oakcommon_ffmpegutils_get_compatible_bridge_pixel_format"]
fn ffmpegutils_get_compatible_bridge_pixel_format_abi(
pix_fmt: c_int,
maximum_pix_fmt: c_int,
out: *mut c_int,
) -> c_int;
/// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
#[link_name = "oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space"]
fn ffmpegutils_convert_jpeg_space_to_regular_space_abi(
pix_fmt: c_int,
out: *mut c_int,
) -> c_int;
/// `oakcommon_oiioutils_init`.
pub fn oakcommon_oiioutils_init() -> OakOIIOUtils;
/// `oakcommon_oiioutils_free`.
pub fn oakcommon_oiioutils_free(self_: *mut OakOIIOUtils);
/// `oakcommon_oiioutils_get_oiio_base_type_from_format`.
#[link_name = "oakcommon_oiioutils_get_oiio_base_type_from_format"]
fn oiioutils_get_oiio_base_type_from_format_abi(
self_: OakOIIOUtils,
pixel_format: c_int,
out_base_type: *mut c_int,
) -> c_int;
/// `oakcommon_oiioutils_get_format_from_oiio_basetype`.
#[link_name = "oakcommon_oiioutils_get_format_from_oiio_basetype"]
fn oiioutils_get_format_from_oiio_basetype_abi(
self_: OakOIIOUtils,
base_type: c_int,
out_pixel_format: *mut c_int,
) -> c_int;
/// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).
#[link_name = "oakcommon_oiioutils_get_pixel_aspect_ratio"]
fn oiioutils_get_pixel_aspect_ratio_abi(
self_: OakOIIOUtils,
pixel_aspect_ratio: f64,
out_numerator: *mut c_int,
out_denominator: *mut c_int,
) -> c_int;
}
// ---------------------------------------------------------------------------
// Value-style adapters over the frozen out-pointer ABI (old call shapes)
// ---------------------------------------------------------------------------
/// `oakcommon_videoparams_init_basic` — value-style shape kept for call
/// sites; defaults: U8 format, 4 channels, 1:1 aspect, progressive,
/// divider 1.
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 { videoparams_init_basic_abi(width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den, interlacing, divider) }
}
/// `oakcommon_videoparams_init_with_time_base` — value-style shape kept
/// for call sites.
pub fn oakcommon_videoparams_init_with_time_base(
width: c_int,
height: c_int,
time_base_num: c_int,
time_base_den: 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 {
videoparams_init_with_time_base_abi(
width,
height,
time_base_num,
time_base_den,
pixel_format,
nb_channels,
pixel_aspect_num,
pixel_aspect_den,
interlacing,
divider,
)
}
}
/// Value-style `oakcommon_videoparams_get_width`.
pub fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int {
let mut w: c_int = 0;
unsafe { videoparams_get_width_abi(params, &mut w) };
w
}
/// Value-style `oakcommon_videoparams_get_height`.
pub fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int {
let mut h: c_int = 0;
unsafe { videoparams_get_height_abi(params, &mut h) };
h
}
/// Value-style `oakcommon_videoparams_get_format`.
pub fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int {
let mut f: c_int = -1;
unsafe { videoparams_get_format_abi(params, &mut f) };
f
}
/// Value-style `oakcommon_videoparams_get_is_valid`.
pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int {
let mut v: c_int = 0;
unsafe { videoparams_get_is_valid_abi(params, &mut v) };
v
}
/// Value-style `oakcommon_videoparams_equals` (1 when equal).
pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int {
let mut eq: c_int = 0;
unsafe { videoparams_equals_abi(a, b, &mut eq) };
eq
}
/// Value-style `oakcommon_videoparams_get_stream_index`.
pub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int {
let mut i: c_int = -1;
unsafe { videoparams_get_stream_index_abi(params, &mut i) };
i
}
/// Value-style `oakcommon_videoparams_get_divider`.
pub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int {
let mut d: c_int = 0;
unsafe { videoparams_get_divider_abi(params, &mut d) };
d
}
/// Value-style `oakcommon_videoparams_get_duration`.
pub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64 {
let mut d: i64 = 0;
unsafe { videoparams_get_duration_abi(params, &mut d) };
d
}
/// Value-style `oakcommon_videoparams_get_channel_count`.
pub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int {
let mut c: c_int = 0;
unsafe { videoparams_get_channel_count_abi(params, &mut c) };
c
}
/// Value-style `oakcommon_videoparams_get_color_primaries`.
pub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int {
let mut p: c_int = 0;
unsafe { videoparams_get_color_primaries_abi(params, &mut p) };
p
}
/// Value-style `oakcommon_videoparams_get_color_transfer`.
pub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int {
let mut t: c_int = 0;
unsafe { videoparams_get_color_transfer_abi(params, &mut t) };
t
}
/// Value-style `oakcommon_videoparams_get_interlacing`.
pub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int {
let mut i: c_int = 0;
unsafe { videoparams_get_interlacing_abi(params, &mut i) };
i
}
/// Value-style `oakcommon_videoparams_get_time_base` (num/den out as
/// `i64`, widened from the frozen `i32` outs).
pub fn oakcommon_videoparams_get_time_base(
params: OakVideoParams,
out_num: *mut i64,
out_den: *mut i64,
) -> c_int {
let mut num: c_int = 0;
let mut den: c_int = 0;
let rc = unsafe { videoparams_get_time_base_abi(params, &mut num, &mut den) };
if !out_num.is_null() {
unsafe { *out_num = num as i64 };
}
if !out_den.is_null() {
unsafe { *out_den = den as i64 };
}
rc
}
/// Value-style `oakcommon_videoparams_get_frame_rate` (num/den out).
pub fn oakcommon_videoparams_get_frame_rate(
params: OakVideoParams,
out_num: *mut c_int,
out_den: *mut c_int,
) -> c_int {
unsafe { videoparams_get_frame_rate_abi(params, out_num, out_den) }
}
/// Value-style `oakcommon_videoparams_static_get_bytes_per_pixel`
/// (4 channels assumed; the frozen ABI takes channels explicitly).
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
unsafe { videoparams_static_get_bytes_per_pixel_abi(format, 4) }
}
/// Value-style `oakcommon_subtitleparams_get_stream_index`.
pub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -> c_int {
let mut i: c_int = -1;
unsafe { subtitleparams_get_stream_index_abi(params, &mut i) };
i
}
/// Value-style `oakcommon_ffmpegutils_get_native_sample_format`.
pub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_get_native_sample_format_abi(sample_format, &mut out) };
out
}
/// Value-style `oakcommon_ffmpegutils_get_compatible_pixel_format`.
pub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_get_compatible_pixel_format_abi(format, &mut out) };
out
}
/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`
/// (4 channels assumed).
pub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_get_ffmpeg_pixel_format_abi(format, 4, &mut out) };
out
}
/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
pub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_get_ffmpeg_sample_format_abi(format, &mut out) };
out
}
/// Value-style `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`
/// (no maximum constraint).
pub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_get_compatible_bridge_pixel_format_abi(format, -1, &mut out) };
out
}
/// Value-style `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
pub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { ffmpegutils_convert_jpeg_space_to_regular_space_abi(format, &mut out) };
out
}
/// The process-wide filefunctions instance (lazily created; freed on
/// process exit is not required — the singleton outlives all users).
fn filefunctions_instance() -> OakFileFunctions {
static FF: std::sync::OnceLock<OakFileFunctions> = std::sync::OnceLock::new();
*FF.get_or_init(|| unsafe { oakcommon_filefunctions_init() })
}
/// Value-style `oakcommon_filefunctions_get_application_path`
/// (two-stage string getter on the shared instance).
pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe { filefunctions_get_application_path_abi(filefunctions_instance(), buf, buf_size) }
}
/// Value-style `oakcommon_filefunctions_get_configuration_location`.
pub fn oakcommon_filefunctions_get_configuration_location(
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
unsafe {
filefunctions_get_configuration_location_abi(filefunctions_instance(), buf, buf_size)
}
}
/// Value-style `oakcommon_filefunctions_get_unique_file_identifier`
/// (the frozen ABI writes a string; the value-style call sites expect an
/// `i64`, so the string is folded into a stable 64-bit FNV hash).
pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64 {
let mut buf = [0 as c_char; 512];
// Two-stage contract: 0 = written, positive = required size, negative
// = error. The adapter's value-style call sites get the i64 fold.
let n = unsafe {
filefunctions_get_unique_file_identifier_abi(
filefunctions_instance(),
path,
buf.as_mut_ptr(),
buf.len() as c_int,
)
};
if n < 0 {
return 0;
}
let avail = if n == 0 { buf.len() } else { (n as usize).min(buf.len()) };
let bytes: Vec<u8> = buf[..avail]
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect();
let mut hash: u64 = 0xcbf29ce484222325;
for &b in &bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash as i64
}
/// The process-wide oiioutils instance (lazily created).
fn oiioutils_instance() -> OakOIIOUtils {
static OU: std::sync::OnceLock<OakOIIOUtils> = std::sync::OnceLock::new();
*OU.get_or_init(|| unsafe { oakcommon_oiioutils_init() })
}
/// Value-style `oakcommon_oiioutils_get_oiio_base_type_from_format`.
pub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { oiioutils_get_oiio_base_type_from_format_abi(oiioutils_instance(), format, &mut out) };
out
}
/// Value-style `oakcommon_oiioutils_get_format_from_oiio_basetype`.
pub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int {
let mut out: c_int = -1;
unsafe { oiioutils_get_format_from_oiio_basetype_abi(oiioutils_instance(), basetype, &mut out) };
out
}
/// Value-style `oakcommon_oiioutils_get_pixel_aspect_ratio`
/// (the frozen ABI takes the aspect ratio as `f64`; the value-style call
/// sites pass width/height and get num/den back).
pub fn oakcommon_oiioutils_get_pixel_aspect_ratio(
width: c_int,
height: c_int,
out_num: *mut c_int,
out_den: *mut c_int,
) -> c_int {
let ratio = if height != 0 {
f64::from(width) / f64::from(height)
} else {
1.0
};
unsafe {
oiioutils_get_pixel_aspect_ratio_abi(oiioutils_instance(), ratio, out_num, out_den)
}
}
-35
View File
@@ -1,35 +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 the other oak modules.
//!
//! The codec module links against oakcommon and oakrender at the C ABI.
//! Every signature below mirrors the corresponding public header
//! verbatim and is resolved at link time. The by-value handle structs
//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of
//! the `{ctx, addref, release, abi_version}` layout so the codec crate
//! can hold and hand them across the FFI boundary without translation.
pub mod common;
pub mod render;
// In-memory mocks for the oakcommon/oakrender C ABI so the crate links
// and is testable under `cargo test` (where those dylibs are absent).
// The `test-stubs` feature additionally compiles the oakcore_*/oakrender_*
// host-mocks for consumer test binaries (e.g. oaknode's) that link this
// crate directly — those symbols are not provided by any Rust crate.
#[cfg(any(test, feature = "test-stubs"))]
pub mod test_stubs;
-140
View File
@@ -1,140 +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/>.
//! oakrender C ABI imports (display textures, renderers, cancel atoms).
//!
//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a
//! `CancelAtom`; both are oakrender refcounted handles with the standard
//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`
//! is a flattened POD the decoders construct to describe the frame.
use std::ffi::{c_char, c_int, c_void};
use crate::handle::CHandle;
/// `OakRenderTexture` — refcounted GPU texture handle.
pub type OakRenderTexture = CHandle;
/// `OakCancelAtom` — refcounted cancellation atom handle.
pub type OakCancelAtom = CHandle;
/// `OakRenderRenderer` — refcounted display-renderer handle.
pub type OakRenderRenderer = CHandle;
/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.
pub type OakCodecFrame = CHandle;
// Refcounted opaque handles; thread-safe in the C library.
/// `oakrender_video_params` — flattened POD of `olive::VideoParams`
/// passed into oakrender; see `include/render/renderer.h`.
#[repr(C)]
pub struct oakrender_video_params {
/// Width in pixels.
pub width: c_int,
/// Height in pixels.
pub height: c_int,
/// Frame-duration numerator (e.g. 1001/30000 s).
pub time_base_num: c_int,
/// Frame-duration denominator.
pub time_base_den: c_int,
/// `olive::PixelFormat::Format`.
pub format: c_int,
/// Pixel-aspect numerator.
pub pixel_aspect_num: c_int,
/// Pixel-aspect denominator.
pub pixel_aspect_den: c_int,
/// `olive::VideoParams::Interlacing`.
pub interlacing: c_int,
/// `olive::VideoParams::ColorRange`.
pub color_range: c_int,
/// Preview-resolution divider (1 = full).
pub divider: c_int,
/// `olive::VideoParams::Type` (0 = video).
pub video_type: c_int,
/// 0/1 premultiplied alpha.
pub premultiplied_alpha: c_int,
}
extern "C" {
/// `oakrender_cancelatom_init`.
pub fn oakrender_cancelatom_init() -> OakCancelAtom;
/// `oakrender_cancelatom_free` (NULL/empty no-op).
pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);
/// `oakrender_cancelatom_is_cancelled`.
pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int;
/// `oakrender_cancelatom_heard_cancel`.
pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int;
/// `oakrender_cancelatom_cancel`.
pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);
/// `oakrender_cancelatom_get_native`.
pub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void;
/// `oakrender_display_texture_create`.
pub fn oakrender_display_texture_create(
renderer: OakRenderRenderer,
params: *const oakrender_video_params,
data: *const c_void,
linesize: c_int,
) -> OakRenderTexture;
/// `oakrender_display_texture_retain`.
pub fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture;
/// `oakrender_display_texture_free` (NULL/empty no-op).
pub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);
/// `oakrender_display_texture_upload`.
pub fn oakrender_display_texture_upload(texture: OakRenderTexture) -> c_int;
/// `oakrender_display_texture_download`.
pub fn oakrender_display_texture_download(
texture: OakRenderTexture,
pixels: *mut c_void,
linesize: c_int,
) -> c_int;
/// `oakrender_display_texture_get_params`.
pub fn oakrender_display_texture_get_params(
texture: OakRenderTexture,
out: *mut oakrender_video_params,
) -> c_int;
/// `oakrender_display_texture_id`.
pub fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int;
/// `oakrender_display_texture_is_dummy`.
pub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -> c_int;
/// `oakrender_display_texture_get_frame` (two-stage frame access).
pub fn oakrender_display_texture_get_frame(
texture: OakRenderTexture,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
/// `oakrender_codec_frame_width`.
pub fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int;
/// `oakrender_codec_frame_height`.
pub fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int;
/// `oakrender_codec_frame_fb_format`.
pub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -> c_int;
/// `oakrender_codec_frame_free` (NULL/empty no-op).
pub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);
/// `oakrender_codec_frame_allocate`.
pub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int;
/// `oakrender_codec_frame_linesize_bytes`.
pub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int;
/// `oakrender_codec_frame_is_allocated`.
pub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int;
/// `oakrender_display_renderer_blit_color_managed`.
pub fn oakrender_display_renderer_blit_color_managed(
renderer: OakRenderRenderer,
job: *const c_void,
dst_texture: OakRenderTexture,
params: *const oakrender_video_params,
) -> c_int;
}
File diff suppressed because it is too large Load Diff
+26 -42
View File
@@ -22,7 +22,7 @@
//! `Unavailable`. Deterministic per-channel filenames derive from the
//! source + target audio params.
use std::ffi::CString;
use oakcommon::filefunctions::FileFunctions;
use std::path::Path;
/// Conform state of one audio stream.
@@ -184,18 +184,11 @@ fn conform_filenames(
out
}
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
/// returns a 64-bit id directly).
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
fn unique_file_identifier(filename: &str) -> String {
let c = match CString::new(filename) {
Ok(c) => c,
Err(_) => return String::new(),
};
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
let id = unsafe {
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
};
format!("{}", id)
FileFunctions::new()
.get_unique_file_identifier(filename)
.unwrap_or_default()
}
/// True when every conform filename already exists on disk.
@@ -234,34 +227,23 @@ mod tests {
dir.to_string_lossy().into_owned()
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 14695981039346656037;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(1099511628211);
}
h
}
#[test]
fn unique_identifier_matches_bridge_hash() {
// The frozen ABI returns the identifier as a string; the
// value-style adapter folds it into an FNV-1a-64. The test stub
// produces the decimal FNV of the path bytes, so the expected
// value is FNV of that decimal string.
let stub_id = fnv1a64(b"media.mp4") as i64;
let expected = format!("{}", fnv1a64(stub_id.to_string().as_bytes()) as i64);
assert_eq!(unique_file_identifier("media.mp4"), expected);
fn unique_identifier_uses_file_metadata() {
// The identifier is the 16-digit FNV-1a hex of (absolute path +
// mtime); a missing file has no identifier.
assert_eq!(unique_file_identifier("no-such-file.mp4"), "");
let dir = temp_subdir("id");
let file = std::path::Path::new(&dir).join("media.mp4");
std::fs::write(&file, b"x").unwrap();
let id = unique_file_identifier(&file.to_string_lossy());
assert_eq!(id.len(), 16);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
// Deterministic: same input, same id.
assert_eq!(
unique_file_identifier("media.mp4"),
unique_file_identifier("media.mp4")
);
assert_eq!(id, unique_file_identifier(&file.to_string_lossy()));
let other = std::path::Path::new(&dir).join("other.mp4");
std::fs::write(&other, b"y").unwrap();
// Different input, different id.
assert_ne!(
unique_file_identifier("media.mp4"),
unique_file_identifier("other.mp4")
);
assert_ne!(id, unique_file_identifier(&other.to_string_lossy()));
}
#[test]
@@ -279,21 +261,23 @@ mod tests {
#[test]
fn conform_filename_derivation_and_range() {
let m = ConformManager::instance();
let src_dir = temp_subdir("names");
let src = std::path::Path::new(&src_dir).join("media.mp4");
std::fs::write(&src, b"x").unwrap();
let cache = temp_subdir("names");
let stub_id = fnv1a64(b"media.mp4") as i64;
let id = fnv1a64(stub_id.to_string().as_bytes()) as i64;
let id = unique_file_identifier(&src.to_string_lossy());
let base = format!("{}-0.48000.0.3", id);
let f0 = m
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0)
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 0)
.unwrap();
let f1 = m
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 1)
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 1)
.unwrap();
assert_eq!(f0, format!("{}/{}.0.pcm", cache, base));
assert_eq!(f1, format!("{}/{}.1.pcm", cache, base));
// Out of range.
assert!(matches!(
m.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 5),
m.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 5),
Err(crate::error::Error::NotFound)
));
}
+24 -14
View File
@@ -26,12 +26,22 @@
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
use oakcommon::cancelatom::CancelAtom;
use oakcore_rs::{Rational, TimeRange};
use crate::bridge::render::{OakCancelAtom, OakRenderTexture};
use crate::footagedescription::FootageDescription;
use crate::frame::Frame;
/// `OakRenderTexture` — refcounted GPU texture handle (an oakrender type,
/// opaque to oakcodec). The codec crate cannot depend on oakrender (the
/// dependency cycle), so it only ever produces an empty handle — the
/// shared [`crate::handle::CHandle`] carries that value unchanged.
pub type OakRenderTexture = crate::handle::CHandle;
/// `OakNodeBlock` — opaque node-block handle owned elsewhere; the codec
/// only stores and forwards it (borrowed, never dereferenced).
pub type OakNodeBlock = crate::handle::CHandle;
/// `oakcodec_video_stream_info` — POD probe output describing one video
/// stream; see `include/codec/decoder.h`.
#[repr(C)]
@@ -158,7 +168,7 @@ pub enum RetrieveState {
pub struct CodecStream {
filename: String,
stream: i32,
block: Option<crate::bridge::common::OakNodeBlock>,
block: Option<OakNodeBlock>,
}
impl CodecStream {
@@ -175,7 +185,7 @@ impl CodecStream {
pub fn with_block(
filename: String,
stream: i32,
block: Option<crate::bridge::common::OakNodeBlock>,
block: Option<OakNodeBlock>,
) -> Self {
CodecStream {
filename,
@@ -212,7 +222,7 @@ impl CodecStream {
}
/// Associated timeline block (borrowed; only compared, never used).
pub fn block(&self) -> Option<crate::bridge::common::OakNodeBlock> {
pub fn block(&self) -> Option<OakNodeBlock> {
self.block.clone()
}
}
@@ -241,7 +251,7 @@ pub trait Decoder: Send + Sync {
fn probe(
&self,
filename: &str,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> Option<FootageDescription>;
/// Open `stream` for decoding. Thread-safe.
@@ -281,7 +291,7 @@ pub trait Decoder: Send + Sync {
sample_rate: i32,
channel_layout: u64,
sample_format: i32,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()>;
/// Offset of the audio start relative to the video (rational seconds).
@@ -326,7 +336,7 @@ impl Decoder for UnimplementedDecoder {
fn probe(
&self,
_filename: &str,
_cancelled: Option<&OakCancelAtom>,
_cancelled: Option<&CancelAtom>,
) -> Option<FootageDescription> {
None
}
@@ -377,7 +387,7 @@ impl Decoder for UnimplementedDecoder {
_sample_rate: i32,
_channel_layout: u64,
_sample_format: i32,
_cancelled: Option<&OakCancelAtom>,
_cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()> {
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
@@ -400,13 +410,13 @@ pub fn create_from_id(id: &str) -> Option<Arc<dyn Decoder>> {
/// not injected, in which case the built-in list below is used.
static TEST_DECODERS: OnceLock<Mutex<Vec<Arc<dyn Decoder>>>> = OnceLock::new();
/// Serializes every test that reads the built-in decoder registry. The ffi
/// decoder tests inject through `crate::ffi::lock_tests()` (the shared
/// `TEST_LOCK`), so the registry assertions below take that same lock to
/// never race with an injected list.
/// Serializes every test that reads the built-in decoder registry. Tests
/// inject through [`set_test_decoders`] under [`crate::lock_tests`] (the
/// shared test lock), so the registry assertions below take that same lock
/// to never race with an injected list.
#[cfg(test)]
fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
crate::ffi::lock_tests()
fn registry_guard() -> crate::TestLock {
crate::lock_tests()
}
/// Replace the decoder registry with `list`; pass an empty list to restore
-308
View File
@@ -1,308 +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/>.
//! `include/codec/conform.h` exports.
//!
//! Complete inventory: create_instance / destroy_instance / get_state /
//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the
//! states.
//!
//! # CPP-PARITY
//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the
//! singleton is absent (`!ConformManager::instance()`); the Rust
//! [`crate::conformmanager::ConformManager::instance`] is a lazy
//! `'static` singleton that can never be absent, so that branch cannot
//! trigger.
use std::ffi::{c_char, c_int};
use crate::conformmanager::{ConformManager, ConformState};
use crate::handle;
/// `OAKCODEC_CONFORM_EXISTS`.
pub const OAKCODEC_CONFORM_EXISTS: c_int = 0;
/// `OAKCODEC_CONFORM_GENERATING`.
pub const OAKCODEC_CONFORM_GENERATING: c_int = 1;
/// `OAKCODEC_CONFORM_UNAVAILABLE`.
pub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;
/// `oakcodec_conform_create_instance`: create the singleton (always
/// present here, so a no-op).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_conform_create_instance() -> c_int {
handle::guard_raw(|| {
let _ = ConformManager::instance();
crate::error::OAKCODEC_OK
})
}
/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust
/// manager is stateless, so a no-op).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_conform_destroy_instance() -> c_int {
handle::guard_raw(|| crate::error::OAKCODEC_OK)
}
/// `oakcodec_conform_get_state`: query the conform state of one audio
/// stream.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_conform_get_state(
cache_path: *const c_char,
source_filename: *const c_char,
stream_index: c_int,
sample_rate: c_int,
channel_layout: u64,
sample_format: c_int,
wait: c_int,
) -> c_int {
handle::guard_raw(|| {
let cache = match crate::ffi::c_str(cache_path) {
Some(c) if !c.is_empty() => c,
_ => return crate::error::OAKCODEC_E_INVALID,
};
let source = match crate::ffi::c_str(source_filename) {
Some(s) if !s.is_empty() => s,
_ => return crate::error::OAKCODEC_E_INVALID,
};
let m = ConformManager::instance();
match m.get_conform_state(
&cache,
&source,
stream_index,
sample_rate,
channel_layout,
sample_format,
wait != 0,
) {
Ok(ConformState::Exists) => OAKCODEC_CONFORM_EXISTS,
Ok(ConformState::Generating) => OAKCODEC_CONFORM_GENERATING,
Ok(ConformState::Unavailable) | Err(_) => OAKCODEC_CONFORM_UNAVAILABLE,
}
})
}
/// `oakcodec_conform_filename_count`: number of conform files for the
/// given stream/params; 0 on invalid arguments.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_conform_filename_count(
cache_path: *const c_char,
source_filename: *const c_char,
stream_index: c_int,
sample_rate: c_int,
channel_layout: u64,
sample_format: c_int,
) -> c_int {
handle::guard_raw(|| {
let cache = match crate::ffi::c_str(cache_path) {
Some(c) if !c.is_empty() => c,
_ => return 0,
};
let source = match crate::ffi::c_str(source_filename) {
Some(s) if !s.is_empty() => s,
_ => return 0,
};
let m = ConformManager::instance();
m.get_conform_filename_count(
&cache,
&source,
stream_index,
sample_rate,
channel_layout,
sample_format,
) as c_int
})
}
/// `oakcodec_conform_filename_at`: the `index`-th conform filename
/// (two-stage string).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_conform_filename_at(
cache_path: *const c_char,
source_filename: *const c_char,
stream_index: c_int,
sample_rate: c_int,
channel_layout: u64,
sample_format: c_int,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let cache = match crate::ffi::c_str(cache_path) {
Some(c) if !c.is_empty() => c,
_ => return crate::error::OAKCODEC_E_INVALID,
};
let source = match crate::ffi::c_str(source_filename) {
Some(s) if !s.is_empty() => s,
_ => return crate::error::OAKCODEC_E_INVALID,
};
let m = ConformManager::instance();
match m.get_conform_filename(
&cache,
&source,
stream_index,
sample_rate,
channel_layout,
sample_format,
index as usize,
) {
Ok(f) => super::string_out(&f, buf, buf_size),
Err(crate::error::Error::NotFound) => crate::error::OAKCODEC_E_NOT_FOUND,
Err(_) => crate::error::OAKCODEC_E_FAILED,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conformmanager::test_util::REG_LOCK;
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap()
}
fn temp_cache(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"oakcodec_ffi_conform_{}_{}",
name,
std::process::id()
));
let _ = std::fs::create_dir_all(&dir);
dir.to_string_lossy().into_owned()
}
#[test]
fn create_destroy_instance_ok() {
let _g = crate::ffi::lock_tests();
assert_eq!(
unsafe { oakcodec_conform_create_instance() },
crate::error::OAKCODEC_OK
);
assert_eq!(
unsafe { oakcodec_conform_destroy_instance() },
crate::error::OAKCODEC_OK
);
}
#[test]
fn get_state_maps_states() {
let _g = crate::ffi::lock_tests();
let _g = REG_LOCK.lock().unwrap();
// No registrar and no files -> UNAVAILABLE.
let cache = cstr(&temp_cache("state"));
let src = cstr("media.mp4");
let rc = unsafe {
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
};
assert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);
// Invalid arguments -> E_INVALID.
let rc = unsafe {
oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
let empty = cstr("");
let rc = unsafe {
oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
// Write the conform files -> EXISTS.
let m = ConformManager::instance();
for i in 0..2 {
let f = m
.get_conform_filename(&temp_cache("state"), "media.mp4", 0, 48000, 0x3, 0, i)
.unwrap();
std::fs::write(&f, b"pcm").unwrap();
}
let rc = unsafe {
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
};
assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
}
#[test]
fn filename_count_and_at() {
let _g = crate::ffi::lock_tests();
let cache = cstr(&temp_cache("names"));
let src = cstr("media.mp4");
// Stereo -> 2 files.
let rc = unsafe {
oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0)
};
assert_eq!(rc, 2);
// Invalid args -> 0 (not an error).
let rc = unsafe {
oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0)
};
assert_eq!(rc, 0);
// filename_at round-trips the deterministic name.
let mut buf = [0i8; 512];
let rc = unsafe {
oakcodec_conform_filename_at(
cache.as_ptr(),
src.as_ptr(),
0,
48000,
0x3,
0,
0,
buf.as_mut_ptr(),
512,
)
};
assert!(rc > 0);
let name = crate::ffi::c_str(buf.as_ptr()).unwrap();
assert!(name.ends_with(".0.pcm"));
// Out-of-range index -> E_NOT_FOUND.
let rc = unsafe {
oakcodec_conform_filename_at(
cache.as_ptr(),
src.as_ptr(),
0,
48000,
0x3,
0,
5,
buf.as_mut_ptr(),
512,
)
};
assert_eq!(rc, OAKCODEC_E_NOT_FOUND);
// Invalid args -> E_INVALID.
let rc = unsafe {
oakcodec_conform_filename_at(
std::ptr::null(),
src.as_ptr(),
0,
48000,
0x3,
0,
0,
buf.as_mut_ptr(),
512,
)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
}
}
File diff suppressed because it is too large Load Diff
-818
View File
@@ -1,818 +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/>.
//! `include/codec/encoder.h` exports.
//!
//! Complete inventory: init / free / set_video_option / open / write_video
//! / write_audio / write_subtitle / flush / last_error /
//! get_desired_pixel_format / export_format_get_extension /
//! encoding_generate_matrix.
//!
//! # CPP-PARITY
//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the
//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex<EncoderBox>`
//! and carries an extra `last_error` field because the Rust `Encoder` trait
//! has no `get_error()` (the C++ reads the message off the encoder). The
//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note
//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format`
//! are typed enums, so `to_native` converts the int fields.
use std::ffi::{c_char, c_int};
use std::sync::{Arc, Mutex};
use oakcore_rs::PixelFormat;
use crate::encoder::Encoder;
use crate::encodingparams::EncodingParams;
use crate::handle::{self, CHandle};
/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/
/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled
/// configuration). Field names and order mirror the header verbatim.
#[allow(missing_docs)]
#[repr(C)]
pub struct oakcodec_encoding_params {
pub filename: [u8; 1024],
pub format: c_int,
pub video_enabled: c_int,
pub video_codec: c_int,
pub video_width: c_int,
pub video_height: c_int,
pub video_time_base_num: c_int,
pub video_time_base_den: c_int,
pub video_pixel_format: c_int,
pub video_interlacing: c_int,
pub video_pixel_aspect_num: c_int,
pub video_pixel_aspect_den: c_int,
pub video_bit_rate: i64,
pub video_min_bit_rate: i64,
pub video_max_bit_rate: i64,
pub video_buffer_size: i64,
pub video_threads: c_int,
pub video_pix_fmt: [u8; 64],
pub video_is_image_sequence: c_int,
pub video_scaling_method: c_int,
pub audio_enabled: c_int,
pub audio_codec: c_int,
pub audio_sample_rate: c_int,
pub audio_channel_layout: u64,
pub audio_sample_format: c_int,
pub audio_bit_rate: i64,
pub subtitles_enabled: c_int,
pub subtitles_codec: c_int,
pub subtitles_are_sidecar: c_int,
pub subtitles_sidecar_format: c_int,
pub color_transform_output: [u8; 256],
pub export_length_num: c_int,
pub export_length_den: c_int,
pub has_custom_range: c_int,
pub custom_range_in_num: i64,
pub custom_range_in_den: i64,
pub custom_range_out_num: i64,
pub custom_range_out_den: i64,
}
/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`).
struct EncoderBox {
encoder: Option<Arc<dyn Encoder>>,
params: EncodingParams,
/// Per-codec video options set via `oakcodec_encoder_set_video_option`
/// between init and open. Kept here (not in [`EncodingParams`], which
/// is a byte-exact mirror of the C POD).
video_opts: Vec<(String, String)>,
open: bool,
flushed: bool,
/// Last error detail (the C++ reads it off the encoder's `get_error`).
last_error: String,
}
/// Convert an `OakPixelFormat` int code to a [`PixelFormat`].
fn pixel_format_from_i32(v: c_int) -> PixelFormat {
match v {
0 => PixelFormat::U8,
1 => PixelFormat::U10,
2 => PixelFormat::U16,
3 => PixelFormat::F16,
4 => PixelFormat::F32,
_ => PixelFormat::Invalid,
}
}
/// Flatten the C POD into the crate's [`EncodingParams`]
/// (`to_native` in `c_api/encoder.cpp`).
fn to_native(p: &oakcodec_encoding_params) -> EncodingParams {
let mut n = EncodingParams::default();
n.filename = p.filename;
n.format = p.format;
n.video_enabled = p.video_enabled;
n.video_codec = p.video_codec;
n.video_width = p.video_width;
n.video_height = p.video_height;
n.video_time_base_num = p.video_time_base_num;
n.video_time_base_den = p.video_time_base_den;
n.video_pixel_format = pixel_format_from_i32(p.video_pixel_format);
n.video_interlacing = p.video_interlacing;
n.video_pixel_aspect_num = p.video_pixel_aspect_num;
n.video_pixel_aspect_den = p.video_pixel_aspect_den;
n.video_bit_rate = p.video_bit_rate;
n.video_min_bit_rate = p.video_min_bit_rate;
n.video_max_bit_rate = p.video_max_bit_rate;
n.video_buffer_size = p.video_buffer_size;
n.video_threads = p.video_threads;
n.video_pix_fmt = p.video_pix_fmt;
n.video_is_image_sequence = p.video_is_image_sequence;
n.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method);
n.audio_enabled = p.audio_enabled;
n.audio_codec = p.audio_codec;
n.audio_sample_rate = p.audio_sample_rate;
n.audio_channel_layout = p.audio_channel_layout;
n.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format);
n.audio_bit_rate = p.audio_bit_rate;
n.subtitles_enabled = p.subtitles_enabled;
n.subtitles_codec = p.subtitles_codec;
n.subtitles_are_sidecar = p.subtitles_are_sidecar;
n.subtitles_sidecar_format = p.subtitles_sidecar_format;
n.color_transform_output = p.color_transform_output;
n.export_length_num = p.export_length_num;
n.export_length_den = p.export_length_den;
n.has_custom_range = p.has_custom_range;
n.custom_range_in_num = p.custom_range_in_num;
n.custom_range_in_den = p.custom_range_in_den;
n.custom_range_out_num = p.custom_range_out_num;
n.custom_range_out_den = p.custom_range_out_den;
n
}
/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks
/// only that at least one track is enabled.
fn is_valid(p: &EncodingParams) -> bool {
p.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0
}
/// `oakcodec_encoder_init`: create an encoder for `params` (count 1);
/// empty handle when the configuration is invalid.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -> CHandle {
handle::guard_handle(|| {
if params.is_null() {
return Ok(CHandle::null());
}
let native = to_native(unsafe { &*params });
if !is_valid(&native) {
return Ok(CHandle::null());
}
Ok(handle::make_owned(Mutex::new(EncoderBox {
encoder: None,
params: native,
video_opts: Vec::new(),
open: false,
flushed: false,
last_error: String::new(),
})))
})
}
/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) {
handle::guard_void(|| super::free_handle(encoder));
}
/// `oakcodec_encoder_set_video_option`: set a per-codec video option
/// (e.g. "crf"); only valid between init and open.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
encoder: CHandle,
key: *const c_char,
value: *const c_char,
) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
let key = match crate::ffi::c_str(key) {
Some(k) => k,
None => return Err(crate::error::Error::Invalid),
};
let mut b = b.lock().unwrap();
if b.open {
return Err(crate::error::Error::State);
}
let value = crate::ffi::c_str(value).unwrap_or_default();
// `EncodingParams::set_video_option` replaces an existing key.
b.video_opts.retain(|(k, _)| k != &key);
b.video_opts.push((key, value));
Ok(())
})
}
/// `oakcodec_encoder_open`: create the encoder for the configured params,
/// apply the video options and open the output.
///
/// # CPP-PARITY
/// The C++ `open()` calls `create_from_params` (which applies the options
/// internally) then `open()`. The Rust trait separates `configure`, so it
/// is invoked between the two; the box's `video_opts` (set between init
/// and open) are stored for future wiring but not yet passed to
/// `configure` (no trait channel carries them in the interim).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
let mut b = b.lock().unwrap();
if b.open {
return Err(crate::error::Error::State);
}
let e = match crate::encoder::create_from_params(&b.params) {
Some(e) => e,
None => {
b.last_error = "failed to create encoder".to_string();
return Err(crate::error::Error::Failed(
"failed to create encoder".to_string(),
));
}
};
if e.configure(&b.params).is_err() {
b.last_error = "failed to configure encoder".to_string();
return Err(crate::error::Error::Failed(
"failed to configure encoder".to_string(),
));
}
if e.open().is_err() {
b.last_error = "failed to open stream".to_string();
return Err(crate::error::Error::Failed(
"failed to open stream".to_string(),
));
}
b.encoder = Some(e);
b.open = true;
Ok(())
})
}
/// `oakcodec_encoder_write_video`: encode one video frame.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
let f = super::get_box::<Mutex<crate::frame::Frame>>(&frame)
.ok_or(crate::error::Error::Invalid)?;
let e = {
let b = b.lock().unwrap();
if !b.open || b.flushed || b.encoder.is_none() {
return Err(crate::error::Error::State);
}
b.encoder.as_ref().unwrap().clone()
};
let f = f.lock().unwrap();
e.write_video(&f)
.map_err(|_| crate::error::Error::Failed("write_video failed".to_string()))
})
}
/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_write_audio(
encoder: CHandle,
samples: *const f32,
frame_count: c_int,
) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
if (samples.is_null() && frame_count > 0) || frame_count < 0 {
return Err(crate::error::Error::Invalid);
}
let (slice, enc) = {
let b = b.lock().unwrap();
if !b.open || b.flushed || b.encoder.is_none() {
return Err(crate::error::Error::State);
}
let channels = b.params.audio_channel_layout.count_ones();
if channels == 0 {
return Err(crate::error::Error::State);
}
let sample_count = (frame_count as usize).wrapping_mul(channels as usize);
let slice: &[f32] = if samples.is_null() {
&[]
} else {
// SAFETY: the caller guarantees `samples` holds
// `frame_count * channels` floats.
unsafe { std::slice::from_raw_parts(samples, sample_count) }
};
let e = b.encoder.as_ref().unwrap().clone();
(slice, e)
};
enc.write_audio(slice, frame_count)
.map_err(|_| crate::error::Error::Failed("write_audio failed".to_string()))
})
}
/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
encoder: CHandle,
text: *const c_char,
in_seconds: f64,
out_seconds: f64,
) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
let text = match crate::ffi::c_str(text) {
Some(t) => t,
None => return Err(crate::error::Error::Invalid),
};
let e = {
let b = b.lock().unwrap();
if !b.open || b.flushed || b.encoder.is_none() {
return Err(crate::error::Error::State);
}
b.encoder.as_ref().unwrap().clone()
};
e.write_subtitle(&text, in_seconds, out_seconds)
.map_err(|_| crate::error::Error::Failed("write_subtitle failed".to_string()))
})
}
/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and
/// close the file. Idempotent.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_flush(encoder: CHandle) -> c_int {
handle::guard(|| {
let b =
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
let mut b = b.lock().unwrap();
if !b.open {
return Err(crate::error::Error::State);
}
if b.flushed {
return Ok(());
}
let e = b.encoder.as_ref().unwrap().clone();
// The C++ ignores the close() result; the Rust interim surfaces it.
e.close()
.map_err(|_| crate::error::Error::Failed("close failed".to_string()))?;
b.flushed = true;
Ok(())
})
}
/// `oakcodec_encoder_last_error` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_last_error(
encoder: CHandle,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match super::get_box::<Mutex<EncoderBox>>(&encoder) {
Some(b) => {
let b = b.lock().unwrap();
super::string_out(&b.last_error, buf, buf_size)
}
None => super::string_out("", buf, buf_size),
})
}
/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the
/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for
/// an empty/invalid encoder.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -> c_int {
handle::guard_raw(|| {
let b = match super::get_box::<Mutex<EncoderBox>>(&encoder) {
Some(b) => b,
None => return crate::error::OAKCODEC_E_INVALID,
};
let e = match &b.lock().unwrap().encoder {
Some(e) => e.clone(),
None => return crate::error::OAKCODEC_E_INVALID,
};
match e.desired_pixel_format() {
Some(p) => p as c_int,
None => -1,
}
})
}
/// `oakcodec_export_format_get_extension` (two-stage); unknown formats
/// yield the empty string.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_export_format_get_extension(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let ext = match crate::exportformat::Format::from_i32(format) {
Some(f) => crate::exportformat::Format::get_extension(f),
None => String::new(),
};
super::string_out(&ext, buf, buf_size)
})
}
/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling
/// method, row-major 4x4 `double` into `out_matrix[16]`.
#[no_mangle]
pub unsafe extern "C" 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 {
handle::guard(|| {
if out_matrix.is_null() {
return Err(crate::error::Error::Invalid);
}
let mut m = [0.0f64; 16];
crate::encodingparams::EncodingParams::generate_matrix(
crate::encodingparams::scaling_from_i32(method),
src_width,
src_height,
dst_width,
dst_height,
&mut m,
);
// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.
unsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::common::oakcommon_videoparams_init_basic;
use crate::encoder::set_test_encoders;
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
use crate::ffi::frame::{
oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params,
};
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap()
}
fn zeroed_params() -> oakcodec_encoding_params {
unsafe { std::mem::zeroed() }
}
fn valid_params() -> oakcodec_encoding_params {
let mut p = zeroed_params();
p.filename = {
let mut f = [0u8; 1024];
let name = b"out.mp4";
f[..name.len()].copy_from_slice(name);
f
};
p.format = 2; // MPEG-4
p.video_enabled = 1;
p.video_codec = 3;
p.video_width = 1920;
p.video_height = 1080;
p.video_time_base_num = 1;
p.video_time_base_den = 30;
p.video_pixel_format = 0; // U8
p.audio_enabled = 1;
p.audio_codec = 4;
p.audio_sample_rate = 48000;
p.audio_channel_layout = 0x3;
p.audio_sample_format = 10; // f32 packed
p
}
/// Fake encoder that accepts every operation.
struct FakeEncoder {
id: &'static str,
}
impl Encoder for FakeEncoder {
fn id(&self) -> String {
self.id.to_string()
}
fn supports_video(&self) -> bool {
true
}
fn supports_audio(&self) -> bool {
true
}
fn supports_subtitles(&self) -> bool {
true
}
fn configure(&self, _p: &EncodingParams) -> crate::error::Result<()> {
Ok(())
}
fn open(&self) -> crate::error::Result<()> {
Ok(())
}
fn close(&self) -> crate::error::Result<()> {
Ok(())
}
fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> {
Ok(())
}
fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> {
Ok(())
}
fn write_subtitle(
&self,
_text: &str,
_in_seconds: f64,
_out_seconds: f64,
) -> crate::error::Result<()> {
Ok(())
}
fn flush(&self) -> crate::error::Result<()> {
Ok(())
}
fn desired_pixel_format(&self) -> Option<PixelFormat> {
Some(PixelFormat::U8)
}
fn desired_sample_format(&self) -> Option<oakcore_rs::SampleFormat> {
None
}
fn filename(&self) -> String {
"out.mp4".to_string()
}
}
#[test]
fn init_open_write_flush_golden() {
let _g = crate::ffi::lock_tests();
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
let p = valid_params();
let before = handle::alive_count();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(!h.is_null());
assert_eq!(handle::alive_count(), before + 1);
// set_video_option between init and open.
let key = cstr("crf");
let val = cstr("18");
let rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
let rc = unsafe { oakcodec_encoder_open(h) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// write_video with a real frame handle.
let params = unsafe { oakcommon_videoparams_init_basic(16, 16, 0, 4, 1, 1, 0, 1) };
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
assert_eq!(
unsafe { oakcodec_frame_allocate(fh) },
crate::error::OAKCODEC_OK
);
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// write_audio: stereo interleaved floats.
let mut samples = [0f32; 64];
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// write_subtitle.
let text = cstr("hello");
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// desired pixel format from the fake.
assert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8
// flush is idempotent.
let rc = unsafe { oakcodec_encoder_flush(h) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
let rc = unsafe { oakcodec_encoder_flush(h) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// Writes after flush -> E_STATE.
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
assert_eq!(rc, OAKCODEC_E_STATE);
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
assert_eq!(rc, OAKCODEC_E_STATE);
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) };
assert_eq!(rc, OAKCODEC_E_STATE);
unsafe { oakcodec_frame_free(&mut fh) };
unsafe { oakcodec_encoder_free(&mut h) };
assert!(h.is_null());
assert_eq!(handle::alive_count(), before);
set_test_encoders(Vec::new());
}
#[test]
fn init_invalid_config_and_null_params() {
let _g = crate::ffi::lock_tests();
set_test_encoders(Vec::new());
// NULL params -> empty handle.
let mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) };
assert!(h.is_null());
// All tracks disabled -> empty handle (is_valid).
let p = zeroed_params();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(h.is_null());
// Audio-only config is valid.
let mut p = zeroed_params();
p.format = 7; // WAV
p.audio_enabled = 1;
p.audio_sample_rate = 44100;
p.audio_channel_layout = 0x4;
p.audio_sample_format = 10;
let before = handle::alive_count();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(!h.is_null());
assert_eq!(handle::alive_count(), before + 1);
unsafe { oakcodec_encoder_free(&mut h) };
assert_eq!(handle::alive_count(), before);
}
#[test]
fn open_errors_and_state_machine() {
let _g = crate::ffi::lock_tests();
// Production path: an unknown export format cannot create an encoder.
set_test_encoders(Vec::new());
let mut p = valid_params();
p.format = 99; // unknown format -> create_from_params returns None
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(!h.is_null());
let rc = unsafe { oakcodec_encoder_open(h) };
assert_eq!(rc, crate::error::OAKCODEC_E_FAILED);
let mut err = [0i8; 128];
unsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) };
assert_eq!(
crate::ffi::c_str(err.as_ptr()).as_deref(),
Some("failed to create encoder")
);
// Empty handle -> E_INVALID; last_error empty.
let empty = CHandle::null();
assert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID);
assert_eq!(
unsafe {
oakcodec_encoder_set_video_option(empty, cstr("crf").as_ptr(), cstr("18").as_ptr())
},
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoder_get_desired_pixel_format(empty) },
OAKCODEC_E_INVALID
);
let rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) };
assert_eq!(rc, 1);
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(""));
unsafe { oakcodec_encoder_free(&mut h) };
}
#[test]
fn state_and_argument_errors_with_fake() {
let _g = crate::ffi::lock_tests();
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
let p = valid_params();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(!h.is_null());
// set_video_option with a NULL key -> E_INVALID.
let rc =
unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) };
assert_eq!(rc, OAKCODEC_E_INVALID);
// Writes before open -> E_STATE.
let params = unsafe { oakcommon_videoparams_init_basic(4, 4, 0, 4, 1, 1, 0, 1) };
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
assert_eq!(rc, OAKCODEC_E_STATE);
let rc = unsafe { oakcodec_encoder_flush(h) };
assert_eq!(rc, OAKCODEC_E_STATE);
// Null samples with a positive count is an argument error (checked
// before the state, matching the C++ validation order).
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
assert_eq!(rc, OAKCODEC_E_INVALID);
// Valid args before open -> E_STATE.
let mut pre = [0f32; 8];
let rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) };
assert_eq!(rc, OAKCODEC_E_STATE);
unsafe { oakcodec_frame_free(&mut fh) };
unsafe { oakcodec_encoder_free(&mut h) };
set_test_encoders(Vec::new());
}
#[test]
fn write_audio_argument_validation() {
let _g = crate::ffi::lock_tests();
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
let p = valid_params();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert_eq!(
unsafe { oakcodec_encoder_open(h) },
crate::error::OAKCODEC_OK
);
// NULL samples with a positive frame count -> E_INVALID.
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
assert_eq!(rc, OAKCODEC_E_INVALID);
// Negative frame count -> E_INVALID.
let samples = [0f32; 8];
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) };
assert_eq!(rc, OAKCODEC_E_INVALID);
unsafe { oakcodec_encoder_free(&mut h) };
set_test_encoders(Vec::new());
}
#[test]
fn write_audio_zero_channels_is_state() {
let _g = crate::ffi::lock_tests();
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
// Audio enabled but empty channel layout -> E_STATE at write time.
let mut p = zeroed_params();
p.format = 7;
p.audio_enabled = 1;
p.audio_sample_rate = 44100;
p.audio_channel_layout = 0;
p.audio_sample_format = 10;
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert!(!h.is_null());
assert_eq!(
unsafe { oakcodec_encoder_open(h) },
crate::error::OAKCODEC_OK
);
let mut samples = [0f32; 8];
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) };
assert_eq!(rc, OAKCODEC_E_STATE);
unsafe { oakcodec_encoder_free(&mut h) };
set_test_encoders(Vec::new());
}
#[test]
fn export_format_extension_and_generate_matrix() {
let mut buf = [0i8; 64];
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 4); // "mp4" + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp4"));
// Unknown format -> empty string (size 1 for the NUL).
let rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 1);
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(""));
// Truncation rule: small buffer writes buf_size-1 chars + NUL.
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) };
assert_eq!(rc, 4); // required size unchanged
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp"));
// generate_matrix: Stretch (1) is the identity.
let mut m = [9.0f64; 16];
let rc =
unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert_eq!(m[0], 1.0);
assert_eq!(m[5], 1.0);
assert_eq!(m[10], 1.0);
assert_eq!(m[15], 1.0);
// Fit (0) with a square source into a 2:1 destination scales x.
let mut m = [0.0f64; 16];
let rc =
unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert!((m[0] - 0.5).abs() < 1e-9);
assert_eq!(m[5], 1.0);
// NULL out_matrix -> E_INVALID.
let rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) };
assert_eq!(rc, OAKCODEC_E_INVALID);
}
}
-573
View File
@@ -1,573 +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/>.
//! `include/codec/format.h` exports.
//!
//! Complete inventory: format_count / format_name / format_extension /
//! format_{video,audio,subtitle}_codec_{count,at} / codec_name /
//! codec_is_still_image / codec_is_lossless / pix_fmt_{count,at,index} /
//! sample_format_{count,at} / filename_contains_digit_placeholder /
//! image_sequence_digit_count / filename_remove_digit_placeholder.
//!
//! # CPP-PARITY
//! The C++ `c_api/format.cpp` mirrors the facade (oakengine/encoding.h)
//! against the `olive::ExportFormat` / `olive::ExportCodec` / `olive::Encoder`
//! statics. The Rust tables live in [`crate::exportformat`] /
//! [`crate::exportcodec`]; the encoder pixel-format query is bridge-dependent
//! on the C++ side and returns empty here (see
//! `Format::get_pixel_formats_for_codec`), so `oakcodec_encoding_pix_fmt_*`
//! report 0/`E_NOT_FOUND`/0 (the preferred-format fallback) like the C++
//! base `Encoder` default. The filename helpers mirror the `Encoder`
//! statics in [`crate::encoder`].
use std::ffi::{c_char, c_int};
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
use crate::exportcodec::Codec;
use crate::exportformat::Format;
use crate::handle;
/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_count() -> c_int {
handle::guard_raw(|| Format::Count as c_int)
}
/// `oakcodec_encoding_format_name` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_name(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => super::string_out(&Format::get_name(f), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_extension` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_extension(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => super::string_out(&Format::get_extension(f), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_video_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_video_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_video_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_video_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_format_audio_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_audio_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_audio_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_audio_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_format_subtitle_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_subtitle_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_subtitle_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_subtitle_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_codec_name` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_name(
codec: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => super::string_out(&Codec::get_codec_name(c), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => Codec::is_codec_a_still_image(c) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => Codec::is_codec_lossless(c) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_pix_fmt_count`.
///
/// # CPP-PARITY
/// The C++ side instantiates the format's encoder and asks it for the codec's
/// pixel formats; the Rust table is empty (see [`Format::get_pixel_formats_for_codec`]),
/// so the count is 0 — the same as the C++ base `Encoder` default and the
/// C++ result for encoder-less codecs.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
Format::get_pixel_formats_for_codec(f, c).len() as c_int
})
}
/// `oakcodec_encoding_pix_fmt_at` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_at(
format: c_int,
codec: c_int,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
let list = Format::get_pixel_formats_for_codec(f, c);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
// Interim: the Rust list carries no names yet, so this arm is
// unreachable while the list is empty (C++ queries the FFmpeg bridge).
super::string_out(&list[index as usize].to_string(), buf, buf_size)
})
}
/// `oakcodec_encoding_pix_fmt_index` — index of `pix_fmt` in `codec`'s
/// supported pixel formats; 0 (the preferred format) for an invalid codec,
/// a NULL/empty `pix_fmt`, or when not found.
///
/// # CPP-PARITY
/// The C++ side searches the FFmpeg encoder's list. The Rust table is empty,
/// so every lookup falls back to 0 — the documented behavior for "absent".
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_index(
codec: c_int,
pix_fmt: *const c_char,
) -> c_int {
handle::guard_raw(|| {
if Codec::from_i32(codec).is_none() {
return 0;
}
match crate::ffi::c_str(pix_fmt) {
Some(s) if !s.is_empty() => {
// Interim: empty table (see module doc) -> preferred index 0.
let _ = s;
0
}
_ => 0,
}
})
}
/// `oakcodec_encoding_sample_format_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_sample_format_count(
format: c_int,
codec: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
Format::get_sample_formats_for_codec(f, c).len() as c_int
})
}
/// `oakcodec_encoding_sample_format_at` — an
/// `olive::core::SampleFormat::Format` value.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_sample_format_at(
format: c_int,
codec: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
let list = Format::get_sample_formats_for_codec(f, c);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_filename_contains_digit_placeholder(
filename: *const c_char,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => crate::encoder::filename_contains_digit_placeholder(&f) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_image_sequence_digit_count(
filename: *const c_char,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => crate::encoder::image_sequence_placeholder_digit_count(&f),
None => 0,
})
}
/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_filename_remove_digit_placeholder(
filename: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => super::string_out(
&crate::encoder::filename_remove_digit_placeholder(&f),
buf,
buf_size,
),
None => OAKCODEC_E_INVALID,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap()
}
#[test]
fn format_metadata_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
// Count matches the 15-entry table (0..=14, Count = 15).
assert_eq!(unsafe { oakcodec_encoding_format_count() }, 15);
// Matroska (1): "Matroska Video" / "mkv".
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 15); // "Matroska Video" (14) + NUL
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("Matroska Video")
);
let rc = unsafe { oakcodec_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 4); // "mkv" + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mkv"));
// Truncation rule on a two-stage getter.
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 4) };
assert_eq!(rc, 15); // required size unchanged
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Mat"));
// Invalid format -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_format_name(-1, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_format_extension(15, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
); // Count is not a real format
assert_eq!(
unsafe { oakcodec_encoding_format_extension(99, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
}
#[test]
fn format_codec_lists_exports() {
let _g = crate::ffi::lock_tests();
// MPEG-4 video (2) carries H.264/H.264RGB/H.265.
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(2) }, 3);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, 0) },
1 // H.264
);
// WAV (7) has no video codecs but PCM (13) audio.
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(7) }, 0);
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(7) }, 1);
assert_eq!(
unsafe { oakcodec_encoding_format_audio_codec_at(7, 0) },
13 // PCM
);
// SRT (13): subtitle-only, with the SRT (17) codec.
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(13) }, 0);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_count(13) },
1
);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
17 // SRT
);
// Failure paths.
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_count(-1) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, -1) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, 3) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_audio_codec_at(7, 1) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 1) },
OAKCODEC_E_NOT_FOUND
);
}
#[test]
fn codec_metadata_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
let rc = unsafe { oakcodec_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 6); // "H.264" (5) + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("H.264"));
assert_eq!(
unsafe { oakcodec_encoding_codec_name(-1, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
// Still images: PNG (5) yes, H.264 (1) no.
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(5) }, 1);
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(1) }, 0);
// Lossless: PCM (13) yes, AAC (12) no.
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(13) }, 1);
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(12) }, 0);
// Invalid codec -> 0 (not an error) for both flags.
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(99) }, 0);
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(99) }, 0);
}
#[test]
fn pixel_and_sample_format_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
// Bad arguments -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_count(-1, 1) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_count(2, 99) },
OAKCODEC_E_INVALID
);
// Interim: the Rust pixel-format table is empty, so the count is 0
// and any index is E_NOT_FOUND (see the module doc).
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_count(2, 1) }, 0);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 64) },
OAKCODEC_E_NOT_FOUND
);
// pix_fmt_index: absent/empty/NULL/invalid codec all yield 0.
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(1, cstr("yuv420p").as_ptr()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(1, std::ptr::null()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(99, cstr("yuv420p").as_ptr()) },
0
);
// PCM (13) in WAV (7) exposes its native sample formats.
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(7, 13) }, 6);
// f32 packed = 10 (oakcore SampleFormat values match the C++).
assert_eq!(unsafe { oakcodec_encoding_sample_format_at(7, 13, 4) }, 10);
// Out-of-range index -> E_NOT_FOUND; bad args -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_sample_format_at(7, 13, 6) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_sample_format_at(-1, 13, 0) },
OAKCODEC_E_INVALID
);
// Non-PCM codecs query the bridge on the C++ side; empty here.
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(2, 12) }, 0);
}
#[test]
fn filename_helper_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 128];
assert_eq!(
unsafe {
oakcodec_encoding_filename_contains_digit_placeholder(
cstr("/tmp/out_[#####].png").as_ptr(),
)
},
1
);
assert_eq!(
unsafe {
oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out.png").as_ptr())
},
0
);
assert_eq!(
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(std::ptr::null()) },
0
);
assert_eq!(
unsafe {
oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr())
},
5
);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out.png").as_ptr()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) },
0
);
let rc = unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
cstr("/tmp/out_[#####].png").as_ptr(),
buf.as_mut_ptr(),
128,
)
};
assert_eq!(rc, 13); // "/tmp/out.png" (12) + NUL
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("/tmp/out.png")
);
assert_eq!(
unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
std::ptr::null(),
buf.as_mut_ptr(),
128,
)
},
OAKCODEC_E_INVALID
);
}
}
-476
View File
@@ -1,476 +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/>.
//! `include/codec/frame.h` exports.
//!
//! Complete inventory: frame_init / init_with_params / free / get_params /
//! set_params / allocate / is_allocated / data / const_data /
//! allocated_size / linesize_bytes / linesize_pixels / width / height /
//! format / channel_count / get_timestamp / set_timestamp /
//! debug_alive_count.
//!
//! # CPP-PARITY
//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an
//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may
//! alias the decoder's internal cache. The Rust equivalent boxes
//! `Mutex<Frame>`; a decode that hands out a still-shared `Arc<Frame>`
//! therefore cannot be aliased here and reports an empty handle instead
//! (see `ffi::decoder`).
use std::ffi::{c_int, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use oakcore_rs::Rational;
use crate::bridge::common::OakVideoParams;
use crate::frame::Frame;
use crate::handle::{self, CHandle};
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`).
const OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1;
/// `oakcodec_frame_init`: new frame with default (invalid) params,
/// refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle {
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new()))))
}
/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params`
/// (the handle is addref'd internally); buffer unallocated.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_init_with_params(params: OakVideoParams) -> CHandle {
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::with_params(params)))))
}
/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_free(frame: *mut CHandle) {
handle::guard_void(|| super::free_handle(frame));
}
/// `oakcodec_frame_get_params`: copy of the frame's parameter set.
///
/// The copy is addref'd: the caller must release it with
/// `oakcommon_videoparams_free` (see the header contract). Test-stub
/// handles carry no `addref`, so the caller must not free them.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_get_params(
frame: CHandle,
out: *mut OakVideoParams,
) -> c_int {
handle::guard(|| {
if out.is_null() {
return Err(crate::error::Error::Invalid);
}
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
let f = f.lock().unwrap();
let p = f.params().cloned().ok_or(crate::error::Error::Invalid)?;
crate::frame::params_addref(&p);
// SAFETY: the caller guarantees `out` points to a writable
// `OakVideoParams`.
unsafe { *out = p };
Ok(())
})
}
/// `oakcodec_frame_set_params`: replace the parameter set (addref'd
/// internally); recomputes line sizes, does not reallocate.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_set_params(
frame: CHandle,
params: OakVideoParams,
) -> c_int {
handle::guard(|| {
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
f.lock().unwrap().set_params(params);
Ok(())
})
}
/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current
/// params; `OAKCODEC_E_STATE` when the params are invalid.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_allocate(frame: CHandle) -> c_int {
handle::guard(|| {
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
f.lock().unwrap().allocate()
})
}
/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_is_allocated(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
let f = f.lock().unwrap();
if f.is_allocated() {
1
} else {
0
}
})
}
/// `oakcodec_frame_data`: writable pixel buffer, NULL when
/// unallocated/empty.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void {
match catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(&frame) })) {
Ok(p) => p,
Err(_) => std::ptr::null_mut(),
}
}
/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_const_data(frame: CHandle) -> *const c_void {
match catch_unwind(AssertUnwindSafe(|| unsafe {
frame_const_data_inner(&frame)
})) {
Ok(p) => p,
Err(_) => std::ptr::null_mut(),
}
}
unsafe fn frame_data_inner(frame: &CHandle) -> *mut c_void {
let f = match super::get_box::<Mutex<Frame>>(frame) {
Some(f) => f,
None => return std::ptr::null_mut(),
};
match f.lock().unwrap().data_mut() {
Some(d) => d.as_mut_ptr() as *mut c_void,
None => std::ptr::null_mut(),
}
}
unsafe fn frame_const_data_inner(frame: &CHandle) -> *const c_void {
let f = match super::get_box::<Mutex<Frame>>(frame) {
Some(f) => f,
None => return std::ptr::null(),
};
match f.lock().unwrap().data() {
Some(d) => d.as_ptr() as *const c_void,
None => std::ptr::null(),
}
}
/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes
/// (0 when unallocated).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_allocated_size(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().allocated_size() as c_int
})
}
/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes
/// (0 when params are unset).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_linesize_bytes(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().linesize_bytes()
})
}
/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_linesize_pixels(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().linesize_pixels()
})
}
/// `oakcodec_frame_width`: frame width (0 when params are empty).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_width(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().width()
})
}
/// `oakcodec_frame_height`: frame height (0 when params are empty).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_height(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().height()
})
}
/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value;
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_format(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return OAKCOMMON_PIXEL_FORMAT_INVALID,
};
f.lock().unwrap().format() as c_int
})
}
/// `oakcodec_frame_channel_count`: plane channel count of the params
/// format (0 on an empty handle).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_channel_count(frame: CHandle) -> c_int {
handle::guard_raw(|| {
let f = match super::get_box::<Mutex<Frame>>(&frame) {
Some(f) => f,
None => return 0,
};
f.lock().unwrap().channel_count()
})
}
/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number
/// of seconds, written through `numerator`/`denominator`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_get_timestamp(
frame: CHandle,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
handle::guard(|| {
if numerator.is_null() || denominator.is_null() {
return Err(crate::error::Error::Invalid);
}
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
let f = f.lock().unwrap();
let ts = f.timestamp();
// SAFETY: both pointers were range-checked above.
unsafe {
*numerator = ts.numerator() as c_int;
*denominator = ts.denominator() as c_int;
}
Ok(())
})
}
/// `oakcodec_frame_set_timestamp`: replace the frame timestamp.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_frame_set_timestamp(
frame: CHandle,
numerator: c_int,
denominator: c_int,
) -> c_int {
handle::guard(|| {
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
f.lock()
.unwrap()
.set_timestamp(Rational::new(numerator as i64, denominator as i64));
Ok(())
})
}
/// `oakcodec_debug_alive_count`: number of live boxed handle objects
/// across all families (see `crate::handle::alive_count`).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_debug_alive_count() -> c_int {
handle::guard_raw(handle::alive_count)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::common::{
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
oakcommon_videoparams_init_basic,
};
use crate::error::OAKCODEC_E_INVALID;
#[test]
fn frame_lifecycle_golden() {
let _g = crate::ffi::lock_tests();
let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) };
let before = handle::alive_count();
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
assert!(!h.is_null());
// init -> exactly one more live box.
assert_eq!(handle::alive_count(), before + 1);
// get_params round-trips width/height through the stub.
let mut out = empty_params();
let rc = unsafe { oakcodec_frame_get_params(h, &mut out) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
// NOTE: stub handles carry no addref and `oakcommon_videoparams_free`
// would drop the shared box; the test keeps the copy alive for the
// frame's lifetime and does not free it.
assert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100);
assert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50);
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
let rc = unsafe { oakcodec_frame_allocate(h) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1);
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
// U8 RGBA: 100px -> 4*128 bytes linesize.
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
// set_timestamp round-trip.
assert_eq!(
unsafe { oakcodec_frame_set_timestamp(h, 1, 30) },
crate::error::OAKCODEC_OK
);
let (mut num, mut den) = (0, 0);
assert_eq!(
unsafe { oakcodec_frame_get_timestamp(h, &mut num, &mut den) },
crate::error::OAKCODEC_OK
);
assert_eq!((num, den), (1, 30));
unsafe { oakcodec_frame_free(&mut h) };
assert!(h.is_null());
assert_eq!(handle::alive_count(), before);
}
#[test]
fn frame_errors_and_empty_handles() {
let _g = crate::ffi::lock_tests();
let empty = CHandle::null();
assert_eq!(unsafe { oakcodec_frame_width(empty) }, 0);
assert_eq!(
unsafe { oakcodec_frame_format(empty) },
OAKCOMMON_PIXEL_FORMAT_INVALID
);
assert_eq!(
unsafe { oakcodec_frame_allocate(empty) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) },
OAKCODEC_E_INVALID
);
// init_basic(0, 0) is not valid -> allocate rejects with E_STATE.
let params = unsafe { oakcommon_videoparams_init_basic(0, 0, 0, 4, 1, 1, 0, 1) };
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
assert!(!h.is_null());
assert_eq!(
unsafe { oakcodec_frame_allocate(h) },
crate::error::OAKCODEC_E_STATE
);
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
unsafe { oakcodec_frame_free(&mut h) };
}
#[test]
fn free_null_and_empty_are_noops() {
let _g = crate::ffi::lock_tests();
let before = handle::alive_count();
unsafe { oakcodec_frame_free(std::ptr::null_mut()) };
let mut empty = CHandle::null();
unsafe { oakcodec_frame_free(&mut empty) };
assert!(empty.is_null());
assert_eq!(handle::alive_count(), before);
}
#[test]
fn init_set_params_and_query_helpers() {
let _g = crate::ffi::lock_tests();
let before = handle::alive_count();
// Bare init (no params): invalid params, not allocated. The stub's
// default MockParams carries format 0 (U8); the empty-handle -1
// case is covered in `frame_errors_and_empty_handles`.
let mut h = unsafe { oakcodec_frame_init() };
assert!(!h.is_null());
assert_eq!(handle::alive_count(), before + 1);
assert_eq!(unsafe { oakcodec_frame_width(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_height(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0);
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
assert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null());
assert_eq!(
unsafe { oakcodec_frame_allocate(h) },
crate::error::OAKCODEC_E_STATE
);
// set_params replaces the parameter set and recomputes line sizes.
let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) };
let rc = unsafe { oakcodec_frame_set_params(h, params) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
assert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128);
// allocate -> data and const_data point at the buffer.
assert_eq!(
unsafe { oakcodec_frame_allocate(h) },
crate::error::OAKCODEC_OK
);
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
assert!(!unsafe { oakcodec_frame_const_data(h) }.is_null());
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
// get_timestamp rejects NULL out pointers.
assert_eq!(
unsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) },
OAKCODEC_E_INVALID
);
// debug_alive_count reports the live boxes (>= our own).
assert!(unsafe { oakcodec_debug_alive_count() } >= before + 1);
unsafe { oakcodec_frame_free(&mut h) };
assert_eq!(handle::alive_count(), before);
}
fn empty_params() -> OakVideoParams {
OakVideoParams {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: 0,
}
}
}
-118
View File
@@ -1,118 +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 export layer: implements `include/codec/*.h` verbatim.
//!
//! Organization: one submodule per public header (frame / decoder /
//! encoder / format / conform / proxy / task). The authoritative function
//! list is the header itself; each export only unwraps handles, calls the
//! safe Rust domains, and maps results through [`crate::handle::guard*`].
//! `include/codec/error.h` exports macros only, so it is folded into the
//! preamble below instead of getting its own submodule.
//!
//! Shared helpers live here: the two-stage string convention
//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle
//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers.
/// `include/codec/error.h` — macros only, no exported functions.
///
/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as
/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in
/// [`crate::handle`].
pub mod conform;
pub mod decoder;
pub mod encoder;
pub mod format;
pub mod frame;
pub mod proxy;
pub mod task;
use std::ffi::{c_char, c_int};
#[cfg(test)]
use std::sync::Mutex;
use crate::handle::CHandle;
/// Serializes every ffi unit test: they share the global handle ALIVE
/// counter, the injected decoder/encoder registries and the probe error,
/// so exact `alive_count` assertions and registry injection require
/// serial execution. Held poison-tolerant (`into_inner`) so one failing
/// test cannot cascade-fail the rest.
#[cfg(test)]
pub(crate) static TEST_LOCK: Mutex<()> = Mutex::new(());
/// Poison-tolerant lock helper for the ffi tests.
#[cfg(test)]
pub(crate) fn lock_tests() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`).
///
/// Returns the required buffer size including the trailing NUL; when
/// `buf` is non-NULL and `buf_size > 0` the string is copied truncated to
/// `buf_size - 1` bytes and NUL-terminated.
pub(crate) fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
let need = s.len() as c_int + 1;
if !buf.is_null() && buf_size > 0 {
let n = (s.len() as c_int).min(buf_size - 1);
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
unsafe {
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);
*buf.add(n as usize) = 0;
}
}
need
}
/// Read a NUL-terminated C string; `None` on NULL pointers.
pub(crate) fn c_str(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
// SAFETY: `ptr` must be a valid NUL-terminated C string by contract.
let s = unsafe { std::ffi::CStr::from_ptr(ptr) };
Some(s.to_string_lossy().into_owned())
}
/// Release a handle in place and null its `ctx` (`free_handle` in
/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops.
pub(crate) fn free_handle(h: *mut CHandle) {
if h.is_null() {
return;
}
let handle = unsafe { &mut *h };
if handle.ctx.is_null() {
return;
}
if let Some(release) = handle.release {
// SAFETY: `release` targets the box behind `ctx`.
unsafe { release(handle.ctx) };
}
handle.ctx = std::ptr::null_mut();
}
/// Safe view into a handle's boxed value; `None` for empty handles.
///
/// Thin wrapper over [`crate::handle::get`] so the export bodies can call
/// it without `unsafe` blocks everywhere.
///
/// # Safety
/// `T` must be the boxed type; each export asserts it via the handle
/// contract (the same typed box is used by its `make_owned` call).
pub(crate) fn get_box<T: 'static>(h: &CHandle) -> Option<&T> {
unsafe { crate::handle::get::<T>(h) }
}
-484
View File
@@ -1,484 +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/>.
//! `include/codec/proxy.h` exports.
//!
//! Complete inventory: create_instance / destroy_instance / params_default
//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename
//! / get_working_filename / get_or_start / find_ffmpeg.
//!
//! # CPP-PARITY
//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored
//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs
//! are still defined here (rather than reused) so the ffi layer never
//! depends on the crate-internal type's layout. A NULL `params` maps to
//! the crate's compiled-in defaults (`ProxyParams::default`), matching
//! `to_native` in `c_api/proxy.cpp`.
use std::ffi::{c_char, c_int};
use crate::handle;
use crate::proxymanager::{ProxyManager, ProxyParams, ProxyState};
/// `OAKCODEC_PROXY_STATE_MISSING`.
pub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0;
/// `OAKCODEC_PROXY_STATE_GENERATING`.
pub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1;
/// `OAKCODEC_PROXY_STATE_READY`.
pub const OAKCODEC_PROXY_STATE_READY: c_int = 2;
/// `OAKCODEC_PROXY_STATE_FAILED`.
pub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3;
/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`.
#[allow(missing_docs)]
#[repr(C)]
pub struct oakcodec_proxy_params {
pub width: c_int,
pub height: c_int,
pub divider: c_int,
pub version: c_int,
pub crf: c_int,
pub include_audio: c_int,
pub extension: [u8; 32],
pub preset: [u8; 32],
}
/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`.
#[allow(missing_docs)]
#[repr(C)]
pub struct oakcodec_proxy_result {
pub state: c_int,
pub filename: [u8; 1024],
}
/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in
/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults.
fn to_native(p: *const oakcodec_proxy_params) -> ProxyParams {
if p.is_null() {
return ProxyParams::default();
}
let p = unsafe { &*p };
ProxyParams {
width: p.width,
height: p.height,
divider: p.divider,
version: p.version,
crf: p.crf,
include_audio: p.include_audio,
extension: p.extension,
preset: p.preset,
}
}
/// Copy a NUL-terminated byte array into a C buffer (truncated).
fn copy_cstr(src: &[u8], dst: &mut [u8]) {
dst.fill(0);
let n = src.len().min(dst.len().saturating_sub(1));
dst[..n].copy_from_slice(&src[..n]);
}
/// `oakcodec_proxy_create_instance`: create the singleton (always present
/// here, so a no-op).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_create_instance() -> c_int {
handle::guard_raw(|| {
let _ = ProxyManager::instance();
crate::error::OAKCODEC_OK
})
}
/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust
/// manager is stateless, so a no-op).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_destroy_instance() -> c_int {
handle::guard_raw(|| crate::error::OAKCODEC_OK)
}
/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in
/// default proxy parameters.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -> c_int {
handle::guard(|| {
if out.is_null() {
return Err(crate::error::Error::Invalid);
}
let n = ProxyManager::proxy_params_from_config();
unsafe {
(*out).width = n.width;
(*out).height = n.height;
(*out).divider = n.divider;
(*out).version = n.version;
(*out).crf = n.crf;
(*out).include_audio = n.include_audio;
copy_cstr(&n.extension, &mut (*out).extension);
copy_cstr(&n.preset, &mut (*out).preset);
}
Ok(())
})
}
/// `oakcodec_proxy_get_state`: state of a proxy file on disk
/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -> c_int {
handle::guard_raw(|| {
let f = match crate::ffi::c_str(proxy_filename) {
Some(f) if !f.is_empty() => f,
_ => return OAKCODEC_PROXY_STATE_MISSING,
};
ProxyManager::get_proxy_state(&f) as c_int
})
}
/// `oakcodec_proxy_state_to_string` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_state_to_string(
state: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let s = match state {
0 => ProxyManager::proxy_state_to_string(ProxyState::Missing),
1 => ProxyManager::proxy_state_to_string(ProxyState::Generating),
2 => ProxyManager::proxy_state_to_string(ProxyState::Ready),
3 => ProxyManager::proxy_state_to_string(ProxyState::Failed),
_ => return crate::error::OAKCODEC_E_INVALID,
};
super::string_out(&s, buf, buf_size)
})
}
/// `oakcodec_proxy_get_proxy_directory` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_directory(
cache_path: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let cache = match crate::ffi::c_str(cache_path) {
Some(c) => c,
None => return crate::error::OAKCODEC_E_INVALID,
};
match ProxyManager::get_proxy_directory(&cache) {
Ok(s) => super::string_out(&s, buf, buf_size),
Err(_) => crate::error::OAKCODEC_E_FAILED,
}
})
}
/// `oakcodec_proxy_get_proxy_filename` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_filename(
cache_path: *const c_char,
source_filename: *const c_char,
stream_index: c_int,
params: *const oakcodec_proxy_params,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let cache = match crate::ffi::c_str(cache_path) {
Some(c) => c,
None => return crate::error::OAKCODEC_E_INVALID,
};
let source = match crate::ffi::c_str(source_filename) {
Some(s) => s,
None => return crate::error::OAKCODEC_E_INVALID,
};
let native = to_native(params);
match ProxyManager::get_proxy_filename(&cache, &source, stream_index, &native) {
Ok(s) => super::string_out(&s, buf, buf_size),
Err(_) => crate::error::OAKCODEC_E_FAILED,
}
})
}
/// `oakcodec_proxy_get_working_filename` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_get_working_filename(
proxy_filename: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let proxy = match crate::ffi::c_str(proxy_filename) {
Some(p) => p,
None => return crate::error::OAKCODEC_E_INVALID,
};
match ProxyManager::get_working_filename(&proxy) {
Ok(s) => super::string_out(&s, buf, buf_size),
Err(_) => crate::error::OAKCODEC_E_FAILED,
}
})
}
/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for
/// `source_filename`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_get_or_start(
cache_path: *const c_char,
source_filename: *const c_char,
stream_index: c_int,
params: *const oakcodec_proxy_params,
out: *mut oakcodec_proxy_result,
) -> c_int {
handle::guard(|| {
if out.is_null() {
return Err(crate::error::Error::Invalid);
}
let cache = match crate::ffi::c_str(cache_path) {
Some(c) => c,
None => return Err(crate::error::Error::Invalid),
};
let source = match crate::ffi::c_str(source_filename) {
Some(s) => s,
None => return Err(crate::error::Error::Invalid),
};
let native = to_native(params);
let (state, filename) = ProxyManager::instance()
.get_or_start(&cache, &source, stream_index, &native)
.map_err(|_| crate::error::Error::Failed("get_or_start failed".to_string()))?;
unsafe {
let out_ref = &mut *out;
out_ref.state = state as c_int;
// Truncate to 1023 chars + NUL, matching `snprintf` in
// `c_api/proxy.cpp`.
let n = filename.len().min(1023);
out_ref.filename[..n].copy_from_slice(&filename.as_bytes()[..n]);
out_ref.filename[n] = 0;
}
Ok(())
})
}
/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_proxy_find_ffmpeg(
configured_path: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let configured = crate::ffi::c_str(configured_path).unwrap_or_default();
let s = ProxyManager::find_ffmpeg(&configured);
super::string_out(&s, buf, buf_size)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
use crate::task::set_task_submit_cb_extern;
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap()
}
fn temp_cache(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"oakcodec_ffi_proxy_{}_{}",
name,
std::process::id()
));
let _ = std::fs::create_dir_all(&dir);
dir.to_string_lossy().into_owned()
}
fn defaults() -> oakcodec_proxy_params {
let mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() };
let rc = unsafe { oakcodec_proxy_params_default(&mut p) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
p
}
#[test]
fn create_destroy_and_params_default() {
let _g = crate::ffi::lock_tests();
assert_eq!(
unsafe { oakcodec_proxy_create_instance() },
crate::error::OAKCODEC_OK
);
assert_eq!(
unsafe { oakcodec_proxy_destroy_instance() },
crate::error::OAKCODEC_OK
);
let p = defaults();
assert_eq!(p.width, 1280);
assert_eq!(p.height, 720);
assert_eq!(p.divider, 1);
assert_eq!(p.version, 1);
assert_eq!(p.crf, 23);
assert_eq!(p.include_audio, 1);
assert_eq!(&p.extension[..3], b"mp4");
assert_eq!(&p.preset[..8], b"veryfast");
let rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) };
assert_eq!(rc, OAKCODEC_E_INVALID);
}
#[test]
fn get_state_and_state_to_string() {
let _g = crate::ffi::lock_tests();
let cache = temp_cache("state");
let p = defaults();
let src = cstr("media.mp4");
let cache_c = cstr(&cache);
// Resolve the proxy filename, then query its state.
let mut name = [0i8; 1024];
let rc = unsafe {
oakcodec_proxy_get_proxy_filename(
cache_c.as_ptr(),
src.as_ptr(),
0,
&p,
name.as_mut_ptr(),
1024,
)
};
assert!(rc > 0);
let proxy = crate::ffi::c_str(name.as_ptr()).unwrap();
assert!(proxy.contains("1280x720"));
// Missing by default.
let pc = cstr(&proxy);
assert_eq!(
unsafe { oakcodec_proxy_get_state(pc.as_ptr()) },
OAKCODEC_PROXY_STATE_MISSING
);
assert_eq!(
unsafe { oakcodec_proxy_get_state(std::ptr::null()) },
OAKCODEC_PROXY_STATE_MISSING
);
// Ready once the file exists.
std::fs::create_dir_all(std::path::Path::new(&proxy).parent().unwrap()).unwrap();
std::fs::write(&proxy, b"x").unwrap();
assert_eq!(
unsafe { oakcodec_proxy_get_state(pc.as_ptr()) },
OAKCODEC_PROXY_STATE_READY
);
// state_to_string mapping + invalid range.
let mut buf = [0i8; 64];
let rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 6); // "ready" + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("ready"));
let rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) };
assert_eq!(rc, OAKCODEC_E_INVALID);
}
#[test]
fn directory_working_and_get_or_start() {
let _g = crate::ffi::lock_tests();
let cache = temp_cache("getorstart");
let cache_c = cstr(&cache);
let src = cstr("media.mp4");
let p = defaults();
// get_proxy_directory.
let mut buf = [0i8; 512];
let rc =
unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) };
assert!(rc > 0);
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some(format!("{}/proxy", cache).as_str())
);
// get_working_filename appends ".working.mp4".
let proxy = format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, 12345);
let pc = cstr(&proxy);
let rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) };
assert!(rc > 0);
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some(format!("{}.working.mp4", proxy).as_str())
);
// get_or_start without a registrar -> Missing.
let _g = REG_LOCK.lock().unwrap();
set_task_submit_cb_extern(None, std::ptr::null_mut());
let mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() };
let rc =
unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING);
// With a registrar and no files -> Generating.
set_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut());
let rc =
unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
assert_eq!(rc, crate::error::OAKCODEC_OK);
assert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING);
// Invalid args.
let rc =
unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, &p, &mut out) };
assert_eq!(rc, OAKCODEC_E_INVALID);
let rc = unsafe {
oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, std::ptr::null_mut())
};
assert_eq!(rc, OAKCODEC_E_INVALID);
// get_proxy_directory / get_proxy_filename / get_working_filename
// argument validation.
let rc =
unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) };
assert_eq!(rc, OAKCODEC_E_INVALID);
let rc = unsafe {
oakcodec_proxy_get_proxy_filename(
std::ptr::null(),
src.as_ptr(),
0,
&p,
buf.as_mut_ptr(),
512,
)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
let rc =
unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) };
assert_eq!(rc, OAKCODEC_E_INVALID);
set_task_submit_cb_extern(None, std::ptr::null_mut());
}
#[test]
fn find_ffmpeg_uses_configured_path() {
let _g = crate::ffi::lock_tests();
// The current test binary is a real executable: the configured
// path resolves to an absolute path.
let me = std::env::current_exe().unwrap();
let mc = cstr(me.to_str().unwrap());
let mut buf = [0i8; 1024];
let rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) };
assert!(rc > 0);
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
assert!(found.starts_with('/'));
// NULL configured path falls back to the search (empty or absolute).
let rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) };
assert!(rc > 0);
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
assert!(found.is_empty() || found.starts_with('/'));
}
}
-73
View File
@@ -1,73 +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/>.
//! `include/codec/task.h` exports.
//!
//! Complete inventory: set_task_submit_cb / task_submit_is_registered.
//! The callback typedef and request struct are mirrored in
//! [`crate::task`].
use std::ffi::c_int;
use crate::handle;
/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with
/// NULL) the global task submit callback.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_set_task_submit_cb(
cb: Option<crate::task::OakCodecTaskSubmitFn>,
userdata: *mut std::ffi::c_void,
) {
handle::guard_void(|| {
crate::task::set_task_submit_cb_extern(cb, userdata);
})
}
/// `oakcodec_task_submit_is_registered`: 1 when a callback is set.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_task_submit_is_registered() -> c_int {
handle::guard_raw(|| {
if crate::task::task_submit_is_registered() {
1
} else {
0
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
use crate::task::set_task_submit_cb_extern;
#[test]
fn register_query_clear() {
let _g = crate::ffi::lock_tests();
let _g = REG_LOCK.lock().unwrap();
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
unsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) };
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1);
unsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) };
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
// Restore a clean slate for the other modules.
set_task_submit_cb_extern(None, std::ptr::null_mut());
}
}
+160 -95
View File
@@ -56,32 +56,20 @@ use ffmpeg::software::{resampling, scaling};
use ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};
use ffmpeg_next as ffmpeg;
use oakcommon::cancelatom::CancelAtom;
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
use oakcommon::videoparams::{Interlacing, VideoParams, VideoType};
use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};
use crate::bridge::common::{
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_channel_count,
oakcommon_videoparams_set_duration, oakcommon_videoparams_set_format,
oakcommon_videoparams_set_frame_rate, oakcommon_videoparams_set_height,
oakcommon_videoparams_set_interlacing, oakcommon_videoparams_set_pixel_aspect_ratio,
oakcommon_videoparams_set_premultiplied_alpha, oakcommon_videoparams_set_start_time,
oakcommon_videoparams_set_stream_index, oakcommon_videoparams_set_time_base,
oakcommon_videoparams_set_video_type, oakcommon_videoparams_set_width,
oakcore_audioparams_create, oakcore_audioparams_set_duration,
oakcore_audioparams_set_stream_index, oakcore_audioparams_set_time_base, OakAudioParams,
};
use crate::bridge::render::{oakrender_cancelatom_is_cancelled, OakCancelAtom, OakRenderTexture};
use crate::decoder::{CodecStream, Decoder, RetrieveAudioStatus, RetrieveVideoParams};
use crate::audioparams::AudioParams;
use crate::decoder::{CodecStream, Decoder, OakRenderTexture, RetrieveAudioStatus, RetrieveVideoParams};
use crate::encoder::Encoder;
use crate::encodingparams::EncodingParams;
use crate::footagedescription::{FootageDescription, StreamEntry};
use crate::frame::Frame;
/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).
const OAKCOMMON_VIDEO_INTERLACE_NONE: i32 = 0;
/// `OAKCOMMON_COLOR_RANGE_FULL`.
const OAKCOMMON_COLOR_RANGE_FULL: i32 = 1;
/// `OAKCOMMON_VIDEO_TYPE_VIDEO`.
const OAKCOMMON_VIDEO_TYPE_VIDEO: i32 = 0;
/// The format-level time base (microseconds), `FB_TIME_BASE` in the bridge.
const FB_TIME_BASE: i64 = 1_000_000;
/// `AV_NOPTS_VALUE`.
@@ -112,18 +100,12 @@ fn fail(msg: impl Into<String>) -> crate::error::Error {
crate::error::Error::Failed(msg.into())
}
/// `cancel_atom_is_cancelled` — NULL/empty-handle-safe check of an
/// oakrender cancel atom (borrowed pointer).
/// `cancel_atom_is_cancelled` — check of a cancel atom (borrowed pointer).
///
/// # CPP-PARITY
/// `src/codec/src/ffmpeg/ffmpegdecoder.cpp` (anonymous namespace helper).
fn cancel_atom_is_cancelled(cancelled: Option<&OakCancelAtom>) -> bool {
match cancelled {
Some(atom) if !atom.ctx.is_null() => unsafe {
oakrender_cancelatom_is_cancelled(atom.clone()) != 0
},
_ => false,
}
fn cancel_atom_is_cancelled(cancelled: Option<&CancelAtom>) -> bool {
cancelled.is_some_and(|atom| atom.is_cancelled())
}
/// Whether an FFmpeg error means "end of stream" or "try again".
@@ -261,7 +243,7 @@ impl Decoder for FFmpegDecoder {
fn probe(
&self,
filename: &str,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> Option<FootageDescription> {
ffmpeg_init().ok()?;
probe_file(filename, cancelled)
@@ -377,7 +359,7 @@ impl Decoder for FFmpegDecoder {
sample_rate: i32,
channel_layout: u64,
sample_format: i32,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()> {
ffmpeg_init()?;
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
@@ -705,7 +687,7 @@ impl DecoderState {
&mut self,
time: &Rational,
any_timecode: bool,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> crate::error::Result<Option<ffmpeg::frame::Video>> {
// Move the video state out so `self.seek` / `self.pull` (which touch
// other fields) can be called without conflicting borrows.
@@ -1023,7 +1005,7 @@ impl DecoderState {
sample_rate: i32,
channel_layout: u64,
sample_format: i32,
cancelled: Option<&OakCancelAtom>,
cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()> {
if self.input_channel_layout_mask == 0 {
return Err(fail(
@@ -1456,11 +1438,18 @@ fn copy_rgba_f32_to_frame(
bytes: &[u8],
timestamp: Rational,
) -> crate::error::Result<Frame> {
let params = unsafe { oakcommon_videoparams_init_basic(width as i32, height as i32, 0, 4, 1, 1, 0, 1) };
unsafe {
oakcommon_videoparams_set_format(params.clone(), PixelFormat::F32 as i32);
oakcommon_videoparams_set_channel_count(params.clone(), VIDEO_CHANNELS);
}
let mut params = VideoParams::new_basic(
width as i32,
height as i32,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
params.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
params.set_channel_count(VIDEO_CHANNELS);
let mut frame = Frame::with_params(params);
frame.set_timestamp(timestamp);
frame.allocate()?;
@@ -1490,7 +1479,7 @@ fn copy_rgba_f32_to_frame(
/// stream details are taken from stream parameters (no second decode pass),
/// so `is_still` is always false and interlacing always progressive;
/// subtitle streams are counted but not added.
fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<FootageDescription> {
fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option<FootageDescription> {
let mut dict = Dictionary::new();
dict.set("analyzeduration", "5000000");
dict.set("probesize", "20000000");
@@ -1545,29 +1534,25 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
let frame_rate = stream.avg_frame_rate();
let tb = stream.time_base();
let vp = unsafe { oakcommon_videoparams_init_basic(1, 1, 0, 4, 1, 1, 0, 1) };
let mut vp =
VideoParams::new_basic(1, 1, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
vp.set_stream_index(i as i32);
// SAFETY: `raw` points at the live stream's parameters (from
// `params.as_ptr()` above), valid for the duration of `probe_file`.
unsafe {
oakcommon_videoparams_set_stream_index(vp.clone(), i as i32);
oakcommon_videoparams_set_width(vp.clone(), (*raw).width);
oakcommon_videoparams_set_height(vp.clone(), (*raw).height);
oakcommon_videoparams_set_video_type(vp.clone(), OAKCOMMON_VIDEO_TYPE_VIDEO);
oakcommon_videoparams_set_format(vp.clone(), native as i32);
oakcommon_videoparams_set_channel_count(vp.clone(), VIDEO_CHANNELS);
oakcommon_videoparams_set_interlacing(
vp.clone(),
OAKCOMMON_VIDEO_INTERLACE_NONE,
);
oakcommon_videoparams_set_pixel_aspect_ratio(vp.clone(), 1, 1);
oakcommon_videoparams_set_frame_rate(
vp.clone(),
frame_rate.0 as i64,
frame_rate.1 as i64,
);
oakcommon_videoparams_set_start_time(vp.clone(), stream.start_time());
oakcommon_videoparams_set_time_base(vp.clone(), tb.0 as i64, tb.1 as i64);
oakcommon_videoparams_set_duration(vp.clone(), stream.duration());
oakcommon_videoparams_set_premultiplied_alpha(vp.clone(), 0);
vp.set_width((*raw).width);
vp.set_height((*raw).height);
}
vp.set_video_type(VideoType::Video);
vp.set_format(OakPixelFormat::from_code(native as i32));
vp.set_channel_count(VIDEO_CHANNELS);
vp.set_interlacing(Interlacing::None);
vp.set_pixel_aspect_ratio(1, 1);
vp.set_frame_rate(frame_rate.0 as i32, frame_rate.1 as i32);
vp.set_start_time(stream.start_time());
vp.set_time_base(tb.0 as i32, tb.1 as i32);
vp.set_duration(stream.duration());
vp.set_premultiplied_alpha(false);
desc.push_stream(StreamEntry::Video(vp));
}
MediaType::Audio => {
@@ -1584,22 +1569,26 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
};
}
let sample_rate = unsafe { (*raw).sample_rate };
let layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
let ap = unsafe { oakcore_audioparams_create(sample_rate, layout_mask, 0) };
if !ap.is_null() {
let tb = stream.time_base();
unsafe {
oakcore_audioparams_set_stream_index(ap, i as i32);
oakcore_audioparams_set_duration(ap, stream_duration);
oakcore_audioparams_set_time_base(ap, tb.0 as i32, tb.1 as i32);
}
desc.push_stream(StreamEntry::Audio(OakAudioParams {
ctx: ap as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
}));
}
let raw_layout = unsafe { ChannelLayout::from((*raw).ch_layout) };
// Count-only layouts (WAV and other PCM containers report
// AV_CHANNEL_ORDER_UNSPEC with a channel count but no mask)
// yield a zero mask; derive a default mask from the count so
// the stream stays usable (CPP-PARITY channel_layout_from_mask
// fallback in the audio processors).
let layout_mask = if raw_layout.bits() == 0 && raw_layout.channels() > 0 {
ChannelLayout::default(raw_layout.channels()).bits()
} else {
raw_layout.bits()
};
let tb = stream.time_base();
desc.push_stream(StreamEntry::Audio(AudioParams {
sample_rate,
channel_layout: layout_mask,
format: 0,
stream_index: i as i32,
duration: stream_duration,
time_base: (tb.0 as i32, tb.1 as i32),
}));
}
_ => {}
}
@@ -1699,6 +1688,12 @@ struct VideoEncoderState {
scaler: scaling::Context,
width: u32,
height: u32,
/// The encoder's time base after `open` (the frame PTS are expressed
/// in it; see `FFmpegEncoder::open`).
time_base: FfRational,
/// One frame in the encoder's time base (the last packet's duration;
/// see `FFmpegEncoder::open`).
frame_duration: i64,
}
/// Opened audio encoder + its conversion resampler.
@@ -1877,7 +1872,6 @@ impl EncoderState {
.video()
.map_err(ffmpeg_err)?;
stream.set_parameters(&encoder);
stream.set_time_base(time_base);
encoder.set_width(width);
encoder.set_height(height);
@@ -1886,7 +1880,19 @@ impl EncoderState {
params.video_pixel_aspect_den.max(1),
));
encoder.set_frame_rate(Some(frame_rate));
encoder.set_time_base(time_base);
// The codecs' packet timestamps use a fine tick (x264 encodes at
// 1024 ticks per frame); give H.264 an encoder time base scaled
// to that so the frame PTS stay integral, and sync the stream to
// the encoder's ACTUAL post-open time base (the value the muxer
// reads) so the container timing is `seconds * frame_rate`.
// Other codecs (e.g. MPEG-2) reject the scaled rate and keep the
// nominal frame-duration time base.
let tick = if codec_id == ffmpeg::codec::Id::H264 {
FfRational(time_base.0, time_base.1 * 1024)
} else {
time_base
};
encoder.set_time_base(tick);
if params.video_bit_rate > 0 {
encoder.set_bit_rate(params.video_bit_rate as usize);
}
@@ -1902,6 +1908,20 @@ impl EncoderState {
let opened = encoder.open().map_err(|e| { eprintln!("DBG-AUD: audio open failed: {e:?}"); ffmpeg_err(e) })?;
stream.set_parameters(&opened);
// The encoder may adjust the time base during `open` (x264
// picks its own); sync the stream to the encoder's ACTUAL time
// base so the container timing matches the frame PTS computed
// from it (a mismatched stream time base crams the whole video
// into the first milliseconds).
let time_base = opened.time_base();
stream.set_time_base(time_base);
// One frame in the encoder's time base, used to fill the last
// packet's duration: the muxer normally derives it from the
// codec context attached to the stream, but the ffmpeg-next
// flow never attaches one, so the final frame would carry
// duration 0 and the track would be one frame short.
let frame_duration = (time_base.1 as i64 * i64::from(frame_rate.1))
/ (i64::from(time_base.0) * i64::from(frame_rate.0)).max(1);
let scaler = scaling::Context::get(
Pixel::RGBA,
@@ -1920,6 +1940,8 @@ impl EncoderState {
scaler,
width,
height,
time_base,
frame_duration,
});
}
@@ -1987,7 +2009,7 @@ impl EncoderState {
}
/// Encode one frame (F32 RGBA) into the open output.
fn write_video(&mut self, frame: &Frame, params: &EncodingParams) -> crate::error::Result<()> {
fn write_video(&mut self, frame: &Frame, _params: &EncodingParams) -> crate::error::Result<()> {
let output = self
.output
.as_mut()
@@ -2040,12 +2062,11 @@ impl EncoderState {
// # CPP-PARITY
// `FFmpegEncoder::write_frame` passes the frame time in seconds; the
// Rust `Frame` carries the timestamp as a rational.
// Rust `Frame` carries the timestamp as a rational. The PTS is
// expressed in the encoder's own time base (captured at open), so
// the container timing is exactly `seconds * rate`.
let secs = frame.timestamp().to_f64();
let tb = FfRational(
params.video_time_base_num.max(1),
params.video_time_base_den.max(1),
);
let tb = video.time_base;
let pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;
scaled.set_pts(Some(pts));
@@ -2078,22 +2099,57 @@ impl EncoderState {
let channels = audio.resampler.dst_channels.max(1);
let in_frames = samples.len() / channels;
// Presentation timestamp in the output stream time base (1/sample_rate).
let pts = output.audio_pts;
// The encoder accepts at most `frame_size` samples per frame (AAC:
// 1024), so the (possibly whole-range) input buffer is split into
// chunks. The resampler converts at the same rate (the rendered
// audio rate equals the encoder rate), but swr may buffer a small
// delay, so the input chunk is shrunk until its predicted output
// fits `frame_size`.
let frame_size = audio.encoder.frame_size().max(1) as usize;
let mut offset = 0usize;
while offset < in_frames {
let mut take = frame_size.min(in_frames - offset);
loop {
let out = unsafe { sys::swr_get_out_samples(audio.resampler.ctx.as_mut_ptr(), take as i32) };
if out >= 0 && out as usize <= frame_size {
break;
}
take = take.saturating_sub(1);
if take == 0 {
take = 1;
break;
}
}
let layout = channel_layout_from_mask(params.audio_channel_layout);
let mut input =
ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), in_frames, layout);
let bytes =
unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4) };
input.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
// Presentation timestamp in the output stream time base (1/sample_rate).
let pts = output.audio_pts;
let layout = channel_layout_from_mask(params.audio_channel_layout);
let chunk = &samples[offset * channels..(offset + take) * channels];
let mut input = ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), take, layout);
let bytes = unsafe {
std::slice::from_raw_parts(chunk.as_ptr() as *const u8, chunk.len() * 4)
};
input.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| { eprintln!("DBG-AUD: convert failed: {e:?}"); fail(format!("{e:?}")) })?;
converted.set_pts(Some(pts));
output.audio_pts += converted.samples() as i64;
audio.encoder.send_frame(&converted).map_err(|e| { eprintln!("DBG-AUD: send failed: {e:?}"); ffmpeg_err(e) })?;
drain_audio_packets(&mut output.output, audio)
let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| {
eprintln!("DBG-AUD: convert failed: {e:?}");
fail(format!("{e:?}"))
})?;
if converted.samples() > 0 {
converted.set_pts(Some(pts));
output.audio_pts += converted.samples() as i64;
audio
.encoder
.send_frame(&converted)
.map_err(|e| {
eprintln!("DBG-AUD: send failed: {e:?}");
ffmpeg_err(e)
})?;
drain_audio_packets(&mut output.output, audio)?;
}
offset += take;
}
Ok(())
}
/// Flush encoders, write the trailer and close the output (idempotent).
@@ -2128,6 +2184,9 @@ fn drain_video_encoder(
match video.encoder.receive_packet(&mut pkt) {
Ok(()) => {
pkt.set_stream(video.stream_index);
if pkt.duration() <= 0 {
pkt.set_duration(video.frame_duration);
}
pkt.write_interleaved(output).map_err(ffmpeg_err)?;
}
Err(e) if is_eof_or_eagain(&e) => break,
@@ -2167,6 +2226,12 @@ fn drain_video_packets(
match video.encoder.receive_packet(&mut pkt) {
Ok(()) => {
pkt.set_stream(video.stream_index);
// The final frame carries no duration (see the
// `frame_duration` note); fill it so the track length is
// the full export range.
if pkt.duration() <= 0 {
pkt.set_duration(video.frame_duration);
}
pkt.write_interleaved(output).map_err(ffmpeg_err)?;
}
Err(e) if is_eof_or_eagain(&e) => break,
+36 -31
View File
@@ -23,19 +23,21 @@
//! `Track::Type` mapping and XML load/save are intentionally not reproduced
//! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc.
use oakcommon::subtitleparams::SubtitleParams;
use oakcommon::videoparams::VideoParams;
use oakcore_rs::{Rational, TimeRange};
use crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams};
use crate::audioparams::AudioParams;
/// One stream entry in a footage description.
#[derive(Clone, Debug)]
pub enum StreamEntry {
/// A video stream (params handle, addref'd).
Video(OakVideoParams),
/// A video stream.
Video(VideoParams),
/// An audio stream.
Audio(OakAudioParams),
/// A subtitle stream (params handle, addref'd).
Subtitle(OakSubtitleParams),
Audio(AudioParams),
/// A subtitle stream.
Subtitle(SubtitleParams),
}
/// `olive::FootageDescription` — the decoder name plus stream inventory.
@@ -127,7 +129,7 @@ impl FootageDescription {
}
/// The `index`-th video stream's params (by video-stream ordinal).
pub fn get_video_stream(&self, index: usize) -> Option<&OakVideoParams> {
pub fn get_video_stream(&self, index: usize) -> Option<&VideoParams> {
self.streams
.iter()
.filter_map(|s| match s {
@@ -138,7 +140,7 @@ impl FootageDescription {
}
/// The `index`-th audio stream's params (by audio-stream ordinal).
pub fn get_audio_stream(&self, index: usize) -> Option<&OakAudioParams> {
pub fn get_audio_stream(&self, index: usize) -> Option<&AudioParams> {
self.streams
.iter()
.filter_map(|s| match s {
@@ -149,7 +151,7 @@ impl FootageDescription {
}
/// The `index`-th subtitle stream's params (by subtitle-stream ordinal).
pub fn get_subtitle_stream(&self, index: usize) -> Option<&OakSubtitleParams> {
pub fn get_subtitle_stream(&self, index: usize) -> Option<&SubtitleParams> {
self.streams
.iter()
.filter_map(|s| match s {
@@ -210,31 +212,34 @@ impl FootageDescription {
mod tests {
use super::*;
fn video_params(index: i32) -> OakVideoParams {
OakVideoParams {
ctx: index as usize as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
fn video_params(index: i32) -> VideoParams {
let mut vp = VideoParams::new_basic(
1920,
1080,
oakcommon::ocioutils::PixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
vp.set_stream_index(index);
vp
}
fn audio_params() -> AudioParams {
AudioParams {
sample_rate: 48000,
channel_layout: 0x3,
format: 0,
stream_index: 1,
duration: 0,
time_base: (1, 48000),
}
}
fn audio_params() -> OakAudioParams {
OakAudioParams {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
}
}
fn subtitle_params() -> OakSubtitleParams {
OakSubtitleParams {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
}
fn subtitle_params() -> SubtitleParams {
SubtitleParams::new()
}
#[test]
+39 -92
View File
@@ -14,19 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle.
//! `olive::Frame` — a CPU pixel buffer plus a [`VideoParams`] value.
//!
//! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon
//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the
//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is
//! unchanged; the pixel data itself is a plain `Vec<u8>`. Line-size and
//! pixel-format math lives here.
//! [`VideoParams`] value (single-lib unification; the former refcounted
//! oakcommon handle is gone, so copies are plain clones); the pixel data
//! itself is a plain `Vec<u8>`. Line-size and pixel-format math lives
//! here.
use crate::bridge::common::{
oakcommon_videoparams_free, oakcommon_videoparams_get_format, oakcommon_videoparams_get_height,
oakcommon_videoparams_get_is_valid, oakcommon_videoparams_get_width,
oakcommon_videoparams_init, OakVideoParams,
};
use oakcommon::videoparams::VideoParams;
use oakcore_rs::{PixelFormat, Rational};
/// Number of channels in the internal RGBA pipeline layout
@@ -47,11 +43,11 @@ pub enum Interlacing {
BottomFieldFirst = 2,
}
/// `olive::Frame`: reference-counted CPU pixel buffer + params handle.
/// `olive::Frame`: reference-counted CPU pixel buffer + params value.
#[derive(Debug)]
pub struct Frame {
/// Video parameter set (oakcommon handle, refcounted).
pub params: Option<OakVideoParams>,
/// Video parameter set.
pub params: Option<VideoParams>,
/// Pixel buffer (unallocated until `allocate`).
data: Vec<u8>,
/// Distance between rows in bytes (0 until params are set).
@@ -87,40 +83,11 @@ fn bytes_per_pixel(format: PixelFormat, channels: i32) -> i32 {
(format.bytes_per_channel() as i32) * channels
}
/// Increment the refcount of a params handle (a no-op for test-stub handles
/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out
/// addref'd copies (`oakcodec_frame_get_params`).
pub(crate) fn params_addref(p: &OakVideoParams) {
if let Some(addref) = p.addref {
// SAFETY: `addref` is a valid C function pointer targeting `ctx`.
unsafe { addref(p.ctx) };
}
}
/// Release a params handle (prefers the `release` function pointer; the
/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the
/// handle cannot be released twice.
pub(crate) fn params_release(p: &mut OakVideoParams) {
if p.ctx.is_null() {
return;
}
if let Some(release) = p.release {
// SAFETY: `release` is a valid C function pointer targeting `ctx`.
unsafe { release(p.ctx) };
} else {
// SAFETY: `p` points at a live handle; `oakcommon_videoparams_free`
// is a no-op for the null ctx we leave behind.
unsafe { oakcommon_videoparams_free(p) };
}
p.ctx = std::ptr::null_mut();
}
impl Frame {
/// New frame with default (invalid) params; buffer unallocated.
pub fn new() -> Self {
let params = unsafe { oakcommon_videoparams_init() };
Frame {
params: Some(params),
params: Some(VideoParams::new()),
data: Vec::new(),
linesize_bytes: 0,
timestamp: Rational::new(0, 1),
@@ -128,9 +95,8 @@ impl Frame {
}
}
/// New frame with a copy of `params` (handle addref'd internally).
pub fn with_params(params: OakVideoParams) -> Self {
params_addref(&params);
/// New frame with a copy of `params`.
pub fn with_params(params: VideoParams) -> Self {
let mut frame = Frame {
params: Some(params),
data: Vec::new(),
@@ -143,17 +109,13 @@ impl Frame {
}
/// The video parameter set, or `None` when empty.
pub fn params(&self) -> Option<&OakVideoParams> {
pub fn params(&self) -> Option<&VideoParams> {
self.params.as_ref()
}
/// Replace the parameter set (handle addref'd), recompute line sizes,
/// do NOT reallocate the buffer.
pub fn set_params(&mut self, params: OakVideoParams) {
if let Some(mut old) = self.params.take() {
params_release(&mut old);
}
params_addref(&params);
/// Replace the parameter set, recompute line sizes, do NOT reallocate
/// the buffer.
pub fn set_params(&mut self, params: VideoParams) {
self.params = Some(params);
self.recompute_linesize();
// Deliberately do not touch `data`: an existing buffer keeps its
@@ -165,10 +127,8 @@ impl Frame {
fn recompute_linesize(&mut self) {
self.linesize_bytes = match &self.params {
Some(p) => {
let w = unsafe { oakcommon_videoparams_get_width(p.clone()) };
let fmt =
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) });
Self::generate_linesize_bytes(fmt, w)
let fmt = pixel_format_from_i32(p.format().code());
Self::generate_linesize_bytes(fmt, p.width())
}
None => 0,
};
@@ -177,12 +137,11 @@ impl Frame {
/// Allocate the pixel buffer from the current params.
pub fn allocate(&mut self) -> crate::error::Result<()> {
let params = match &self.params {
Some(p) => p.clone(),
Some(p) => p,
None => return Err(crate::error::Error::State),
};
let is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) };
if is_valid == 0 {
if !params.is_valid() {
return Err(crate::error::Error::State);
}
@@ -191,9 +150,9 @@ impl Frame {
return Ok(());
}
let width = unsafe { oakcommon_videoparams_get_width(params.clone()) };
let height = unsafe { oakcommon_videoparams_get_height(params.clone()) };
let format = pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(params) });
let width = params.width();
let height = params.height();
let format = pixel_format_from_i32(params.format().code());
let linesize = Self::generate_linesize_bytes(format, width);
let size = (linesize as usize).wrapping_mul(height as usize);
@@ -256,7 +215,7 @@ impl Frame {
/// Frame width in pixels (0 when params are empty).
pub fn width(&self) -> i32 {
match &self.params {
Some(p) => unsafe { oakcommon_videoparams_get_width(p.clone()) },
Some(p) => p.width(),
None => 0,
}
}
@@ -264,7 +223,7 @@ impl Frame {
/// Frame height in pixels (0 when params are empty).
pub fn height(&self) -> i32 {
match &self.params {
Some(p) => unsafe { oakcommon_videoparams_get_height(p.clone()) },
Some(p) => p.height(),
None => 0,
}
}
@@ -272,9 +231,7 @@ impl Frame {
/// Pixel format (`OakPixelFormat` value).
pub fn format(&self) -> PixelFormat {
match &self.params {
Some(p) => {
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) })
}
Some(p) => pixel_format_from_i32(p.format().code()),
None => PixelFormat::Invalid,
}
}
@@ -282,10 +239,9 @@ impl Frame {
/// Plane channel count of the params format.
///
/// # CPP-PARITY
/// `src/codec/src/frame.h` reads this from the params handle via
/// `oakcommon_videoparams_get_channel_count`, which is not exposed in the
/// Rust bridge. Decoder frames are always produced in the internal RGBA
/// layout, so this returns [`VIDEO_CHANNELS`] (4).
/// `src/codec/src/frame.h` reads this from the params via
/// `VideoParams::channel_count`. Decoder frames are always produced in
/// the internal RGBA layout, so this returns [`VIDEO_CHANNELS`] (4).
pub fn channel_count(&self) -> i32 {
VIDEO_CHANNELS
}
@@ -317,7 +273,7 @@ impl Frame {
///
/// # CPP-PARITY
/// `src/codec/src/frame.cpp` — the destination params are carried by
/// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the
/// the C++ callers via `VideoParams` setters; Rust keeps the
/// equivalent state in `self.params`.
///
/// When the current params format already matches the format the buffer
@@ -372,23 +328,13 @@ impl Frame {
}
}
impl Drop for Frame {
/// Release the owned params handle when the last reference dies,
/// mirroring the C++ `Frame::~Frame`.
fn drop(&mut self) {
if let Some(mut p) = self.params.take() {
params_release(&mut p);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::common::oakcommon_videoparams_init_basic;
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
fn frame(w: i32, h: i32) -> Frame {
let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) };
let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
Frame::with_params(params)
}
@@ -459,12 +405,13 @@ mod tests {
#[test]
fn set_params_recomputes_linesize_without_realloc() {
let params = unsafe { oakcommon_videoparams_init_basic(10, 10, 0, 4, 1, 1, 0, 1) };
let params = VideoParams::new_basic(10, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
let mut f = Frame::with_params(params);
f.allocate().unwrap();
let before = f.allocated_size();
let wider = unsafe { oakcommon_videoparams_init_basic(100, 10, 0, 4, 1, 1, 0, 1) };
let wider =
VideoParams::new_basic(100, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
f.set_params(wider);
// linesize reflects the new width, but the buffer is untouched.
assert_eq!(f.linesize_bytes(), 4 * 128);
@@ -475,10 +422,10 @@ mod tests {
#[cfg(test)]
mod tests_extra {
use super::*;
use crate::bridge::common::oakcommon_videoparams_init_basic;
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
fn frame(w: i32, h: i32) -> Frame {
let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) };
let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
Frame::with_params(params)
}
@@ -513,8 +460,8 @@ mod tests_extra {
#[test]
fn pixel_format_from_unknown_code_is_invalid() {
let p = unsafe { oakcommon_videoparams_init_basic(1, 1, 0, 4, 1, 1, 0, 1) };
unsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) };
let mut p = VideoParams::new_basic(1, 1, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
p.set_format(OakPixelFormat::from_code(99));
let f = Frame::with_params(p);
assert_eq!(f.format(), PixelFormat::Invalid);
}
+15 -12
View File
@@ -27,7 +27,7 @@ use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;
use crate::bridge::common::{oakcommon_videoparams_equals, OakVideoParams};
use oakcommon::videoparams::VideoParams;
use crate::frame::Frame;
/// `olive::FrameManager`: singleton frame pool with background GC.
@@ -71,7 +71,7 @@ impl FrameManager {
/// Create a frame with the given params (borrowed from the pool when a
/// compatible free frame exists, else freshly allocated).
pub fn create_frame(&self, params: OakVideoParams) -> Arc<Frame> {
pub fn create_frame(&self, params: VideoParams) -> Arc<Frame> {
let frame = {
let mut pool = self.pool.lock().unwrap();
match pool.iter().position(|f| frame_matches(f, &params)) {
@@ -138,18 +138,21 @@ fn spawn_gc_thread_once(mgr: &'static FrameManager) {
}
/// True when `frame` carries params equal to `params`.
fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool {
fn frame_matches(frame: &Frame, params: &VideoParams) -> bool {
let Some(frame_params) = frame.params() else {
return false;
};
let eq = unsafe { oakcommon_videoparams_equals(frame_params.clone(), params.clone()) };
eq != 0
frame_params.equals(params)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::common::oakcommon_videoparams_init_basic;
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
fn test_params(w: i32, h: i32) -> VideoParams {
VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1)
}
#[test]
fn create_and_return_tracks_counts() {
@@ -157,7 +160,7 @@ mod tests {
assert_eq!(mgr.live_count(), 0);
assert_eq!(mgr.peak_count(), 0);
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
let params = test_params(64, 64);
let frame = mgr.create_frame(params);
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 1);
@@ -172,14 +175,14 @@ mod tests {
#[test]
fn pool_reuses_compatible_frames() {
let mgr = FrameManager::new();
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
let params = test_params(64, 64);
let f1 = mgr.create_frame(params);
mgr.return_frame(Arc::try_unwrap(f1).unwrap());
assert_eq!(mgr.live_count(), 0);
// A compatible request reuses the pooled buffer rather than
// allocating a new one.
let f2 = mgr.create_frame(unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) });
let f2 = mgr.create_frame(test_params(64, 64));
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 1);
Arc::try_unwrap(f2).unwrap();
@@ -188,8 +191,8 @@ mod tests {
#[test]
fn peak_count_tracks_maximum() {
let mgr = FrameManager::new();
let p1 = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
let p2 = unsafe { oakcommon_videoparams_init_basic(128, 128, 0, 4, 1, 1, 0, 1) };
let p1 = test_params(64, 64);
let p2 = test_params(128, 128);
let a = mgr.create_frame(p1);
let b = mgr.create_frame(p2);
assert_eq!(mgr.live_count(), 2);
@@ -203,7 +206,7 @@ mod tests {
#[test]
fn clear_drops_pooled_frames() {
let mgr = FrameManager::new();
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
let params = test_params(64, 64);
let f = mgr.create_frame(params);
mgr.return_frame(Arc::try_unwrap(f).unwrap());
assert_eq!(mgr.pool.lock().unwrap().len(), 1);
+3 -5
View File
@@ -21,9 +21,7 @@
//! created the object).
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use crate::error::{self, OAKCODEC_E_FAILED};
/// Number of boxed handle objects currently alive (leak/debug checking).
@@ -183,9 +181,9 @@ mod tests {
#[test]
fn make_owned_lifecycle_tracks_alive_count() {
// The shared ffi test lock serializes the crate's `alive_count`
// The shared test lock serializes the crate's `alive_count`
// assertions against every other test that creates handles.
let _g = crate::ffi::lock_tests();
let _g = crate::lock_tests();
let before = alive_count();
let h = make_owned(42u32);
assert!(!h.is_null());
@@ -214,7 +212,7 @@ mod tests {
#[test]
fn make_borrowed_takes_ownership() {
let _g = crate::ffi::lock_tests();
let _g = crate::lock_tests();
let before = alive_count();
let raw = Box::into_raw(Box::new(7u32));
let h = unsafe { make_borrowed(raw) };
+49 -2
View File
@@ -29,7 +29,10 @@
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod bridge;
#[cfg(test)]
use std::sync::{Mutex, MutexGuard};
pub mod audioparams;
pub mod conformmanager;
pub mod decoder;
pub mod encoder;
@@ -37,7 +40,6 @@ pub mod encodingparams;
pub mod error;
pub mod exportcodec;
pub mod exportformat;
pub mod ffi;
pub mod ffmpeg;
pub mod footagedescription;
pub mod frame;
@@ -54,6 +56,51 @@ pub mod timecodemetadata;
#[cfg(test)]
mod realmedia_tests;
/// Process-wide test lock: serializes every test that reads or mutates
/// crate-global state (the injected decoder registry, the handle alive
/// count). One lock for the whole crate — tests race only with each
/// other, never with production code.
#[cfg(test)]
static TEST_LOCK: Mutex<()> = Mutex::new(());
/// RAII guard over [`TEST_LOCK`]: the lock is taken when the guard is
/// created ([`TestLock::acquire`]) and released when it is dropped —
/// including through panics, so a failing test can never deadlock the
/// tests that follow. Poison-tolerant: a panicking holder does not leave
/// the mutex poisoned for the next acquirer.
#[cfg(test)]
pub struct TestLock {
/// The held lock guard; dropping it releases [`TEST_LOCK`] (never read,
/// only dropped — the whole point of the RAII guard).
#[allow(dead_code)]
guard: MutexGuard<'static, ()>,
}
#[cfg(test)]
impl TestLock {
/// Acquire exclusive access to the crate's shared test state, blocking
/// until every earlier holder has released it.
pub fn acquire() -> TestLock {
TestLock {
guard: TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()),
}
}
}
#[cfg(test)]
impl Drop for TestLock {
fn drop(&mut self) {
// Dropping the held guard releases TEST_LOCK; the explicit Drop
// documents the acquire-on-create / release-on-drop contract.
}
}
/// Acquire the process-wide test lock (see [`TestLock::acquire`]).
#[cfg(test)]
pub(crate) fn lock_tests() -> TestLock {
TestLock::acquire()
}
// Keep the oakffmpeg-link rlib referenced so its build script's native
// link flags (the static FFmpeg's transitive dependencies) reach the
// final link — rustc prunes the flags of an unreferenced rlib.
+4 -4
View File
@@ -19,7 +19,7 @@
//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO
//! frame conversion goes through the local
//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping
//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`).
//! functions.
//!
//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every
//! operation that would touch the media engine is a documented stub returning
@@ -60,7 +60,7 @@ impl Decoder for OIIODecoder {
fn probe(
&self,
_filename: &str,
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
) -> Option<crate::footagedescription::FootageDescription> {
// Probing is a dylib operation; without it we cannot report anything.
None
@@ -98,7 +98,7 @@ impl Decoder for OIIODecoder {
fn retrieve_video(
&self,
_p: &RetrieveVideoParams,
) -> crate::error::Result<crate::bridge::render::OakRenderTexture> {
) -> crate::error::Result<crate::decoder::OakRenderTexture> {
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
}
@@ -118,7 +118,7 @@ impl Decoder for OIIODecoder {
_sample_rate: i32,
_channel_layout: u64,
_sample_format: i32,
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
) -> crate::error::Result<()> {
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
}
+31 -36
View File
@@ -30,10 +30,8 @@
//! timestamp and time base alongside the raw pixel rows, so a buffer can be
//! turned back into an equivalent [`Frame`] without any external state.
use crate::bridge::common::{
oakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,
oakcommon_videoparams_set_format,
};
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
use oakcommon::videoparams::VideoParams;
use crate::frame::Frame;
use oakcore_rs::Rational;
use std::ffi::c_int;
@@ -154,14 +152,8 @@ pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result<Vec<u8>> {
let (time_base_num, time_base_den) = match frame.params() {
Some(p) => {
let mut num = 0i64;
let mut den = 0i64;
// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid
// handle; the C function only writes through the two out pointers.
unsafe {
oakcommon_videoparams_get_time_base(p.clone(), &mut num, &mut den);
}
(num, den)
let (num, den) = p.time_base();
(i64::from(num), i64::from(den))
}
None => (0, 0),
};
@@ -215,26 +207,19 @@ pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
}
// Build the params from the header, then hand ownership to the frame.
// SAFETY: the init returns a live handle; the clone for `set_format` is
// only read, and the original is moved into `Frame::with_params` (which
// takes ownership), so there is no double release.
let params = unsafe {
oakcommon_videoparams_init_with_time_base(
header.width,
header.height,
header.time_base_num as c_int,
header.time_base_den as c_int,
0,
4,
1,
1,
0,
1,
)
};
unsafe {
oakcommon_videoparams_set_format(params.clone(), header.format);
}
let mut params = VideoParams::new_with_time_base(
header.width,
header.height,
header.time_base_num as c_int,
header.time_base_den as c_int,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
params.set_format(OakPixelFormat::from_code(header.format));
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));
frame.allocate()?;
@@ -351,11 +336,21 @@ mod tests {
}
/// A small helper to build a fully allocated, filled frame using the same
/// test-stub params pattern as `frame.rs`.
/// params pattern as `frame.rs`.
fn make_frame() -> Frame {
// SAFETY: test-stub videoparams; ownership moves into `with_params`.
let params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30, 0, 4, 1, 1, 0, 1) };
unsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8
let mut params = VideoParams::new_with_time_base(
100,
50,
1,
30,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
params.set_format(OakPixelFormat::from_code(0)); // U8
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(5, 2));
frame.allocate().unwrap();
+42 -95
View File
@@ -19,11 +19,12 @@
//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual
//! transcodes are delegated to the global task submit callback
//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy
//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI
//! as missing. `proxy_params_from_config` reads the oakcommon config store
//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23
//! / "mp4" / "veryfast" / audio included).
use std::ffi::{c_char, CString};
use oakcommon::configstore::ConfigStore;
use oakcommon::filefunctions::FileFunctions;
use std::path::Path;
/// Proxy state of a proxy file on disk.
@@ -336,97 +337,35 @@ fn cstr_slice(a: &[u8; 32]) -> &str {
/// `oakcommon_config_get_int` wrapper (null group).
fn config_get_int(key: &str, default: i32) -> i32 {
let ckey = cstring(key);
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
unsafe {
crate::bridge::common::oakcommon_config_get_int(std::ptr::null(), ckey.as_ptr(), default)
}
ConfigStore::instance().get_int(None, key, default)
}
/// `oakcommon_config_get_bool` wrapper (null group).
fn config_get_bool(key: &str, default: i32) -> i32 {
let ckey = cstring(key);
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
unsafe {
crate::bridge::common::oakcommon_config_get_bool(std::ptr::null(), ckey.as_ptr(), default)
}
ConfigStore::instance().get_bool(None, key, default)
}
/// Two-stage `oakcommon_config_get` string read; `None` when the stored
/// value is empty or absent.
/// `oakcommon_config_get` string read; `None` when the stored value is
/// empty or absent.
fn config_get_str(key: &str) -> Option<String> {
let ckey = cstring(key);
// # Safety: `ckey` is valid; first call asks only for the required size.
let size = unsafe {
crate::bridge::common::oakcommon_config_get(
std::ptr::null(),
ckey.as_ptr(),
std::ptr::null_mut(),
0,
)
};
if size <= 1 {
return None;
match ConfigStore::instance().get(None, key) {
Ok(s) if !s.is_empty() => Some(s),
_ => None,
}
let mut buf = vec![0u8; size as usize];
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
unsafe {
crate::bridge::common::oakcommon_config_get(
std::ptr::null(),
ckey.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
size,
);
}
let mut end = buf.len();
while end > 0 && buf[end - 1] == 0 {
end -= 1;
}
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
}
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
/// returns a 64-bit id directly).
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
fn unique_file_identifier(filename: &str) -> String {
let c = match CString::new(filename) {
Ok(c) => c,
Err(_) => return String::new(),
};
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
let id = unsafe {
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
};
format!("{}", id)
FileFunctions::new()
.get_unique_file_identifier(filename)
.unwrap_or_default()
}
/// Two-stage `oakcommon_filefunctions_get_application_path` read.
/// `oakcommon_filefunctions_get_application_path` read.
fn application_path() -> String {
// # Safety: first call asks only for the required size.
let size = unsafe {
crate::bridge::common::oakcommon_filefunctions_get_application_path(std::ptr::null_mut(), 0)
};
if size <= 1 {
return String::new();
}
let mut buf = vec![0u8; size as usize];
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
unsafe {
crate::bridge::common::oakcommon_filefunctions_get_application_path(
buf.as_mut_ptr() as *mut c_char,
size,
);
}
let mut end = buf.len();
while end > 0 && buf[end - 1] == 0 {
end -= 1;
}
String::from_utf8_lossy(&buf[..end]).into_owned()
}
/// Build a NUL-terminated C string from a Rust string; empty on embedded
/// NUL (defensive only — callers pass sane keys).
fn cstring(s: &str) -> CString {
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
FileFunctions::new()
.get_application_path()
.unwrap_or_default()
}
/// True when `p` is a regular file with at least one execute bit set.
@@ -456,14 +395,6 @@ mod tests {
dir.to_string_lossy().into_owned()
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 14695981039346656037;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(1099511628211);
}
h
}
#[test]
fn proxy_params_default_values() {
@@ -503,24 +434,40 @@ mod tests {
#[test]
fn proxy_filename_derivation() {
let cache = temp_subdir("fn");
let stub_id = fnv1a64(b"media.mp4") as i64;
let id = fnv1a64(stub_id.to_string().as_bytes()) as i64;
let p = ProxyManager::proxy_params_default();
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
assert_eq!(f, format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id));
// A missing source carries no unique-file identifier
// (get_unique_file_identifier is empty for non-existent files —
// C++ parity), so the name is the plain size/version/audio tags.
let missing = std::path::Path::new(&temp_subdir("missing")).join("nope.mp4");
let f = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &p).unwrap();
assert_eq!(f, format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache));
// An existing source embeds a stable per-file identifier.
let existing = std::path::Path::new(&temp_subdir("existing")).join("real.mp4");
std::fs::write(&existing, b"media").unwrap();
let f1 =
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
let f2 =
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
assert!(
f1.contains("-0.1280x720.v1.a1.mp4"),
"size/version/audio tags present: {f1}"
);
assert!(f1 != format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache), "id embedded: {f1}");
assert_eq!(f1, f2, "the identifier is stable for the same file");
// Divider mode tags the divider instead of an absolute size.
let mut d = p.clone();
d.divider = 2;
let f2 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &d).unwrap();
assert!(f2.contains(".div2."));
let f3 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &d).unwrap();
assert!(f3.contains(".div2."));
// No audio.
let mut na = p.clone();
na.include_audio = 0;
let f3 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &na).unwrap();
assert!(f3.contains(".a0."));
let f4 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &na).unwrap();
assert!(f4.contains(".a0."));
}
#[test]
+14 -27
View File
@@ -21,16 +21,13 @@
//! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`.
//!
//! They live inside the crate (not `tests/`) because the crate's
//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the
//! `Frame`/`FootageDescription` paths need — are only linked for the lib
//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot
//! resolve those symbols; see `tests/ffi_contract_test.rs`).
//! `#[cfg(test)]` in-memory stubs — which the `Frame`/`FootageDescription`
//! paths need — are only linked for the lib test binary (`tests/` is
//! compiled without `#[cfg(test)]` and cannot resolve those symbols; see
//! `tests/ffi_contract_test.rs`).
use crate::bridge::common::{
oakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate,
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
};
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
use oakcommon::videoparams::VideoParams;
use crate::decoder::{
CodecStream, Decoder, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
K_COLOR_RANGE_DEFAULT,
@@ -80,8 +77,8 @@ fn h264_params(out: &std::path::Path) -> crate::encodingparams::EncodingParams {
/// Build an allocated F32-RGBA frame with a moving color pattern.
fn pattern_frame(i: i32) -> Frame {
let vp = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };
let mut vp = VideoParams::new_basic(64, 64, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
let mut f = Frame::with_params(vp);
f.set_timestamp(Rational::new(i as i64, 10));
f.allocate().unwrap();
@@ -117,20 +114,10 @@ fn probe_reports_streams_and_duration() {
// Video stream: 1920x1080, 25fps, 17s at 1/12800 time base.
let vp = desc.get_video_stream(0).expect("video stream");
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920);
assert_eq!(
unsafe { oakcommon_videoparams_get_height(vp.clone()) },
1080
);
assert_eq!(
unsafe { oakcommon_videoparams_get_duration(vp.clone()) },
17 * 12800
);
let mut num: i32 = 0;
let mut den: i32 = 0;
unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut num, &mut den) };
assert_eq!((num, den), (25, 1));
assert_eq!(vp.width(), 1920);
assert_eq!(vp.height(), 1080);
assert_eq!(vp.duration(), 17 * 12800);
assert_eq!(vp.frame_rate(), (25, 1));
}
#[test]
@@ -220,8 +207,8 @@ fn encode_h264_roundtrip_to_tmp() {
let desc = d.probe(&out_str, None).expect("probe round-trip output");
assert_eq!(desc.video_stream_count(), 1);
let vp = desc.get_video_stream(0).expect("video stream");
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64);
assert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64);
assert_eq!(vp.width(), 64);
assert_eq!(vp.height(), 64);
// Decode the first frame of the result.
let s = CodecStream::with_block(out_str.clone(), 0, None);
+13 -5
View File
@@ -37,11 +37,10 @@
use std::path::Path;
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
use oakcommon::videoparams::VideoParams;
use oakcore_rs::{PixelFormat, Rational, SampleFormat};
use crate::bridge::common::{
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
};
use crate::encodingparams::EncodingParams;
use crate::encoder::create_from_params;
use crate::frame::Frame;
@@ -106,8 +105,17 @@ pub fn write_test_clip(
/// One frame of the known pattern (see module doc).
fn pattern_frame(i: i32, width: i32, height: i32, fps: i32) -> Frame {
let vp = unsafe { oakcommon_videoparams_init_basic(width, height, 0, 4, 1, 1, 0, 1) };
unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };
let mut vp = VideoParams::new_basic(
width,
height,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
let mut f = Frame::with_params(vp);
f.set_timestamp(Rational::new(i as i64, fps as i64));
f.allocate().expect("test frame allocation");