#![allow(unsafe_code)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]
use ff_format::{AudioFrame, PixelFormat, VideoFrame};
pub(crate) fn audio_frame_to_f32(frame: &AudioFrame) -> Vec<f32> {
frame.as_f32().map(<[f32]>::to_vec).unwrap_or_default()
}
pub(crate) struct SwsRgbaConverter {
ctx: *mut ff_sys::SwsContext,
cache_key: Option<(u32, u32, PixelFormat)>,
}
unsafe impl Send for SwsRgbaConverter {}
impl SwsRgbaConverter {
pub(crate) fn new() -> Self {
Self {
ctx: std::ptr::null_mut(),
cache_key: None,
}
}
pub(crate) fn convert(&mut self, frame: &VideoFrame, dst: &mut Vec<u8>) -> bool {
let w = frame.width();
let h = frame.height();
if w == 0 || h == 0 {
return false;
}
let fmt = frame.format();
let key = (w, h, fmt);
if self.cache_key.as_ref() != Some(&key) {
unsafe { ff_sys::swscale::free_context(self.ctx) };
self.ctx = std::ptr::null_mut();
let src_fmt = pixel_format_to_av(fmt);
let dst_fmt = ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA;
match unsafe {
ff_sys::swscale::get_context(
w as i32,
h as i32,
src_fmt,
w as i32,
h as i32,
dst_fmt,
ff_sys::swscale::scale_flags::FAST_BILINEAR,
)
} {
Ok(ctx) => self.ctx = ctx,
Err(code) => {
log::warn!(
"sws_getContext failed format={fmt:?} width={w} height={h} code={code}"
);
return false;
}
}
self.cache_key = Some(key);
}
let rgba_stride = (w * 4) as usize;
let total = rgba_stride * h as usize;
dst.resize(total, 0u8);
let n = frame.num_planes().min(4);
let mut src_ptrs: [*const u8; 4] = [std::ptr::null(); 4];
let mut src_strides: [i32; 4] = [0i32; 4];
for i in 0..n {
if let (Some(plane), Some(stride)) = (frame.plane(i), frame.stride(i)) {
src_ptrs[i] = plane.as_ptr();
src_strides[i] = stride as i32;
}
}
let dst_ptr = dst.as_mut_ptr();
let dst_stride_val = rgba_stride as i32;
let mut dst_ptrs: [*mut u8; 4] = [
dst_ptr,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
];
let dst_strides: [i32; 4] = [dst_stride_val, 0, 0, 0];
let result = unsafe {
ff_sys::swscale::scale(
self.ctx,
src_ptrs.as_ptr(),
src_strides.as_ptr(),
0,
h as i32,
dst_ptrs.as_mut_ptr().cast::<*mut u8>(),
dst_strides.as_ptr(),
)
};
match result {
Ok(_) => true,
Err(code) => {
log::warn!("sws_scale failed width={w} height={h} code={code}");
false
}
}
}
}
impl Drop for SwsRgbaConverter {
fn drop(&mut self) {
unsafe { ff_sys::swscale::free_context(self.ctx) };
}
}
fn pixel_format_to_av(format: PixelFormat) -> ff_sys::AVPixelFormat {
match format {
PixelFormat::Yuv420p => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P,
PixelFormat::Yuv422p => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P,
PixelFormat::Yuv444p => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P,
PixelFormat::Rgb24 => ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24,
PixelFormat::Bgr24 => ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24,
PixelFormat::Rgba => ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA,
PixelFormat::Bgra => ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA,
PixelFormat::Gray8 => ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8,
PixelFormat::Nv12 => ff_sys::AVPixelFormat_AV_PIX_FMT_NV12,
PixelFormat::Nv21 => ff_sys::AVPixelFormat_AV_PIX_FMT_NV21,
PixelFormat::Yuv420p10le => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE,
PixelFormat::Yuv422p10le => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE,
PixelFormat::Yuv444p10le => ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE,
PixelFormat::Yuva444p10le => ff_sys::AVPixelFormat_AV_PIX_FMT_YUVA444P10LE,
PixelFormat::P010le => ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE,
PixelFormat::Gbrpf32le => ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE,
_ => {
log::warn!(
"pixel_format has no AV mapping, falling back to Yuv420p \
format={format:?} fallback=AV_PIX_FMT_YUV420P"
);
ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ff_format::{AudioFrame, SampleFormat, Timestamp};
#[test]
fn audio_frame_to_f32_should_extract_packed_f32_samples() {
let values: Vec<f32> = vec![1.0, -1.0, 0.5, -0.5];
let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
let frame = AudioFrame::new(
vec![bytes],
2, 2, 48_000,
SampleFormat::F32,
Timestamp::default(),
)
.unwrap();
let out = audio_frame_to_f32(&frame);
assert_eq!(out.len(), 4);
assert!(
(out[0] - 1.0).abs() < f32::EPSILON,
"first sample mismatch: expected 1.0 got {}",
out[0]
);
assert!(
(out[1] - (-1.0)).abs() < f32::EPSILON,
"second sample mismatch: expected -1.0 got {}",
out[1]
);
assert!(
(out[2] - 0.5).abs() < f32::EPSILON,
"third sample mismatch: expected 0.5 got {}",
out[2]
);
assert!(
(out[3] - (-0.5)).abs() < f32::EPSILON,
"fourth sample mismatch: expected -0.5 got {}",
out[3]
);
}
#[test]
fn audio_frame_to_f32_should_return_empty_for_non_f32_format() {
let bytes = vec![0u8; 8];
let frame = AudioFrame::new(
vec![bytes],
2,
2,
48_000,
SampleFormat::I16,
Timestamp::default(),
)
.unwrap();
let out = audio_frame_to_f32(&frame);
assert!(
out.is_empty(),
"non-F32 frame should return an empty Vec, got {} samples",
out.len()
);
}
}