Files
oak-editor/crates/oakcodec/src/ffi/format.rs
T
Mike-Solar 18ff60f147 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)
2026-08-11 23:04:48 +08:00

574 lines
17 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! `include/codec/format.h` exports.
//!
//! Complete inventory: format_count / format_name / format_extension /
//! format_{video,audio,subtitle}_codec_{count,at} / codec_name /
//! codec_is_still_image / codec_is_lossless / pix_fmt_{count,at,index} /
//! sample_format_{count,at} / filename_contains_digit_placeholder /
//! image_sequence_digit_count / filename_remove_digit_placeholder.
//!
//! # CPP-PARITY
//! The C++ `c_api/format.cpp` mirrors the facade (oakengine/encoding.h)
//! against the `olive::ExportFormat` / `olive::ExportCodec` / `olive::Encoder`
//! statics. The Rust tables live in [`crate::exportformat`] /
//! [`crate::exportcodec`]; the encoder pixel-format query is bridge-dependent
//! on the C++ side and returns empty here (see
//! `Format::get_pixel_formats_for_codec`), so `oakcodec_encoding_pix_fmt_*`
//! report 0/`E_NOT_FOUND`/0 (the preferred-format fallback) like the C++
//! base `Encoder` default. The filename helpers mirror the `Encoder`
//! statics in [`crate::encoder`].
use std::ffi::{c_char, c_int};
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
use crate::exportcodec::Codec;
use crate::exportformat::Format;
use crate::handle;
/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_count() -> c_int {
handle::guard_raw(|| Format::Count as c_int)
}
/// `oakcodec_encoding_format_name` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_name(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => super::string_out(&Format::get_name(f), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_extension` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_extension(
format: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => super::string_out(&Format::get_extension(f), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_video_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_video_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_video_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_video_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_format_audio_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_audio_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_audio_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_audio_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_format_subtitle_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
handle::guard_raw(|| match Format::from_i32(format) {
Some(f) => Format::get_subtitle_codecs(f).len() as c_int,
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_format_subtitle_codec_at`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_at(
format: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let f = match Format::from_i32(format) {
Some(f) => f,
None => return OAKCODEC_E_INVALID,
};
let list = Format::get_subtitle_codecs(f);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_codec_name` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_name(
codec: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => super::string_out(&Codec::get_codec_name(c), buf, buf_size),
None => OAKCODEC_E_INVALID,
})
}
/// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => Codec::is_codec_a_still_image(c) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
handle::guard_raw(|| match Codec::from_i32(codec) {
Some(c) => Codec::is_codec_lossless(c) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_pix_fmt_count`.
///
/// # CPP-PARITY
/// The C++ side instantiates the format's encoder and asks it for the codec's
/// pixel formats; the Rust table is empty (see [`Format::get_pixel_formats_for_codec`]),
/// so the count is 0 — the same as the C++ base `Encoder` default and the
/// C++ result for encoder-less codecs.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
Format::get_pixel_formats_for_codec(f, c).len() as c_int
})
}
/// `oakcodec_encoding_pix_fmt_at` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_at(
format: c_int,
codec: c_int,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
let list = Format::get_pixel_formats_for_codec(f, c);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
// Interim: the Rust list carries no names yet, so this arm is
// unreachable while the list is empty (C++ queries the FFmpeg bridge).
super::string_out(&list[index as usize].to_string(), buf, buf_size)
})
}
/// `oakcodec_encoding_pix_fmt_index` — index of `pix_fmt` in `codec`'s
/// supported pixel formats; 0 (the preferred format) for an invalid codec,
/// a NULL/empty `pix_fmt`, or when not found.
///
/// # CPP-PARITY
/// The C++ side searches the FFmpeg encoder's list. The Rust table is empty,
/// so every lookup falls back to 0 — the documented behavior for "absent".
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_index(
codec: c_int,
pix_fmt: *const c_char,
) -> c_int {
handle::guard_raw(|| {
if Codec::from_i32(codec).is_none() {
return 0;
}
match crate::ffi::c_str(pix_fmt) {
Some(s) if !s.is_empty() => {
// Interim: empty table (see module doc) -> preferred index 0.
let _ = s;
0
}
_ => 0,
}
})
}
/// `oakcodec_encoding_sample_format_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_sample_format_count(
format: c_int,
codec: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
Format::get_sample_formats_for_codec(f, c).len() as c_int
})
}
/// `oakcodec_encoding_sample_format_at` — an
/// `olive::core::SampleFormat::Format` value.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_sample_format_at(
format: c_int,
codec: c_int,
index: c_int,
) -> c_int {
handle::guard_raw(|| {
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
(Some(f), Some(c)) => (f, c),
_ => return OAKCODEC_E_INVALID,
};
let list = Format::get_sample_formats_for_codec(f, c);
if index < 0 || index as usize >= list.len() {
return OAKCODEC_E_NOT_FOUND;
}
list[index as usize] as c_int
})
}
/// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_filename_contains_digit_placeholder(
filename: *const c_char,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => crate::encoder::filename_contains_digit_placeholder(&f) as c_int,
None => 0,
})
}
/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_image_sequence_digit_count(
filename: *const c_char,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => crate::encoder::image_sequence_placeholder_digit_count(&f),
None => 0,
})
}
/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage).
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_filename_remove_digit_placeholder(
filename: *const c_char,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| match crate::ffi::c_str(filename) {
Some(f) => super::string_out(
&crate::encoder::filename_remove_digit_placeholder(&f),
buf,
buf_size,
),
None => OAKCODEC_E_INVALID,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap()
}
#[test]
fn format_metadata_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
// Count matches the 15-entry table (0..=14, Count = 15).
assert_eq!(unsafe { oakcodec_encoding_format_count() }, 15);
// Matroska (1): "Matroska Video" / "mkv".
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 15); // "Matroska Video" (14) + NUL
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("Matroska Video")
);
let rc = unsafe { oakcodec_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 4); // "mkv" + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mkv"));
// Truncation rule on a two-stage getter.
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 4) };
assert_eq!(rc, 15); // required size unchanged
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Mat"));
// Invalid format -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_format_name(-1, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_format_extension(15, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
); // Count is not a real format
assert_eq!(
unsafe { oakcodec_encoding_format_extension(99, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
}
#[test]
fn format_codec_lists_exports() {
let _g = crate::ffi::lock_tests();
// MPEG-4 video (2) carries H.264/H.264RGB/H.265.
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(2) }, 3);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, 0) },
1 // H.264
);
// WAV (7) has no video codecs but PCM (13) audio.
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(7) }, 0);
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(7) }, 1);
assert_eq!(
unsafe { oakcodec_encoding_format_audio_codec_at(7, 0) },
13 // PCM
);
// SRT (13): subtitle-only, with the SRT (17) codec.
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(13) }, 0);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_count(13) },
1
);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
17 // SRT
);
// Failure paths.
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_count(-1) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, -1) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_video_codec_at(2, 3) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_audio_codec_at(7, 1) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 1) },
OAKCODEC_E_NOT_FOUND
);
}
#[test]
fn codec_metadata_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
let rc = unsafe { oakcodec_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(rc, 6); // "H.264" (5) + NUL
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("H.264"));
assert_eq!(
unsafe { oakcodec_encoding_codec_name(-1, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
// Still images: PNG (5) yes, H.264 (1) no.
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(5) }, 1);
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(1) }, 0);
// Lossless: PCM (13) yes, AAC (12) no.
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(13) }, 1);
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(12) }, 0);
// Invalid codec -> 0 (not an error) for both flags.
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(99) }, 0);
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(99) }, 0);
}
#[test]
fn pixel_and_sample_format_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 64];
// Bad arguments -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_count(-1, 1) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_count(2, 99) },
OAKCODEC_E_INVALID
);
// Interim: the Rust pixel-format table is empty, so the count is 0
// and any index is E_NOT_FOUND (see the module doc).
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_count(2, 1) }, 0);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 64) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 64) },
OAKCODEC_E_NOT_FOUND
);
// pix_fmt_index: absent/empty/NULL/invalid codec all yield 0.
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(1, cstr("yuv420p").as_ptr()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(1, std::ptr::null()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_pix_fmt_index(99, cstr("yuv420p").as_ptr()) },
0
);
// PCM (13) in WAV (7) exposes its native sample formats.
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(7, 13) }, 6);
// f32 packed = 10 (oakcore SampleFormat values match the C++).
assert_eq!(unsafe { oakcodec_encoding_sample_format_at(7, 13, 4) }, 10);
// Out-of-range index -> E_NOT_FOUND; bad args -> E_INVALID.
assert_eq!(
unsafe { oakcodec_encoding_sample_format_at(7, 13, 6) },
OAKCODEC_E_NOT_FOUND
);
assert_eq!(
unsafe { oakcodec_encoding_sample_format_at(-1, 13, 0) },
OAKCODEC_E_INVALID
);
// Non-PCM codecs query the bridge on the C++ side; empty here.
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(2, 12) }, 0);
}
#[test]
fn filename_helper_exports() {
let _g = crate::ffi::lock_tests();
let mut buf = [0i8; 128];
assert_eq!(
unsafe {
oakcodec_encoding_filename_contains_digit_placeholder(
cstr("/tmp/out_[#####].png").as_ptr(),
)
},
1
);
assert_eq!(
unsafe {
oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out.png").as_ptr())
},
0
);
assert_eq!(
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(std::ptr::null()) },
0
);
assert_eq!(
unsafe {
oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr())
},
5
);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out.png").as_ptr()) },
0
);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) },
0
);
let rc = unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
cstr("/tmp/out_[#####].png").as_ptr(),
buf.as_mut_ptr(),
128,
)
};
assert_eq!(rc, 13); // "/tmp/out.png" (12) + NUL
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("/tmp/out.png")
);
assert_eq!(
unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
std::ptr::null(),
buf.as_mut_ptr(),
128,
)
},
OAKCODEC_E_INVALID
);
}
}