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:
2026-08-11 23:04:48 +08:00
parent a209f63d52
commit 18ff60f147
351 changed files with 32880 additions and 15939 deletions
+20 -31
View File
@@ -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`.
-4
View File
@@ -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`
+53 -45
View File
@@ -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]
+2 -5
View File
@@ -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()
}
+25 -21
View File
@@ -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,
}
}
+19 -12
View File
@@ -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;
+53 -91
View File
@@ -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());
}
+19 -4
View File
@@ -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());
+70 -12
View File
@@ -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);
}
}
+144 -81
View File
@@ -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();
+57 -20
View File
@@ -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);
+51 -25
View File
@@ -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(
+36 -12
View File
@@ -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);
-1
View File
@@ -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;
+60 -15
View File
@@ -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
View File
@@ -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,
}
}
+10 -10
View File
@@ -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);
}
+4 -13
View File
@@ -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, &params))
{
match pool.iter().position(|f| frame_matches(f, &params)) {
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();
+12 -3
View File
@@ -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);
+1 -1
View File
@@ -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;
+1 -2
View File
@@ -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);
}
+4 -17
View File
@@ -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
}
+36 -40
View File
@@ -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
);
}
}
+19 -6
View File
@@ -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);
+11 -9
View File
@@ -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",
+1 -2
View File
@@ -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;
}