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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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('/'));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(¶ms);
|
||||
/// 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(¶ms);
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@@ -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, ¶ms)) {
|
||||
@@ -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);
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user