#![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: Option<ff_sys::ScaleContext>,
cache_key: Option<(u32, u32, PixelFormat, u32, u32)>,
}
impl SwsRgbaConverter {
pub(crate) fn new() -> Self {
Self {
ctx: None,
cache_key: None,
}
}
pub(crate) fn convert(&mut self, frame: &VideoFrame, dst: &mut Vec<u8>) -> bool {
self.convert_to(frame, dst, frame.width(), frame.height())
}
pub(crate) fn convert_to(
&mut self,
frame: &VideoFrame,
dst: &mut Vec<u8>,
dst_w: u32,
dst_h: u32,
) -> bool {
let src_w = frame.width();
let src_h = frame.height();
if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
return false;
}
let fmt = frame.format();
let key = (src_w, src_h, fmt, dst_w, dst_h);
if self.cache_key.as_ref() != Some(&key) {
let src_fmt = pixel_format_to_av(fmt);
let dst_fmt = ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA;
match ff_sys::ScaleContext::new(
src_w as i32,
src_h as i32,
src_fmt,
dst_w as i32,
dst_h as i32,
dst_fmt,
ff_sys::swscale::scale_flags::FAST_BILINEAR,
) {
Ok(ctx) => self.ctx = Some(ctx),
Err(e) => {
log::warn!(
"sws_getContext failed format={fmt:?} src={src_w}x{src_h} \
dst={dst_w}x{dst_h} code={code}",
code = e.code()
);
return false;
}
}
self.cache_key = Some(key);
}
let rgba_stride = (dst_w * 4) as usize;
let total = rgba_stride * dst_h as usize;
dst.resize(total, 0u8);
let n = frame.num_planes().min(4);
let mut src_planes: Vec<&[u8]> = Vec::with_capacity(n);
let mut src_strides: Vec<i32> = Vec::with_capacity(n);
for i in 0..n {
if let (Some(plane), Some(stride)) = (frame.plane(i), frame.stride(i)) {
src_planes.push(plane);
src_strides.push(stride as i32);
}
}
let dst_strides: [i32; 1] = [rgba_stride as i32];
let mut dst_slices: [&mut [u8]; 1] = [dst.as_mut_slice()];
let Some(ctx) = self.ctx.as_mut() else {
log::warn!("sws_scale skipped: scaling context not initialized");
return false;
};
let result = unsafe {
ctx.scale_slices(
&src_planes,
&src_strides,
src_h as i32,
&mut dst_slices,
&dst_strides,
)
};
match result {
Ok(_) => true,
Err(e) => {
log::warn!(
"sws_scale failed src={src_w}x{src_h} dst={dst_w}x{dst_h} code={}",
e.code()
);
false
}
}
}
}
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()
);
}
#[test]
fn convert_to_should_rebuild_scaler_when_source_geometry_changes() {
let mut converter = SwsRgbaConverter::new();
let mut dst = Vec::new();
let frame_a = VideoFrame::from_rgba(8, 8, vec![120u8; 8 * 8 * 4]).unwrap();
assert!(converter.convert_to(&frame_a, &mut dst, 16, 16));
assert_eq!(dst.len(), 16 * 16 * 4);
let frame_b = VideoFrame::from_rgba(4, 4, vec![200u8; 4 * 4 * 4]).unwrap();
assert!(converter.convert_to(&frame_b, &mut dst, 16, 16));
assert_eq!(dst.len(), 16 * 16 * 4);
let frame_c = VideoFrame::from_rgba(4, 4, vec![50u8; 4 * 4 * 4]).unwrap();
assert!(converter.convert_to(&frame_c, &mut dst, 16, 16));
assert_eq!(dst.len(), 16 * 16 * 4);
}
}