feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple trim bugs the stub was hiding); same-track via the frozen C ABI, cross-track supported by the module command - oaknode clip blocks now declare a tex_in texture input and set effect_input to it, so timeline clips can host effect chains; facade test covers effect insert/remove on a real clip - oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a Homebrew upgrade left the system ffmpeg .pc pointing at a deleted dav1d Cellar path, breaking links); reads a git-ignored workspace .env for IDEs that cannot inject env vars (RustRover); links the C++ stdlib for C++ codec libs (svt-av1) - oakengine re-exports oaknode so tests share one crate instance; it_node uses the direct instance's value type where it calls the module FFI (the --workspace dev-dependency feature split builds oaknode twice)
This commit is contained in:
@@ -40,20 +40,16 @@ 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;
|
||||
|
||||
|
||||
// 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.
|
||||
@@ -95,23 +91,11 @@ extern "C" {
|
||||
/// `oakcommon_videoparams_equals`.
|
||||
pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_set_time_base`.
|
||||
pub fn oakcommon_videoparams_set_time_base(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
);
|
||||
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,
|
||||
);
|
||||
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,
|
||||
);
|
||||
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`.
|
||||
@@ -129,7 +113,10 @@ extern "C" {
|
||||
/// `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);
|
||||
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`.
|
||||
@@ -185,11 +172,7 @@ extern "C" {
|
||||
/// `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,
|
||||
);
|
||||
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`.
|
||||
@@ -226,9 +209,17 @@ extern "C" {
|
||||
/// `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, default: c_int) -> c_int;
|
||||
pub fn oakcommon_config_get_int(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_config_get_bool`.
|
||||
pub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -> c_int;
|
||||
pub fn oakcommon_config_get_bool(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_config_get` (two-stage string access).
|
||||
pub fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
@@ -246,10 +237,8 @@ extern "C" {
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier`.
|
||||
pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64;
|
||||
/// `oakcommon_filefunctions_get_application_path` (two-stage).
|
||||
pub fn oakcommon_filefunctions_get_application_path(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int)
|
||||
-> c_int;
|
||||
/// `oakcommon_filefunctions_free` (frees an internally cached string).
|
||||
pub fn oakcommon_filefunctions_free(ptr: *mut c_void);
|
||||
/// `oakcommon_colortransform_init_output`.
|
||||
|
||||
@@ -28,19 +28,15 @@ 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`
|
||||
|
||||
@@ -35,9 +35,7 @@ use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::common::{
|
||||
OakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams,
|
||||
};
|
||||
use crate::bridge::common::{OakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams};
|
||||
use crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};
|
||||
use crate::handle::OAKCODEC_ABI_VERSION;
|
||||
|
||||
@@ -96,10 +94,7 @@ fn params_ref(ctx: *mut c_void) -> Option<&'static mut MockParams> {
|
||||
|
||||
fn params_get(ctx: *mut c_void) -> MockParams {
|
||||
let store = params_store().lock().unwrap();
|
||||
store
|
||||
.get(&(ctx as usize))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
store.get(&(ctx as usize)).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockParams)) {
|
||||
@@ -239,11 +234,11 @@ pub extern "C" fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoPar
|
||||
pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
|
||||
// U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4.
|
||||
match format {
|
||||
0 => 1, // U8
|
||||
1 => 4, // U10
|
||||
2 => 2, // U16
|
||||
3 => 2, // F16
|
||||
4 => 4, // F32
|
||||
0 => 1, // U8
|
||||
1 => 4, // U10
|
||||
2 => 2, // U16
|
||||
3 => 2, // F16
|
||||
4 => 4, // F32
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -337,11 +332,7 @@ pub extern "C" fn oakcommon_videoparams_get_interlacing(params: OakVideoParams)
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_time_base(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64) {
|
||||
params_set(params.ctx, |p| {
|
||||
p.time_base_num = num;
|
||||
p.time_base_den = den;
|
||||
@@ -350,11 +341,7 @@ pub extern "C" fn oakcommon_videoparams_set_time_base(
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_frame_rate(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64) {
|
||||
params_set(params.ctx, |p| {
|
||||
p.frame_rate_num = num as i32;
|
||||
p.frame_rate_den = den as i32;
|
||||
@@ -376,7 +363,10 @@ pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_interlacing(
|
||||
params: OakVideoParams,
|
||||
interlacing: c_int,
|
||||
) {
|
||||
params_set(params.ctx, |p| p.interlacing = interlacing);
|
||||
}
|
||||
|
||||
@@ -394,7 +384,10 @@ pub extern "C" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, s
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_range(
|
||||
params: OakVideoParams,
|
||||
color_range: c_int,
|
||||
) {
|
||||
params_set(params.ctx, |p| p.color_range = color_range);
|
||||
}
|
||||
|
||||
@@ -412,13 +405,19 @@ pub extern "C" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_primaries(
|
||||
params: OakVideoParams,
|
||||
primaries: c_int,
|
||||
) {
|
||||
params_set(params.ctx, |p| p.color_primaries = primaries);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int) {
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_transfer(
|
||||
params: OakVideoParams,
|
||||
transfer: c_int,
|
||||
) {
|
||||
params_set(params.ctx, |p| p.color_trc = transfer);
|
||||
}
|
||||
|
||||
@@ -462,10 +461,7 @@ fn audio_params_store() -> &'static Mutex<HashMap<usize, MockAudioParams>> {
|
||||
|
||||
fn audio_params_get(ctx: *const c_void) -> MockAudioParams {
|
||||
let store = audio_params_store().lock().unwrap();
|
||||
store
|
||||
.get(&(ctx as usize))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
store.get(&(ctx as usize)).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Per-`OakRational` backing state (an owned `(num, den)` pair).
|
||||
@@ -535,10 +531,7 @@ pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioPara
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_channel_layout(
|
||||
params: *mut OakAudioParams,
|
||||
layout: u64,
|
||||
) {
|
||||
pub extern "C" fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64) {
|
||||
audio_params_set(params as *mut c_void, |p| p.channel_layout = layout);
|
||||
}
|
||||
|
||||
@@ -560,10 +553,7 @@ pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, fo
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_stream_index(
|
||||
params: *mut OakAudioParams,
|
||||
index: c_int,
|
||||
) {
|
||||
pub extern "C" fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int) {
|
||||
audio_params_set(params as *mut c_void, |p| p.stream_index = index);
|
||||
}
|
||||
|
||||
@@ -634,7 +624,10 @@ pub extern "C" fn oakcore_rational_free(rational: *mut c_void) {
|
||||
if rational.is_null() {
|
||||
return;
|
||||
}
|
||||
rational_store().lock().unwrap().remove(&(rational as usize));
|
||||
rational_store()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(rational as usize));
|
||||
// SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a
|
||||
// boxed `(i32, i32)` pair; we hold the only reference after removal.
|
||||
unsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };
|
||||
@@ -737,8 +730,9 @@ pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier(path: *cons
|
||||
unsafe { CStr::from_ptr(path) }
|
||||
.to_bytes()
|
||||
.iter()
|
||||
.fold(14695981039346656037u64, |acc, &b| (acc ^ b as u64).wrapping_mul(1099511628211))
|
||||
as i64
|
||||
.fold(14695981039346656037u64, |acc, &b| {
|
||||
(acc ^ b as u64).wrapping_mul(1099511628211)
|
||||
}) as i64
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -773,7 +767,10 @@ pub extern "C" fn oakcommon_colortransform_init_output(
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams) {
|
||||
pub extern "C" fn oakcommon_colortransform_get_output(
|
||||
params: OakVideoParams,
|
||||
out: *mut OakVideoParams,
|
||||
) {
|
||||
if !out.is_null() {
|
||||
unsafe { *out = params.clone() };
|
||||
}
|
||||
@@ -821,7 +818,9 @@ pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(forma
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(test)]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
|
||||
pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
@@ -894,12 +893,21 @@ pub extern "C" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {
|
||||
if atom.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe { cancel_flags().lock().unwrap().remove(&((*atom).ctx as usize)) };
|
||||
unsafe {
|
||||
cancel_flags()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&((*atom).ctx as usize))
|
||||
};
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int {
|
||||
(*cancel_flags().lock().unwrap().get(&(atom.ctx as usize)).unwrap_or(&false)) as c_int
|
||||
(*cancel_flags()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(atom.ctx as usize))
|
||||
.unwrap_or(&false)) as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -228,11 +228,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_conform_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakcodec_conform_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
@@ -254,10 +254,7 @@ pub trait Decoder: Send + Sync {
|
||||
fn stream(&self) -> CodecStream;
|
||||
|
||||
/// Retrieve a video frame into CPU memory.
|
||||
fn retrieve_video_frame(
|
||||
&self,
|
||||
p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<Arc<Frame>>;
|
||||
fn retrieve_video_frame(&self, p: &RetrieveVideoParams) -> crate::error::Result<Arc<Frame>>;
|
||||
|
||||
/// Retrieve a video frame as a render texture (owned by caller).
|
||||
fn retrieve_video(&self, p: &RetrieveVideoParams) -> crate::error::Result<OakRenderTexture>;
|
||||
@@ -335,29 +332,31 @@ impl Decoder for UnimplementedDecoder {
|
||||
}
|
||||
|
||||
fn open(&self, _stream: &CodecStream) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn stream(&self) -> CodecStream {
|
||||
CodecStream::new()
|
||||
}
|
||||
|
||||
fn retrieve_video_frame(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<Arc<Frame>> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
fn retrieve_video_frame(&self, _p: &RetrieveVideoParams) -> crate::error::Result<Arc<Frame>> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn retrieve_video(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<OakRenderTexture> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
fn retrieve_video(&self, _p: &RetrieveVideoParams) -> crate::error::Result<OakRenderTexture> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn retrieve_audio(
|
||||
@@ -367,7 +366,9 @@ impl Decoder for UnimplementedDecoder {
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
) -> crate::error::Result<RetrieveAudioStatus> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn conform_audio(
|
||||
@@ -378,7 +379,9 @@ impl Decoder for UnimplementedDecoder {
|
||||
_sample_format: i32,
|
||||
_cancelled: Option<&OakCancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,9 +483,10 @@ pub fn transform_image_sequence_file_name(filename: &str, number: i64) -> String
|
||||
}
|
||||
|
||||
match path.parent() {
|
||||
Some(parent) if !parent.as_os_str().is_empty() => {
|
||||
Path::new(parent).join(&new_filename).to_string_lossy().into_owned()
|
||||
}
|
||||
Some(parent) if !parent.as_os_str().is_empty() => Path::new(parent)
|
||||
.join(&new_filename)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
_ => new_filename,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +139,12 @@ pub fn create_from_params(params: &EncodingParams) -> Option<Arc<dyn Encoder>> {
|
||||
}
|
||||
}
|
||||
match encoder_type_from_format(params.format) {
|
||||
Some(EncoderType::FFmpeg) => {
|
||||
Some(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(params.clone())))
|
||||
}
|
||||
Some(EncoderType::OIIO) => {
|
||||
Some(Arc::new(crate::oiio::OIIOEncoder { params: params.clone() }))
|
||||
}
|
||||
Some(EncoderType::FFmpeg) => Some(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(
|
||||
params.clone(),
|
||||
))),
|
||||
Some(EncoderType::OIIO) => Some(Arc::new(crate::oiio::OIIOEncoder {
|
||||
params: params.clone(),
|
||||
})),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
@@ -230,9 +230,7 @@ pub fn filename_remove_digit_placeholder(filename: &str) -> String {
|
||||
while i < bytes.len() {
|
||||
// A separator is consumed only when a placeholder follows it.
|
||||
let ph_start = match bytes[i] {
|
||||
b'-' | b'.' | b' ' | b'_' if placeholder_range(bytes, i + 1).is_some() => {
|
||||
i + 1
|
||||
}
|
||||
b'-' | b'.' | b' ' | b'_' if placeholder_range(bytes, i + 1).is_some() => i + 1,
|
||||
_ => i,
|
||||
};
|
||||
match placeholder_range(bytes, ph_start) {
|
||||
@@ -289,16 +287,25 @@ mod tests {
|
||||
assert!(!filename_contains_digit_placeholder("out[].png"));
|
||||
|
||||
// digit count: number of '#' in the first placeholder.
|
||||
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out_[#####].png"), 5);
|
||||
assert_eq!(
|
||||
image_sequence_placeholder_digit_count("/tmp/out_[#####].png"),
|
||||
5
|
||||
);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("out[#].png"), 1);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("a[##]b[####]c"), 2);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out.png"), 0);
|
||||
|
||||
// remove: separator char before the placeholder goes with it.
|
||||
assert_eq!(filename_remove_digit_placeholder("/tmp/out_[#####].png"), "/tmp/out.png");
|
||||
assert_eq!(
|
||||
filename_remove_digit_placeholder("/tmp/out_[#####].png"),
|
||||
"/tmp/out.png"
|
||||
);
|
||||
assert_eq!(filename_remove_digit_placeholder("out[###].png"), "out.png");
|
||||
assert_eq!(filename_remove_digit_placeholder("a_[#]b_[###]c"), "abc");
|
||||
assert_eq!(filename_remove_digit_placeholder("/tmp/out.png"), "/tmp/out.png");
|
||||
assert_eq!(
|
||||
filename_remove_digit_placeholder("/tmp/out.png"),
|
||||
"/tmp/out.png"
|
||||
);
|
||||
}
|
||||
|
||||
struct UnimplementedDummy;
|
||||
|
||||
@@ -210,9 +210,7 @@ impl EncodingParams {
|
||||
let source_ar = src_width as f64 / src_height as f64;
|
||||
|
||||
// qFuzzyCompare(export_ar, source_ar): within one part in 100000.
|
||||
if (export_ar - source_ar).abs() * 100000.0
|
||||
<= export_ar.abs().min(source_ar.abs())
|
||||
{
|
||||
if (export_ar - source_ar).abs() * 100000.0 <= export_ar.abs().min(source_ar.abs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,16 +231,16 @@ impl EncodingParams {
|
||||
/// return the empty string, matching the C++ default case.
|
||||
pub fn extension(&self) -> &str {
|
||||
match self.format {
|
||||
0 => "mxf", // DNxHD
|
||||
1 => "mkv", // Matroska
|
||||
2 => "mp4", // MPEG-4 video
|
||||
3 => "exr", // OpenEXR
|
||||
4 => "mov", // QuickTime
|
||||
5 => "png", // PNG
|
||||
6 => "tiff", // TIFF
|
||||
7 => "wav", // WAV
|
||||
8 => "aiff", // AIFF
|
||||
9 => "mp3", // MP3
|
||||
0 => "mxf", // DNxHD
|
||||
1 => "mkv", // Matroska
|
||||
2 => "mp4", // MPEG-4 video
|
||||
3 => "exr", // OpenEXR
|
||||
4 => "mov", // QuickTime
|
||||
5 => "png", // PNG
|
||||
6 => "tiff", // TIFF
|
||||
7 => "wav", // WAV
|
||||
8 => "aiff", // AIFF
|
||||
9 => "mp3", // MP3
|
||||
10 => "flac", // FLAC
|
||||
11 => "ogg", // Ogg
|
||||
12 => "webm", // WebM
|
||||
@@ -404,7 +402,10 @@ impl EncodingParams {
|
||||
pub fn save_to_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
s.push_str("<export version=\"1\">");
|
||||
s.push_str(&format!("<filename>{}</filename>", escape_xml(cstr(&self.filename))));
|
||||
s.push_str(&format!(
|
||||
"<filename>{}</filename>",
|
||||
escape_xml(cstr(&self.filename))
|
||||
));
|
||||
s.push_str(&format!("<format>{}</format>", self.format));
|
||||
s.push_str(&format!("<range>{}</range>", self.has_custom_range));
|
||||
s.push_str(&format!(
|
||||
@@ -416,10 +417,7 @@ impl EncodingParams {
|
||||
self.custom_range_out_num, self.custom_range_out_den
|
||||
));
|
||||
|
||||
s.push_str(&format!(
|
||||
"<video enabled=\"{}\">",
|
||||
self.video_enabled
|
||||
));
|
||||
s.push_str(&format!("<video enabled=\"{}\">", self.video_enabled));
|
||||
if self.video_enabled != 0 {
|
||||
s.push_str(&format!("<codec>{}</codec>", self.video_codec));
|
||||
s.push_str(&format!("<width>{}</width>", self.video_width));
|
||||
@@ -432,10 +430,7 @@ impl EncodingParams {
|
||||
"<timebase>{}/{}</timebase>",
|
||||
self.video_time_base_num, self.video_time_base_den
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"<divider>{}</divider>",
|
||||
self.video_interlacing
|
||||
));
|
||||
s.push_str(&format!("<divider>{}</divider>", self.video_interlacing));
|
||||
s.push_str(&format!(
|
||||
"<pixelaspect>{}/{}</pixelaspect>",
|
||||
self.video_pixel_aspect_num, self.video_pixel_aspect_den
|
||||
@@ -449,12 +444,12 @@ impl EncodingParams {
|
||||
"<maxbitrate>{}</maxbitrate>",
|
||||
self.video_max_bit_rate
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"<bufsize>{}</bufsize>",
|
||||
self.video_buffer_size
|
||||
));
|
||||
s.push_str(&format!("<bufsize>{}</bufsize>", self.video_buffer_size));
|
||||
s.push_str(&format!("<threads>{}</threads>", self.video_threads));
|
||||
s.push_str(&format!("<pixfmt>{}</pixfmt>", escape_xml(cstr(&self.video_pix_fmt))));
|
||||
s.push_str(&format!(
|
||||
"<pixfmt>{}</pixfmt>",
|
||||
escape_xml(cstr(&self.video_pix_fmt))
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"<imgseq>{}</imgseq>",
|
||||
self.video_is_image_sequence
|
||||
@@ -466,10 +461,7 @@ impl EncodingParams {
|
||||
}
|
||||
s.push_str("</video>");
|
||||
|
||||
s.push_str(&format!(
|
||||
"<audio enabled=\"{}\">",
|
||||
self.audio_enabled
|
||||
));
|
||||
s.push_str(&format!("<audio enabled=\"{}\">", self.audio_enabled));
|
||||
if self.audio_enabled != 0 {
|
||||
s.push_str(&format!("<codec>{}</codec>", self.audio_codec));
|
||||
s.push_str(&format!(
|
||||
@@ -501,10 +493,7 @@ impl EncodingParams {
|
||||
"<sidecarformat>{}</sidecarformat>",
|
||||
self.subtitles_sidecar_format
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"<codec>{}</codec>",
|
||||
self.subtitles_codec
|
||||
));
|
||||
s.push_str(&format!("<codec>{}</codec>", self.subtitles_codec));
|
||||
}
|
||||
s.push_str("</subtitles>");
|
||||
|
||||
@@ -623,7 +612,12 @@ impl<'a> Cursor<'a> {
|
||||
self.skip_ws();
|
||||
if self.starts_with("/>") {
|
||||
self.i += 2;
|
||||
return Ok(El { name, attrs, text: None, children: Vec::new() });
|
||||
return Ok(El {
|
||||
name,
|
||||
attrs,
|
||||
text: None,
|
||||
children: Vec::new(),
|
||||
});
|
||||
}
|
||||
if self.starts_with(">") {
|
||||
self.i += 1;
|
||||
@@ -656,7 +650,12 @@ impl<'a> Cursor<'a> {
|
||||
}
|
||||
}
|
||||
self.expect_end(&name)?;
|
||||
Ok(El { name, attrs, text: None, children })
|
||||
Ok(El {
|
||||
name,
|
||||
attrs,
|
||||
text: None,
|
||||
children,
|
||||
})
|
||||
} else {
|
||||
let start = self.i;
|
||||
while self.i < self.s.len() && !self.starts_with("<") {
|
||||
@@ -664,7 +663,12 @@ impl<'a> Cursor<'a> {
|
||||
}
|
||||
let text = self.s[start..self.i].to_string();
|
||||
self.expect_end(&name)?;
|
||||
Ok(El { name, attrs, text: Some(text), children: Vec::new() })
|
||||
Ok(El {
|
||||
name,
|
||||
attrs,
|
||||
text: Some(text),
|
||||
children: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,7 +688,10 @@ fn text(el: &El) -> String {
|
||||
}
|
||||
|
||||
fn attr<'a>(el: &'a El, name: &str) -> Option<&'a str> {
|
||||
el.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
|
||||
el.attrs
|
||||
.iter()
|
||||
.find(|(k, _)| k == name)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
fn parse_i32(s: &str) -> i32 {
|
||||
@@ -767,10 +774,7 @@ mod tests {
|
||||
const EPS: f64 = 1e-9;
|
||||
|
||||
fn assert_close(a: f64, b: f64) {
|
||||
assert!(
|
||||
(a - b).abs() < EPS,
|
||||
"expected {a} close to {b} (eps {EPS})"
|
||||
);
|
||||
assert!((a - b).abs() < EPS, "expected {a} close to {b} (eps {EPS})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -865,14 +869,7 @@ mod tests {
|
||||
fn generate_matrix_equal_aspect_is_identity() {
|
||||
// Same 16:9 aspect: export_ar == source_ar -> fuzzy-equal -> identity.
|
||||
let mut out = [0.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Fit,
|
||||
1920,
|
||||
1080,
|
||||
1280,
|
||||
720,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 1920, 1080, 1280, 720, &mut out);
|
||||
assert_eq!(out, identity());
|
||||
}
|
||||
|
||||
@@ -881,14 +878,7 @@ mod tests {
|
||||
// src square (ar 1.0) -> dst 2:1 (ar 2.0). Fit: source wider/narrower
|
||||
// relative to export -> the x axis is squeezed to source_ar/export_ar.
|
||||
let mut out = [0.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Fit,
|
||||
1000,
|
||||
1000,
|
||||
2000,
|
||||
1000,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 1000, 1000, 2000, 1000, &mut out);
|
||||
// export_ar(2.0) > source_ar(1.0); Fit => scale(source_ar/export_ar, 1).
|
||||
assert_close(out[0], 0.5);
|
||||
assert_close(out[5], 1.0);
|
||||
@@ -899,14 +889,7 @@ mod tests {
|
||||
// src square -> dst 2:1. Crop: fit inside, so we zoom the y axis by
|
||||
// export_ar/source_ar and keep x unscaled.
|
||||
let mut out = [0.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Crop,
|
||||
1000,
|
||||
1000,
|
||||
2000,
|
||||
1000,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Crop, 1000, 1000, 2000, 1000, &mut out);
|
||||
// export_ar(2.0) > source_ar(1.0); not Fit => scale(1, export_ar/source_ar).
|
||||
assert_close(out[0], 1.0);
|
||||
assert_close(out[5], 2.0);
|
||||
@@ -917,14 +900,7 @@ mod tests {
|
||||
// src square -> dst 0.5:1 (ar 0.5). Crop: export narrower than source,
|
||||
// zoom the x axis by source_ar/export_ar.
|
||||
let mut out = [0.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Crop,
|
||||
1000,
|
||||
1000,
|
||||
500,
|
||||
1000,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Crop, 1000, 1000, 500, 1000, &mut out);
|
||||
// export_ar(0.5) < source_ar(1.0); (ar>source) == false, (method==Fit)
|
||||
// false => false == false true => scale(source_ar/export_ar, 1) = 2.0.
|
||||
assert_close(out[0], 2.0);
|
||||
@@ -935,25 +911,11 @@ mod tests {
|
||||
fn generate_matrix_degenerate_is_identity() {
|
||||
// Zero / negative source sizes must not produce inf/NaN.
|
||||
let mut out = [9.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Fit,
|
||||
0,
|
||||
1080,
|
||||
1280,
|
||||
720,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 0, 1080, 1280, 720, &mut out);
|
||||
assert_eq!(out, identity());
|
||||
|
||||
let mut out = [9.0; 16];
|
||||
EncodingParams::generate_matrix(
|
||||
VideoScalingMethod::Crop,
|
||||
-1,
|
||||
1080,
|
||||
1280,
|
||||
720,
|
||||
&mut out,
|
||||
);
|
||||
EncodingParams::generate_matrix(VideoScalingMethod::Crop, -1, 1080, 1280, 720, &mut out);
|
||||
assert_eq!(out, identity());
|
||||
}
|
||||
|
||||
|
||||
@@ -310,14 +310,26 @@ mod tests {
|
||||
Format::get_video_codecs(Format::MPEG4Video),
|
||||
vec![Codec::H264, Codec::H264RGB, Codec::H265]
|
||||
);
|
||||
assert_eq!(Format::get_video_codecs(Format::OpenEXR), vec![Codec::OpenEXR]);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::OpenEXR),
|
||||
vec![Codec::OpenEXR]
|
||||
);
|
||||
assert_eq!(Format::get_video_codecs(Format::PNG), vec![Codec::PNG]);
|
||||
assert_eq!(Format::get_video_codecs(Format::TIFF), vec![Codec::TIFF]);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::QuickTime),
|
||||
vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::ProRes, Codec::CineForm]
|
||||
vec![
|
||||
Codec::H264,
|
||||
Codec::H264RGB,
|
||||
Codec::H265,
|
||||
Codec::ProRes,
|
||||
Codec::CineForm
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::WebM),
|
||||
vec![Codec::AV1, Codec::VP9]
|
||||
);
|
||||
assert_eq!(Format::get_video_codecs(Format::WebM), vec![Codec::AV1, Codec::VP9]);
|
||||
// Formats without video codecs.
|
||||
assert!(Format::get_video_codecs(Format::WAV).is_empty());
|
||||
assert!(Format::get_video_codecs(Format::MP3).is_empty());
|
||||
@@ -341,7 +353,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn subtitle_and_codec_capability_tables() {
|
||||
assert_eq!(Format::get_subtitle_codecs(Format::Matroska), vec![Codec::SRT]);
|
||||
assert_eq!(
|
||||
Format::get_subtitle_codecs(Format::Matroska),
|
||||
vec![Codec::SRT]
|
||||
);
|
||||
assert_eq!(Format::get_subtitle_codecs(Format::SRT), vec![Codec::SRT]);
|
||||
assert!(Format::get_subtitle_codecs(Format::MPEG4Video).is_empty());
|
||||
|
||||
|
||||
@@ -177,7 +177,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_conform_{}_{}", name, std::process::id()));
|
||||
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()
|
||||
}
|
||||
@@ -185,8 +189,14 @@ mod tests {
|
||||
#[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);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_conform_create_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_conform_destroy_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -196,14 +206,20 @@ mod tests {
|
||||
// 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) };
|
||||
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) };
|
||||
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) };
|
||||
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.
|
||||
@@ -214,7 +230,9 @@ mod tests {
|
||||
.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) };
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
|
||||
}
|
||||
|
||||
@@ -225,26 +243,66 @@ mod tests {
|
||||
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) };
|
||||
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) };
|
||||
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) };
|
||||
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) };
|
||||
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) };
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports
|
||||
//! read a plain [`ProbeBox`], session exports read a `Mutex<DecoderBox>`.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
#[cfg(test)]
|
||||
use std::ffi::c_void;
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -45,13 +45,13 @@ use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_width, oakcore_audioparams_channel_count,
|
||||
oakcore_audioparams_channel_layout, oakcore_audioparams_duration,
|
||||
oakcore_audioparams_sample_rate, oakcore_audioparams_stream_index,
|
||||
oakcore_audioparams_time_base, oakcore_rational_denominator,
|
||||
oakcore_rational_free, oakcore_rational_numerator, OakAudioParams, OakVideoParams,
|
||||
oakcore_audioparams_time_base, oakcore_rational_denominator, oakcore_rational_free,
|
||||
oakcore_rational_numerator, OakAudioParams, OakVideoParams,
|
||||
};
|
||||
use crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom};
|
||||
use crate::decoder::{
|
||||
CodecStream, Decoder, K_COLOR_RANGE_DEFAULT, OakCodecAudioStreamInfo,
|
||||
OakCodecVideoStreamInfo, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
|
||||
CodecStream, Decoder, OakCodecAudioStreamInfo, OakCodecVideoStreamInfo, RenderMode,
|
||||
RetrieveAudioStatus, RetrieveVideoParams, K_COLOR_RANGE_DEFAULT,
|
||||
};
|
||||
use crate::footagedescription::FootageDescription;
|
||||
#[cfg(test)]
|
||||
@@ -230,11 +230,9 @@ pub unsafe extern "C" fn oakcodec_decoder_probe_decoder_name(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
match super::get_box::<ProbeBox>(&probe) {
|
||||
Some(b) => super::string_out(&b.decoder_name, buf, buf_size),
|
||||
None => crate::error::OAKCODEC_E_INVALID,
|
||||
}
|
||||
handle::guard_raw(|| match super::get_box::<ProbeBox>(&probe) {
|
||||
Some(b) => super::string_out(&b.decoder_name, buf, buf_size),
|
||||
None => crate::error::OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -340,7 +338,8 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
|
||||
stream_index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let b =
|
||||
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let filename = match crate::ffi::c_str(filename) {
|
||||
Some(f) => f,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
@@ -367,7 +366,9 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
|
||||
Some(x) => x,
|
||||
None => {
|
||||
b.last_error = format!("no decoder recognizes this file: {}", filename);
|
||||
return Err(crate::error::Error::Failed("no decoder recognizes this file".to_string()));
|
||||
return Err(crate::error::Error::Failed(
|
||||
"no decoder recognizes this file".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,14 +376,18 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
|
||||
Some(d) => d,
|
||||
None => {
|
||||
b.last_error = format!("failed to create decoder: {}", decoder_name);
|
||||
return Err(crate::error::Error::Failed("failed to create decoder".to_string()));
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to create decoder".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let stream = CodecStream::with_block(filename.clone(), stream_index, None);
|
||||
if decoder.open(&stream).is_err() {
|
||||
b.last_error = "failed to open stream".to_string();
|
||||
return Err(crate::error::Error::Failed("failed to open stream".to_string()));
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to open stream".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
b.last_error.clear();
|
||||
@@ -398,7 +403,8 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_decoder_close(decoder: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let b =
|
||||
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let mut b = b.lock().unwrap();
|
||||
if b.open {
|
||||
if let Some(d) = &b.decoder {
|
||||
@@ -441,7 +447,8 @@ pub unsafe extern "C" fn oakcodec_decoder_decode_video(
|
||||
denominator: c_int,
|
||||
) -> CHandle {
|
||||
handle::guard_handle(|| {
|
||||
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let b =
|
||||
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let d = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.decoder.is_none() {
|
||||
@@ -498,20 +505,18 @@ pub unsafe extern "C" fn oakcodec_decoder_decode_audio(
|
||||
buf: *mut f32,
|
||||
buf_frames: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
unsafe {
|
||||
decode_audio_inner(
|
||||
decoder,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
buf,
|
||||
buf_frames,
|
||||
)
|
||||
}
|
||||
handle::guard_raw(|| unsafe {
|
||||
decode_audio_inner(
|
||||
decoder,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
buf,
|
||||
buf_frames,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -589,18 +594,16 @@ pub unsafe extern "C" fn oakcodec_decoder_conform_audio(
|
||||
sample_format: c_int,
|
||||
cancelled: OakCancelAtom,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
unsafe {
|
||||
conform_audio_inner(
|
||||
decoder,
|
||||
output_filenames,
|
||||
filename_count,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
cancelled,
|
||||
)
|
||||
}
|
||||
handle::guard(|| unsafe {
|
||||
conform_audio_inner(
|
||||
decoder,
|
||||
output_filenames,
|
||||
filename_count,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
cancelled,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -678,9 +681,7 @@ pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_digit_count(
|
||||
|
||||
/// `oakcodec_decoder_get_image_sequence_index`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(
|
||||
filename: *const c_char,
|
||||
) -> i64 {
|
||||
pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 {
|
||||
handle::guard_i64(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => crate::decoder::get_image_sequence_index(&f),
|
||||
None => crate::error::OAKCODEC_E_INVALID as i64,
|
||||
@@ -729,8 +730,8 @@ mod tests {
|
||||
};
|
||||
use crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init};
|
||||
use crate::decoder::set_test_decoders;
|
||||
use crate::footagedescription::StreamEntry;
|
||||
use crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
|
||||
use crate::footagedescription::StreamEntry;
|
||||
|
||||
/// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes
|
||||
/// every test in this module (they share the global probe error, the
|
||||
@@ -793,9 +794,8 @@ mod tests {
|
||||
) -> Option<FootageDescription> {
|
||||
if filename.ends_with("test_video.mp4") {
|
||||
let mut desc = FootageDescription::new("fake");
|
||||
let vp = unsafe {
|
||||
oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000)
|
||||
};
|
||||
let vp =
|
||||
unsafe { oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000) };
|
||||
unsafe { oakcommon_videoparams_set_stream_index(vp.clone(), 0) };
|
||||
desc.push_stream(StreamEntry::Video(vp));
|
||||
Some(desc)
|
||||
@@ -900,11 +900,8 @@ mod tests {
|
||||
}
|
||||
|
||||
fn media_file(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_ffi_dec_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakcodec_ffi_dec_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join(name);
|
||||
let _ = std::fs::write(&path, b"media");
|
||||
@@ -930,7 +927,10 @@ mod tests {
|
||||
|
||||
assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 1);
|
||||
assert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) },
|
||||
0
|
||||
);
|
||||
|
||||
let mut info: OakCodecVideoStreamInfo = unsafe { std::mem::zeroed() };
|
||||
let rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, &mut info) };
|
||||
@@ -1001,7 +1001,10 @@ mod tests {
|
||||
assert!(h.is_null());
|
||||
let mut err = [0i8; 256];
|
||||
unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("no filename given"));
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(err.as_ptr()).as_deref(),
|
||||
Some("no filename given")
|
||||
);
|
||||
|
||||
// Empty filename.
|
||||
let f = cstr("");
|
||||
@@ -1025,12 +1028,21 @@ mod tests {
|
||||
let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };
|
||||
assert!(h.is_null());
|
||||
unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };
|
||||
assert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains("no decoder recognizes"));
|
||||
assert!(crate::ffi::c_str(err.as_ptr())
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("no decoder recognizes"));
|
||||
|
||||
// Empty handle on probe exports.
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(empty) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_probe_video_stream_count(empty) },
|
||||
0
|
||||
);
|
||||
restore();
|
||||
}
|
||||
|
||||
@@ -1055,8 +1067,14 @@ mod tests {
|
||||
|
||||
let mut frame = unsafe { oakcodec_decoder_decode_video(h, 1, 30) };
|
||||
assert!(!frame.is_null());
|
||||
assert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_width(frame) }, 100);
|
||||
assert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_height(frame) }, 50);
|
||||
assert_eq!(
|
||||
unsafe { crate::ffi::frame::oakcodec_frame_width(frame) },
|
||||
100
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { crate::ffi::frame::oakcodec_frame_height(frame) },
|
||||
50
|
||||
);
|
||||
|
||||
unsafe { crate::ffi::frame::oakcodec_frame_free(&mut frame) };
|
||||
let rc = unsafe { oakcodec_decoder_close(h) };
|
||||
@@ -1079,7 +1097,10 @@ mod tests {
|
||||
assert!(frame.is_null());
|
||||
let mut err = [0i8; 128];
|
||||
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 128) };
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("failed to decode video frame"));
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(err.as_ptr()).as_deref(),
|
||||
Some("failed to decode video frame")
|
||||
);
|
||||
unsafe { oakcodec_decoder_free(&mut h) };
|
||||
restore();
|
||||
}
|
||||
@@ -1097,9 +1118,7 @@ mod tests {
|
||||
|
||||
let mut buf = [0f32; 64];
|
||||
let frames = unsafe {
|
||||
oakcodec_decoder_decode_audio(
|
||||
h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16,
|
||||
)
|
||||
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(frames, 16);
|
||||
// Interleaved stereo filled by the fake.
|
||||
@@ -1107,20 +1126,28 @@ mod tests {
|
||||
assert_eq!(buf[31], 1.0);
|
||||
|
||||
// Invalid args.
|
||||
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Not open -> E_STATE.
|
||||
let mut h2 = unsafe { oakcodec_decoder_init() };
|
||||
let rc = unsafe { oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
unsafe { oakcodec_decoder_free(&mut h2) };
|
||||
|
||||
// Empty handle -> E_INVALID.
|
||||
let empty = CHandle::null();
|
||||
let rc = unsafe { oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
unsafe { oakcodec_decoder_free(&mut h) };
|
||||
@@ -1137,11 +1164,16 @@ mod tests {
|
||||
let f = cstr(&v);
|
||||
unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };
|
||||
let mut buf = [0f32; 64];
|
||||
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let mut err = [0i8; 256];
|
||||
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };
|
||||
assert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains("conform"));
|
||||
assert!(crate::ffi::c_str(err.as_ptr())
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("conform"));
|
||||
unsafe { oakcodec_decoder_free(&mut h) };
|
||||
restore();
|
||||
}
|
||||
@@ -1192,9 +1224,8 @@ mod tests {
|
||||
// Failure with a cancelled atom -> E_CANCELLED.
|
||||
let atom = unsafe { oakrender_cancelatom_init() };
|
||||
unsafe { oakrender_cancelatom_cancel(atom.clone()) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom)
|
||||
};
|
||||
let rc =
|
||||
unsafe { oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom) };
|
||||
assert_eq!(rc, OAKCODEC_E_CANCELLED);
|
||||
|
||||
unsafe { oakcodec_decoder_free(&mut h) };
|
||||
@@ -1211,17 +1242,46 @@ mod tests {
|
||||
unsafe { oakcodec_decoder_get_image_sequence_digit_count(f.as_ptr()) },
|
||||
4
|
||||
);
|
||||
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) }, 1);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) },
|
||||
1
|
||||
);
|
||||
|
||||
let mut buf = [0i8; 128];
|
||||
let rc = unsafe { oakcodec_decoder_transform_image_sequence_file_name(f.as_ptr(), 7, buf.as_mut_ptr(), 128) };
|
||||
let rc = unsafe {
|
||||
oakcodec_decoder_transform_image_sequence_file_name(
|
||||
f.as_ptr(),
|
||||
7,
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
};
|
||||
assert!(rc > 0);
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("frame_0007.png"));
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some("frame_0007.png")
|
||||
);
|
||||
|
||||
// NULL filename -> E_INVALID.
|
||||
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) }, OAKCODEC_E_INVALID as i64);
|
||||
assert_eq!(unsafe { oakcodec_decoder_transform_image_sequence_file_name(std::ptr::null(), 1, buf.as_mut_ptr(), 128) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) },
|
||||
OAKCODEC_E_INVALID as i64
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_decoder_transform_image_sequence_file_name(
|
||||
std::ptr::null(),
|
||||
1,
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
},
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1247,7 +1307,10 @@ mod tests {
|
||||
assert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);
|
||||
let mut err = [0i8; 256];
|
||||
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("file not found: /missing/file.mp4"));
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(err.as_ptr()).as_deref(),
|
||||
Some("file not found: /missing/file.mp4")
|
||||
);
|
||||
|
||||
// Empty handle -> E_INVALID and empty last_error.
|
||||
let empty = CHandle::null();
|
||||
|
||||
@@ -35,8 +35,8 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakcore_rs::PixelFormat;
|
||||
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::encoder::Encoder;
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::handle::{self, CHandle};
|
||||
|
||||
/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/
|
||||
@@ -204,7 +204,8 @@ pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
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),
|
||||
@@ -233,7 +234,8 @@ pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
|
||||
#[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 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);
|
||||
@@ -242,16 +244,22 @@ pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
|
||||
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()));
|
||||
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()));
|
||||
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()));
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to open stream".to_string(),
|
||||
));
|
||||
}
|
||||
b.encoder = Some(e);
|
||||
b.open = true;
|
||||
@@ -263,7 +271,8 @@ pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
|
||||
#[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 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 = {
|
||||
@@ -287,7 +296,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_audio(
|
||||
frame_count: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
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);
|
||||
}
|
||||
@@ -325,7 +335,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
|
||||
out_seconds: f64,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
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),
|
||||
@@ -347,7 +358,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
|
||||
#[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 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);
|
||||
@@ -454,7 +466,9 @@ mod tests {
|
||||
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};
|
||||
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()
|
||||
@@ -566,7 +580,10 @@ mod tests {
|
||||
// write_video with a real frame handle.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(16, 16) };
|
||||
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(fh) }, crate::error::OAKCODEC_OK);
|
||||
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);
|
||||
|
||||
@@ -648,13 +665,24 @@ mod tests {
|
||||
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"));
|
||||
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);
|
||||
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(""));
|
||||
@@ -672,7 +700,8 @@ mod tests {
|
||||
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()) };
|
||||
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.
|
||||
@@ -703,7 +732,10 @@ mod tests {
|
||||
|
||||
let p = valid_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);
|
||||
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) };
|
||||
@@ -732,7 +764,10 @@ mod tests {
|
||||
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);
|
||||
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);
|
||||
@@ -760,7 +795,8 @@ mod tests {
|
||||
|
||||
// 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()) };
|
||||
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);
|
||||
@@ -769,7 +805,8 @@ mod tests {
|
||||
|
||||
// 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()) };
|
||||
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);
|
||||
|
||||
@@ -74,9 +74,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_extension(
|
||||
|
||||
/// `oakcodec_encoding_format_video_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
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,
|
||||
@@ -104,9 +102,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
|
||||
|
||||
/// `oakcodec_encoding_format_audio_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
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,
|
||||
@@ -134,9 +130,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
|
||||
|
||||
/// `oakcodec_encoding_format_subtitle_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
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,
|
||||
@@ -201,10 +195,7 @@ pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_
|
||||
/// 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 {
|
||||
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),
|
||||
@@ -359,7 +350,10 @@ mod tests {
|
||||
// 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"));
|
||||
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"));
|
||||
@@ -402,7 +396,10 @@ mod tests {
|
||||
);
|
||||
// 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_count(13) },
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
|
||||
17 // SRT
|
||||
@@ -439,7 +436,10 @@ mod tests {
|
||||
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);
|
||||
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);
|
||||
@@ -478,9 +478,18 @@ mod tests {
|
||||
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);
|
||||
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);
|
||||
@@ -505,24 +514,38 @@ mod tests {
|
||||
let mut buf = [0i8; 128];
|
||||
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out_[#####].png").as_ptr()) },
|
||||
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()) },
|
||||
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_filename_contains_digit_placeholder(std::ptr::null()) }, 0);
|
||||
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr()) },
|
||||
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);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
|
||||
let rc = unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
@@ -532,7 +555,10 @@ mod tests {
|
||||
)
|
||||
};
|
||||
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!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some("/tmp/out.png")
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
|
||||
@@ -54,9 +54,7 @@ pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle {
|
||||
/// (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))))
|
||||
})
|
||||
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::with_params(params)))))
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.
|
||||
@@ -144,7 +142,9 @@ pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void {
|
||||
/// `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) })) {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe {
|
||||
frame_const_data_inner(&frame)
|
||||
})) {
|
||||
Ok(p) => p,
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
@@ -350,9 +350,15 @@ mod tests {
|
||||
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);
|
||||
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!(
|
||||
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) };
|
||||
@@ -365,15 +371,27 @@ mod tests {
|
||||
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);
|
||||
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) };
|
||||
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_allocate(h) },
|
||||
crate::error::OAKCODEC_E_STATE
|
||||
);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
}
|
||||
@@ -409,7 +427,10 @@ mod tests {
|
||||
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);
|
||||
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) };
|
||||
@@ -423,7 +444,10 @@ mod tests {
|
||||
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_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);
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
/// `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;
|
||||
|
||||
@@ -292,7 +292,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_proxy_{}_{}", name, std::process::id()));
|
||||
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()
|
||||
}
|
||||
@@ -307,8 +311,14 @@ mod tests {
|
||||
#[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);
|
||||
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);
|
||||
@@ -334,20 +344,38 @@ mod tests {
|
||||
|
||||
// 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) };
|
||||
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);
|
||||
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);
|
||||
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];
|
||||
@@ -368,7 +396,8 @@ mod tests {
|
||||
|
||||
// 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) };
|
||||
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(),
|
||||
@@ -389,29 +418,45 @@ mod tests {
|
||||
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) };
|
||||
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) };
|
||||
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) };
|
||||
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()) };
|
||||
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) };
|
||||
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) };
|
||||
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) };
|
||||
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());
|
||||
|
||||
+145
-87
@@ -48,13 +48,13 @@ use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use ffmpeg::ffi as sys;
|
||||
use ffmpeg::format::sample::Type as SampleType;
|
||||
use ffmpeg::format::{Pixel, Sample};
|
||||
use ffmpeg::media::Type as MediaType;
|
||||
use ffmpeg::software::{resampling, scaling};
|
||||
use ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
|
||||
use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};
|
||||
|
||||
@@ -66,8 +66,8 @@ use crate::bridge::common::{
|
||||
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,
|
||||
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};
|
||||
@@ -94,9 +94,9 @@ const PIXEL_F32_BYTES: usize = 16;
|
||||
/// Lazily initialize the FFmpeg libraries (idempotent, at most once).
|
||||
fn ffmpeg_init() -> crate::error::Result<()> {
|
||||
static INIT: OnceLock<Result<(), String>> = OnceLock::new();
|
||||
if let Err(e) = INIT.get_or_init(|| {
|
||||
ffmpeg::init().map_err(|e| format!("ffmpeg initialization failed: {e}"))
|
||||
}) {
|
||||
if let Err(e) = INIT
|
||||
.get_or_init(|| ffmpeg::init().map_err(|e| format!("ffmpeg initialization failed: {e}")))
|
||||
{
|
||||
return Err(crate::error::Error::Failed(e.clone()));
|
||||
}
|
||||
Ok(())
|
||||
@@ -258,7 +258,11 @@ impl Decoder for FFmpegDecoder {
|
||||
true
|
||||
}
|
||||
|
||||
fn probe(&self, filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<FootageDescription> {
|
||||
fn probe(
|
||||
&self,
|
||||
filename: &str,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
) -> Option<FootageDescription> {
|
||||
ffmpeg_init().ok()?;
|
||||
probe_file(filename, cancelled)
|
||||
}
|
||||
@@ -381,7 +385,13 @@ impl Decoder for FFmpegDecoder {
|
||||
if !matches!(state.inner, DecoderInner::Audio(_)) {
|
||||
return Err(fail("decoder is not open on an audio stream"));
|
||||
}
|
||||
state.conform_audio_to(output_filenames, sample_rate, channel_layout, sample_format, cancelled)
|
||||
state.conform_audio_to(
|
||||
output_filenames,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
cancelled,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_audio_start_offset(&self) -> Rational {
|
||||
@@ -392,7 +402,8 @@ impl Decoder for FFmpegDecoder {
|
||||
match state.as_ref() {
|
||||
Some(s) if s.format_start_time != AV_NOPTS_VALUE => {
|
||||
let fmt_start = Rational::new(s.format_start_time, FB_TIME_BASE);
|
||||
let str_start = oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);
|
||||
let str_start =
|
||||
oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);
|
||||
fmt_start - str_start
|
||||
}
|
||||
_ => Rational::new(0, 1),
|
||||
@@ -490,8 +501,8 @@ impl DecoderState {
|
||||
let mut dict = Dictionary::new();
|
||||
dict.set("analyzeduration", "5000000");
|
||||
dict.set("probesize", "20000000");
|
||||
let input = ffmpeg::format::input_with_dictionary(&stream.filename(), dict)
|
||||
.map_err(ffmpeg_err)?;
|
||||
let input =
|
||||
ffmpeg::format::input_with_dictionary(&stream.filename(), dict).map_err(ffmpeg_err)?;
|
||||
|
||||
let stream_index = stream.stream() as usize;
|
||||
let fstream = input
|
||||
@@ -512,8 +523,7 @@ impl DecoderState {
|
||||
let raw = unsafe { params.as_ptr() };
|
||||
input_sample_format = sample_from_raw(unsafe { (*raw).format });
|
||||
input_sample_rate = unsafe { (*raw).sample_rate }.max(0) as u32;
|
||||
input_channel_layout_mask =
|
||||
unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
|
||||
input_channel_layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
|
||||
}
|
||||
|
||||
let mut open_opts = Dictionary::new();
|
||||
@@ -668,9 +678,8 @@ impl DecoderState {
|
||||
.stream(self.stream_index)
|
||||
.map(|s| s.time_base())
|
||||
.unwrap_or(FfRational(1, 1));
|
||||
let target = unsafe {
|
||||
sys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into())
|
||||
};
|
||||
let target =
|
||||
unsafe { sys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into()) };
|
||||
let ret = unsafe {
|
||||
sys::av_seek_frame(
|
||||
self.input.as_mut_ptr(),
|
||||
@@ -854,21 +863,31 @@ impl DecoderState {
|
||||
|
||||
// swscale cannot reliably output float RGBA on every build; prefer
|
||||
// RGBAF32LE and fall back to RGBA64 (converted to f32 below).
|
||||
let bytes = match get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h) {
|
||||
Ok(ctx) => {
|
||||
let mut out = ffmpeg::frame::Video::empty();
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let stride = out.stride(0);
|
||||
convert_rgba_f32_le(&out.data(0), w, h, stride)
|
||||
}
|
||||
Err(_) => {
|
||||
let ctx = get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBA64LE, w, h)?;
|
||||
let mut out = ffmpeg::frame::Video::empty();
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let stride = out.stride(0);
|
||||
convert_rgba64_to_f32(&out.data(0), w, h, stride)
|
||||
}
|
||||
};
|
||||
let bytes =
|
||||
match get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h)
|
||||
{
|
||||
Ok(ctx) => {
|
||||
let mut out = ffmpeg::frame::Video::empty();
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let stride = out.stride(0);
|
||||
convert_rgba_f32_le(&out.data(0), w, h, stride)
|
||||
}
|
||||
Err(_) => {
|
||||
let ctx = get_or_create_scaler(
|
||||
&mut video.scaler,
|
||||
src_format,
|
||||
w,
|
||||
h,
|
||||
Pixel::RGBA64LE,
|
||||
w,
|
||||
h,
|
||||
)?;
|
||||
let mut out = ffmpeg::frame::Video::empty();
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let stride = out.stride(0);
|
||||
convert_rgba64_to_f32(&out.data(0), w, h, stride)
|
||||
}
|
||||
};
|
||||
Ok((w, h, bytes))
|
||||
}
|
||||
|
||||
@@ -899,21 +918,17 @@ impl DecoderState {
|
||||
dest.fill(0.0);
|
||||
|
||||
// Seek to just before the range start.
|
||||
let start_ts = oak_rational(self.stream_time_base)
|
||||
.time_to_timestamp(Rational::from_double(start_sec));
|
||||
let start_ts =
|
||||
oak_rational(self.stream_time_base).time_to_timestamp(Rational::from_double(start_sec));
|
||||
self.seek(start_ts)?;
|
||||
|
||||
// Take the cached resampler out (or create one) so the decode loop
|
||||
// below can borrow `self` freely; it is put back before returning.
|
||||
let src_layout = channel_layout_from_mask(self.input_channel_layout_mask);
|
||||
let mut resampler = match self
|
||||
.audio
|
||||
.as_mut()
|
||||
.expect("audio session")
|
||||
.resampler
|
||||
.take()
|
||||
{
|
||||
Some((rate, layout, rs)) if rate == sample_rate as u32 && layout == channel_layout => rs,
|
||||
let mut resampler = match self.audio.as_mut().expect("audio session").resampler.take() {
|
||||
Some((rate, layout, rs)) if rate == sample_rate as u32 && layout == channel_layout => {
|
||||
rs
|
||||
}
|
||||
_ => AudioResampler::get(
|
||||
self.input_sample_format,
|
||||
src_layout,
|
||||
@@ -939,7 +954,9 @@ impl DecoderState {
|
||||
let chunk_samples = (converted.len() / dst_channels) as i64;
|
||||
let frame_start = match audio.pts() {
|
||||
Some(pts) => {
|
||||
let secs = oak_rational(stream_time_base).timestamp_to_time(pts).to_f64();
|
||||
let secs = oak_rational(stream_time_base)
|
||||
.timestamp_to_time(pts)
|
||||
.to_f64();
|
||||
(secs * sample_rate as f64).round() as i64
|
||||
}
|
||||
None => next_sample.unwrap_or(start_sample),
|
||||
@@ -965,10 +982,8 @@ impl DecoderState {
|
||||
}
|
||||
|
||||
// Put the resampler back into the cache for the next call.
|
||||
self.audio
|
||||
.as_mut()
|
||||
.expect("audio session")
|
||||
.resampler = Some((sample_rate as u32, channel_layout, resampler));
|
||||
self.audio.as_mut().expect("audio session").resampler =
|
||||
Some((sample_rate as u32, channel_layout, resampler));
|
||||
|
||||
// Flush any samples still buffered in the resampler (rate conversion
|
||||
// tail), appending after the last decoded sample.
|
||||
@@ -1015,7 +1030,9 @@ impl DecoderState {
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
if self.input_channel_layout_mask == 0 {
|
||||
return Err(fail("could not determine the channel layout of the audio file"));
|
||||
return Err(fail(
|
||||
"could not determine the channel layout of the audio file",
|
||||
));
|
||||
}
|
||||
|
||||
let target_fmt = crate::encodingparams::sample_format_from_i32(sample_format);
|
||||
@@ -1080,7 +1097,6 @@ impl DecoderState {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl AudioResampler {
|
||||
@@ -1109,7 +1125,10 @@ impl AudioResampler {
|
||||
|
||||
/// Convert one input frame, returning one byte buffer per output plane
|
||||
/// (plane 0 for packed destinations).
|
||||
fn convert_to_planes(&mut self, input: &ffmpeg::frame::Audio) -> crate::error::Result<Vec<Vec<u8>>> {
|
||||
fn convert_to_planes(
|
||||
&mut self,
|
||||
input: &ffmpeg::frame::Audio,
|
||||
) -> crate::error::Result<Vec<Vec<u8>>> {
|
||||
let (written, bufs) = self.swr_convert_buffers(input)?;
|
||||
let _ = written;
|
||||
Ok(bufs)
|
||||
@@ -1117,7 +1136,10 @@ impl AudioResampler {
|
||||
|
||||
/// Convert one input frame into a freshly allocated output frame in the
|
||||
/// destination format, layout and rate.
|
||||
fn convert_to_frame(&mut self, input: &ffmpeg::frame::Audio) -> crate::error::Result<ffmpeg::frame::Audio> {
|
||||
fn convert_to_frame(
|
||||
&mut self,
|
||||
input: &ffmpeg::frame::Audio,
|
||||
) -> crate::error::Result<ffmpeg::frame::Audio> {
|
||||
let (written, bufs) = self.swr_convert_buffers(input)?;
|
||||
let mut out = ffmpeg::frame::Audio::new(self.dst_format, written, self.dst_layout);
|
||||
out.set_rate(self.dst_rate);
|
||||
@@ -1159,7 +1181,9 @@ impl AudioResampler {
|
||||
if out_samples == 0 {
|
||||
return Ok((0, Vec::new()));
|
||||
}
|
||||
let in_ptrs: Vec<*const u8> = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();
|
||||
let in_ptrs: Vec<*const u8> = (0..input.planes())
|
||||
.map(|i| input.data(i).as_ptr())
|
||||
.collect();
|
||||
let (bufs, written) = self.swr_convert(out_samples as usize, Some(&in_ptrs), in_samples)?;
|
||||
Ok((written, bufs))
|
||||
}
|
||||
@@ -1191,7 +1215,8 @@ impl AudioResampler {
|
||||
return Err(ffmpeg_err(FfmpegError::from(written)));
|
||||
}
|
||||
for buf in bufs.iter_mut() {
|
||||
let keep = written as usize * self.dst_format.bytes()
|
||||
let keep = written as usize
|
||||
* self.dst_format.bytes()
|
||||
* if planar { 1 } else { self.dst_channels };
|
||||
buf.truncate(keep);
|
||||
}
|
||||
@@ -1217,7 +1242,9 @@ fn resample_to_interleaved_f32(
|
||||
let channels = resampler.dst_channels;
|
||||
let mut buf = vec![0f32; out_samples * channels];
|
||||
let out_ptrs = [buf.as_mut_ptr() as *mut u8];
|
||||
let in_ptrs: Vec<*const u8> = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();
|
||||
let in_ptrs: Vec<*const u8> = (0..input.planes())
|
||||
.map(|i| input.data(i).as_ptr())
|
||||
.collect();
|
||||
let written = unsafe {
|
||||
sys::swr_convert(
|
||||
resampler.ctx.as_mut_ptr(),
|
||||
@@ -1369,7 +1396,8 @@ fn convert_rgba_f32_le(data: &[u8], w: u32, h: u32, stride: usize) -> Vec<u8> {
|
||||
let mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];
|
||||
for y in 0..h as usize {
|
||||
let row = &data[y * stride..y * stride + (w as usize) * PIXEL_F32_BYTES];
|
||||
let dst = &mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
|
||||
let dst =
|
||||
&mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
|
||||
dst.copy_from_slice(row);
|
||||
for px in dst.chunks_exact_mut(PIXEL_F32_BYTES) {
|
||||
px[12..16].copy_from_slice(&1.0f32.to_le_bytes());
|
||||
@@ -1383,8 +1411,12 @@ fn convert_rgba64_to_f32(data: &[u8], w: u32, h: u32, stride: usize) -> Vec<u8>
|
||||
let mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];
|
||||
for y in 0..h as usize {
|
||||
let row = &data[y * stride..y * stride + (w as usize) * 8];
|
||||
let dst = &mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
|
||||
for (px, src_px) in dst.chunks_exact_mut(PIXEL_F32_BYTES).zip(row.chunks_exact(8)) {
|
||||
let dst =
|
||||
&mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
|
||||
for (px, src_px) in dst
|
||||
.chunks_exact_mut(PIXEL_F32_BYTES)
|
||||
.zip(row.chunks_exact(8))
|
||||
{
|
||||
for c in 0..3 {
|
||||
let v = u16::from_le_bytes([src_px[c * 2], src_px[c * 2 + 1]]);
|
||||
px[c * 4..c * 4 + 4].copy_from_slice(&(v as f32 / 65535.0).to_le_bytes());
|
||||
@@ -1463,7 +1495,9 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
|
||||
if cancel_atom_is_cancelled(cancelled) {
|
||||
return None;
|
||||
}
|
||||
let Some(stream) = input.stream(i as usize) else { continue };
|
||||
let Some(stream) = input.stream(i as usize) else {
|
||||
continue;
|
||||
};
|
||||
let params = stream.parameters();
|
||||
let medium = params.medium();
|
||||
|
||||
@@ -1475,7 +1509,9 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
|
||||
.collect();
|
||||
let raw = unsafe { params.as_ptr() };
|
||||
source_start_time =
|
||||
extract_source_start_time(&stream_meta, stream.time_base(), unsafe { (*raw).sample_rate });
|
||||
extract_source_start_time(&stream_meta, stream.time_base(), unsafe {
|
||||
(*raw).sample_rate
|
||||
});
|
||||
}
|
||||
|
||||
// Only proceed if a decoder exists for this stream
|
||||
@@ -1500,9 +1536,16 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
|
||||
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_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_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());
|
||||
@@ -1565,7 +1608,10 @@ fn extract_source_start_time(
|
||||
timebase: FfRational,
|
||||
sample_rate: i32,
|
||||
) -> SourceTime {
|
||||
let mut out = SourceTime { valid: false, time: Rational::new(0, 1) };
|
||||
let mut out = SourceTime {
|
||||
valid: false,
|
||||
time: Rational::new(0, 1),
|
||||
};
|
||||
for (key, value) in metadata {
|
||||
if key == "timecode" {
|
||||
let parsed = crate::timecodemetadata::SourceTime::from_timecode_string(
|
||||
@@ -1578,10 +1624,8 @@ fn extract_source_start_time(
|
||||
return out;
|
||||
}
|
||||
} else if key == "time_reference" {
|
||||
let parsed = crate::timecodemetadata::SourceTime::from_bwf_time_reference(
|
||||
value,
|
||||
sample_rate,
|
||||
);
|
||||
let parsed =
|
||||
crate::timecodemetadata::SourceTime::from_bwf_time_reference(value, sample_rate);
|
||||
if parsed.valid {
|
||||
out.valid = true;
|
||||
out.time = parsed.time;
|
||||
@@ -1664,7 +1708,10 @@ impl FFmpegEncoder {
|
||||
/// The effective encoding parameters (configured overrides construction).
|
||||
fn effective_params(&self) -> EncodingParams {
|
||||
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
state.configured.clone().unwrap_or_else(|| self.params.clone())
|
||||
state
|
||||
.configured
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.params.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1739,7 +1786,9 @@ impl Encoder for FFmpegEncoder {
|
||||
// Subtitle muxing is not exposed by the crate's encoder trait flow
|
||||
// (the C++ writes through the bridge's SRT encoder); report the same
|
||||
// unsupported state the stub did.
|
||||
Err(fail("subtitle encoding is not supported by the ffmpeg encoder"))
|
||||
Err(fail(
|
||||
"subtitle encoding is not supported by the ffmpeg encoder",
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&self) -> crate::error::Result<()> {
|
||||
@@ -1798,7 +1847,10 @@ impl EncoderState {
|
||||
let width = params.video_width.max(1) as u32;
|
||||
let height = params.video_height.max(1) as u32;
|
||||
let time_base = FfRational(params.video_time_base_num, params.video_time_base_den);
|
||||
let frame_rate = FfRational(params.video_time_base_den, params.video_time_base_num.max(1));
|
||||
let frame_rate = FfRational(
|
||||
params.video_time_base_den,
|
||||
params.video_time_base_num.max(1),
|
||||
);
|
||||
|
||||
let mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;
|
||||
let stream_index = stream.index();
|
||||
@@ -1944,7 +1996,10 @@ impl EncoderState {
|
||||
for y in 0..(h as usize) {
|
||||
let row = &data[y * linesize..y * linesize + (w as usize) * PIXEL_F32_BYTES];
|
||||
let dst = &mut rgba[y * (w as usize) * 4..(y + 1) * (w as usize) * 4];
|
||||
for (out_px, in_px) in dst.chunks_exact_mut(4).zip(row.chunks_exact(PIXEL_F32_BYTES)) {
|
||||
for (out_px, in_px) in dst
|
||||
.chunks_exact_mut(4)
|
||||
.zip(row.chunks_exact(PIXEL_F32_BYTES))
|
||||
{
|
||||
for c in 0..4 {
|
||||
let v = f32::from_le_bytes([
|
||||
in_px[c * 4],
|
||||
@@ -1967,7 +2022,10 @@ impl EncoderState {
|
||||
// `FFmpegEncoder::write_frame` passes the frame time in seconds; the
|
||||
// Rust `Frame` carries the timestamp as a rational.
|
||||
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 = FfRational(
|
||||
params.video_time_base_num.max(1),
|
||||
params.video_time_base_den.max(1),
|
||||
);
|
||||
let pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;
|
||||
scaled.set_pts(Some(pts));
|
||||
|
||||
@@ -2004,10 +2062,10 @@ impl EncoderState {
|
||||
let pts = output.audio_pts;
|
||||
|
||||
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)
|
||||
};
|
||||
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);
|
||||
|
||||
let mut converted = audio.resampler.convert_to_frame(&input)?;
|
||||
@@ -2122,20 +2180,20 @@ fn drain_audio_packets(
|
||||
/// documented `ExportCodec::Codec` discriminants (see `exportcodec.rs`).
|
||||
fn export_codec_to_id(codec: i32) -> Option<ffmpeg::codec::Id> {
|
||||
match codec {
|
||||
0 => Some(ffmpeg::codec::Id::DNXHD), // DNxHD
|
||||
1 | 2 => Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB
|
||||
3 => Some(ffmpeg::codec::Id::HEVC), // H265
|
||||
6 => Some(ffmpeg::codec::Id::PRORES), // ProRes
|
||||
7 => Some(ffmpeg::codec::Id::CFHD), // CineForm
|
||||
0 => Some(ffmpeg::codec::Id::DNXHD), // DNxHD
|
||||
1 | 2 => Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB
|
||||
3 => Some(ffmpeg::codec::Id::HEVC), // H265
|
||||
6 => Some(ffmpeg::codec::Id::PRORES), // ProRes
|
||||
7 => Some(ffmpeg::codec::Id::CFHD), // CineForm
|
||||
10 => Some(ffmpeg::codec::Id::MPEG2VIDEO), // MP2
|
||||
11 => Some(ffmpeg::codec::Id::MP3), // MP3
|
||||
12 => Some(ffmpeg::codec::Id::AAC), // AAC
|
||||
13 => Some(ffmpeg::codec::Id::PCM_S16LE), // PCM
|
||||
14 => Some(ffmpeg::codec::Id::OPUS), // Opus
|
||||
15 => Some(ffmpeg::codec::Id::VORBIS), // Vorbis
|
||||
16 => Some(ffmpeg::codec::Id::FLAC), // FLAC
|
||||
17 => Some(ffmpeg::codec::Id::SUBRIP), // SRT
|
||||
18 => Some(ffmpeg::codec::Id::AV1), // AV1
|
||||
11 => Some(ffmpeg::codec::Id::MP3), // MP3
|
||||
12 => Some(ffmpeg::codec::Id::AAC), // AAC
|
||||
13 => Some(ffmpeg::codec::Id::PCM_S16LE), // PCM
|
||||
14 => Some(ffmpeg::codec::Id::OPUS), // Opus
|
||||
15 => Some(ffmpeg::codec::Id::VORBIS), // Vorbis
|
||||
16 => Some(ffmpeg::codec::Id::FLAC), // FLAC
|
||||
17 => Some(ffmpeg::codec::Id::SUBRIP), // SRT
|
||||
18 => Some(ffmpeg::codec::Id::AV1), // AV1
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
//! 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,
|
||||
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 oakcore_rs::{PixelFormat, Rational};
|
||||
|
||||
@@ -166,9 +166,8 @@ impl Frame {
|
||||
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())
|
||||
});
|
||||
let fmt =
|
||||
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) });
|
||||
Self::generate_linesize_bytes(fmt, w)
|
||||
}
|
||||
None => 0,
|
||||
@@ -194,9 +193,7 @@ impl Frame {
|
||||
|
||||
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 format = pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(params) });
|
||||
|
||||
let linesize = Self::generate_linesize_bytes(format, width);
|
||||
let size = (linesize as usize).wrapping_mul(height as usize);
|
||||
@@ -399,7 +396,10 @@ mod tests {
|
||||
fn linesize_is_32_byte_aligned_for_u8() {
|
||||
// U8 RGBA: 4 bytes/pixel, width rounded up to a 32-byte boundary.
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 9), 4 * 32);
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 100), 4 * 128);
|
||||
assert_eq!(
|
||||
Frame::generate_linesize_bytes(PixelFormat::U8, 100),
|
||||
4 * 128
|
||||
);
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 0), 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_equals, OakVideoParams,
|
||||
};
|
||||
use crate::bridge::common::{oakcommon_videoparams_equals, OakVideoParams};
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `olive::FrameManager`: singleton frame pool with background GC.
|
||||
@@ -76,10 +74,7 @@ impl FrameManager {
|
||||
pub fn create_frame(&self, params: OakVideoParams) -> Arc<Frame> {
|
||||
let frame = {
|
||||
let mut pool = self.pool.lock().unwrap();
|
||||
match pool
|
||||
.iter()
|
||||
.position(|f| frame_matches(f, ¶ms))
|
||||
{
|
||||
match pool.iter().position(|f| frame_matches(f, ¶ms)) {
|
||||
Some(idx) => pool.swap_remove(idx),
|
||||
None => Frame::with_params(params),
|
||||
}
|
||||
@@ -147,9 +142,7 @@ fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool {
|
||||
let Some(frame_params) = frame.params() else {
|
||||
return false;
|
||||
};
|
||||
let eq = unsafe {
|
||||
oakcommon_videoparams_equals(frame_params.clone(), params.clone())
|
||||
};
|
||||
let eq = unsafe { oakcommon_videoparams_equals(frame_params.clone(), params.clone()) };
|
||||
eq != 0
|
||||
}
|
||||
|
||||
@@ -186,9 +179,7 @@ mod tests {
|
||||
|
||||
// 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)
|
||||
});
|
||||
let f2 = mgr.create_frame(unsafe { oakcommon_videoparams_init_basic(64, 64) });
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
Arc::try_unwrap(f2).unwrap();
|
||||
|
||||
@@ -243,7 +243,10 @@ mod tests {
|
||||
#[test]
|
||||
fn guard_maps_results_and_panics() {
|
||||
assert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);
|
||||
assert_eq!(guard(|| Err(crate::error::Error::Invalid)), OAKCODEC_E_INVALID);
|
||||
assert_eq!(
|
||||
guard(|| Err(crate::error::Error::Invalid)),
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(guard(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
|
||||
|
||||
let ok = guard_handle(|| Ok(make_owned(1u32)));
|
||||
@@ -252,10 +255,16 @@ mod tests {
|
||||
assert!(guard_handle(|| panic!("boom")).is_null());
|
||||
|
||||
assert_eq!(guard_raw(|| 5), 5);
|
||||
assert_eq!(guard_raw(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
|
||||
assert_eq!(
|
||||
guard_raw(|| panic!("boom")),
|
||||
crate::error::OAKCODEC_E_FAILED
|
||||
);
|
||||
|
||||
assert_eq!(guard_i64(|| 5), 5);
|
||||
assert_eq!(guard_i64(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED as i64);
|
||||
assert_eq!(
|
||||
guard_i64(|| panic!("boom")),
|
||||
crate::error::OAKCODEC_E_FAILED as i64
|
||||
);
|
||||
|
||||
let mut called = false;
|
||||
guard_void(|| called = true);
|
||||
|
||||
@@ -37,8 +37,8 @@ pub mod encodingparams;
|
||||
pub mod error;
|
||||
pub mod exportcodec;
|
||||
pub mod exportformat;
|
||||
pub mod ffmpeg;
|
||||
pub mod ffi;
|
||||
pub mod ffmpeg;
|
||||
pub mod footagedescription;
|
||||
pub mod frame;
|
||||
pub mod framemanager;
|
||||
|
||||
@@ -205,8 +205,7 @@ pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
|
||||
if header.linesize_bytes < 0 || header.height < 0 {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let expected =
|
||||
(header.linesize_bytes as u64).checked_mul(header.height as u64);
|
||||
let expected = (header.linesize_bytes as u64).checked_mul(header.height as u64);
|
||||
if expected != Some(header.pixel_len) {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
|
||||
@@ -89,12 +89,7 @@ impl PlanarFileDevice {
|
||||
/// Read `bytes_per_channel` bytes from each channel (at the current file
|
||||
/// position) into `data[i][offset..]`. Returns bytes read per channel, or
|
||||
/// -1 if closed or a buffer is too small.
|
||||
pub fn read(
|
||||
&mut self,
|
||||
data: &mut [&mut [u8]],
|
||||
bytes_per_channel: i64,
|
||||
offset: i64,
|
||||
) -> i64 {
|
||||
pub fn read(&mut self, data: &mut [&mut [u8]], bytes_per_channel: i64, offset: i64) -> i64 {
|
||||
if !self.is_open() {
|
||||
return -1;
|
||||
}
|
||||
@@ -113,12 +108,7 @@ impl PlanarFileDevice {
|
||||
|
||||
/// Write `bytes_per_channel` bytes to each channel from `data[i][offset..]`.
|
||||
/// Returns bytes written per channel, or -1.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
data: &[&[u8]],
|
||||
bytes_per_channel: i64,
|
||||
offset: i64,
|
||||
) -> i64 {
|
||||
pub fn write(&mut self, data: &[&[u8]], bytes_per_channel: i64, offset: i64) -> i64 {
|
||||
if !self.is_open() {
|
||||
return -1;
|
||||
}
|
||||
@@ -171,11 +161,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_planar_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakcodec_planar_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir
|
||||
}
|
||||
|
||||
@@ -256,7 +256,11 @@ impl ProxyManager {
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: if params.divider <= 1 { params.width } else { 0 },
|
||||
proxy_height: if params.divider <= 1 { params.height } else { 0 },
|
||||
proxy_height: if params.divider <= 1 {
|
||||
params.height
|
||||
} else {
|
||||
0
|
||||
},
|
||||
};
|
||||
|
||||
// Interim simplification: submission is synchronous.
|
||||
@@ -335,11 +339,7 @@ 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,
|
||||
)
|
||||
crate::bridge::common::oakcommon_config_get_int(std::ptr::null(), ckey.as_ptr(), default)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,11 +348,7 @@ 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,
|
||||
)
|
||||
crate::bridge::common::oakcommon_config_get_bool(std::ptr::null(), ckey.as_ptr(), default)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,10 +403,7 @@ fn unique_file_identifier(filename: &str) -> String {
|
||||
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,
|
||||
)
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(std::ptr::null_mut(), 0)
|
||||
};
|
||||
if size <= 1 {
|
||||
return String::new();
|
||||
@@ -457,11 +450,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_proxy_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakcodec_proxy_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
@@ -517,10 +507,7 @@ mod tests {
|
||||
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)
|
||||
);
|
||||
assert_eq!(f, format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id));
|
||||
|
||||
// Divider mode tags the divider instead of an absolute size.
|
||||
let mut d = p.clone();
|
||||
@@ -558,13 +545,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn proxy_state_to_string_mapping() {
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), "missing");
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Missing),
|
||||
"missing"
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Generating),
|
||||
"generating"
|
||||
);
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), "ready");
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), "failed");
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Ready),
|
||||
"ready"
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Failed),
|
||||
"failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -581,10 +577,9 @@ mod tests {
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let cache = temp_subdir("nostart");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
let (state, _f) = ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Missing);
|
||||
}
|
||||
|
||||
@@ -595,10 +590,9 @@ mod tests {
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
let (state, filename) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
let (state, filename) = ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Ready);
|
||||
assert_eq!(filename, f);
|
||||
}
|
||||
@@ -612,10 +606,9 @@ mod tests {
|
||||
);
|
||||
let cache = temp_subdir("start");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
let (state, _f) = ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
assert_eq!(state, ProxyState::Generating);
|
||||
}
|
||||
@@ -698,6 +691,9 @@ mod tests_extra {
|
||||
// Empty filename -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state(""), ProxyState::Missing);
|
||||
// A path that does not exist -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state("/nope/nope.mp4"), ProxyState::Missing);
|
||||
assert_eq!(
|
||||
ProxyManager::get_proxy_state("/nope/nope.mp4"),
|
||||
ProxyState::Missing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ use crate::bridge::common::{
|
||||
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
|
||||
};
|
||||
use crate::decoder::{
|
||||
CodecStream, Decoder, K_COLOR_RANGE_DEFAULT, RenderMode, RetrieveAudioStatus,
|
||||
RetrieveVideoParams,
|
||||
CodecStream, Decoder, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
|
||||
K_COLOR_RANGE_DEFAULT,
|
||||
};
|
||||
use crate::encoder::create_from_params;
|
||||
use crate::ffmpeg::FFmpegDecoder;
|
||||
@@ -118,8 +118,14 @@ 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);
|
||||
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;
|
||||
@@ -204,7 +210,10 @@ fn encode_h264_roundtrip_to_tmp() {
|
||||
|
||||
// The output exists and has a plausible size.
|
||||
assert!(out.exists(), "round-trip file was not created");
|
||||
assert!(out.metadata().unwrap().len() > 1000, "round-trip file is empty");
|
||||
assert!(
|
||||
out.metadata().unwrap().len() > 1000,
|
||||
"round-trip file is empty"
|
||||
);
|
||||
|
||||
// Probe the result: one 64x64 video stream.
|
||||
let d = FFmpegDecoder::new();
|
||||
@@ -246,7 +255,11 @@ fn audio_conform_writes_planar_pcm() {
|
||||
let meta = std::fs::metadata(path).expect("conform output exists");
|
||||
assert!(meta.len() > 0, "conform file is empty");
|
||||
// 1 second at 48kHz * 4 bytes = 192 KB minimum.
|
||||
assert!(meta.len() >= 192_000, "conform file too short: {}", meta.len());
|
||||
assert!(
|
||||
meta.len() >= 192_000,
|
||||
"conform file too short: {}",
|
||||
meta.len()
|
||||
);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
@@ -98,10 +98,8 @@ pub struct OakCodecTaskRequest {
|
||||
/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see
|
||||
/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a
|
||||
/// negative `OAKCODEC_E_*` code.
|
||||
pub type OakCodecTaskSubmitFn = unsafe extern "C" fn(
|
||||
req: *const OakCodecTaskRequest,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> i32;
|
||||
pub type OakCodecTaskSubmitFn =
|
||||
unsafe extern "C" fn(req: *const OakCodecTaskRequest, userdata: *mut std::ffi::c_void) -> i32;
|
||||
|
||||
/// One registered submit callback (extern-C from the host, or a crate
|
||||
/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.
|
||||
@@ -175,9 +173,7 @@ pub fn task_submit_is_registered() -> bool {
|
||||
///
|
||||
/// Returns `Ok(false)` when no callback is registered (nothing submitted),
|
||||
/// `Ok(true)` when accepted, or `Err` when the callback rejected it.
|
||||
pub fn submit_task(
|
||||
req: &TaskRequest,
|
||||
) -> crate::error::Result<bool> {
|
||||
pub fn submit_task(req: &TaskRequest) -> crate::error::Result<bool> {
|
||||
let g = TASK_SUBMIT.lock().unwrap();
|
||||
match &*g {
|
||||
SubmitCb::None => Ok(false),
|
||||
@@ -203,7 +199,10 @@ pub fn submit_task(
|
||||
if ret == OAKCODEC_OK {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(Error::Failed(format!("task submit rejected (code {})", ret)))
|
||||
Err(Error::Failed(format!(
|
||||
"task submit rejected (code {})",
|
||||
ret
|
||||
)))
|
||||
}
|
||||
}
|
||||
SubmitCb::Rust { cb, userdata } => {
|
||||
@@ -301,7 +300,10 @@ mod tests {
|
||||
#[test]
|
||||
fn extern_cb_accept_returns_ok() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());
|
||||
set_task_submit_cb_extern(
|
||||
Some(crate::conformmanager::test_util::accept_cb),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Conform,
|
||||
input_filename: "in.mp4",
|
||||
|
||||
@@ -104,8 +104,7 @@ fn timecode_to_time(timecode: &str, timebase: &Rational, drop_frame: bool) -> Op
|
||||
let m = real_fr_ts % frames_per10_minutes;
|
||||
|
||||
if m > drop_frames {
|
||||
frame_count -= drop_frames
|
||||
* ((m - drop_frames) / (llround(fr) * 60 - drop_frames));
|
||||
frame_count -= drop_frames * ((m - drop_frames) / (llround(fr) * 60 - drop_frames));
|
||||
}
|
||||
frame_count -= drop_frames * 9 * d;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user