use std::slice;
use ffmpeg_next::frame;
use mediadecode::PixelFormat;
use crate::{
boundary,
error::{Error, Result},
};
pub(crate) fn alloc_av_video_frame() -> Result<frame::Video> {
let f = frame::Video::empty();
if unsafe { f.as_ptr() }.is_null() {
return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
}
Ok(f)
}
pub(crate) fn alloc_av_audio_frame() -> Result<frame::Audio> {
let f = frame::Audio::empty();
if unsafe { f.as_ptr() }.is_null() {
return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
}
Ok(f)
}
pub struct Frame {
inner: frame::Video,
}
impl core::fmt::Debug for Frame {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Frame")
.field("width", &self.width())
.field("height", &self.height())
.field("pix_fmt", &self.pix_fmt())
.field("planes", &self.planes())
.field("pts", &self.pts())
.finish()
}
}
impl Frame {
pub fn empty() -> Result<Self> {
let inner = frame::Video::empty();
if unsafe { inner.as_ptr() }.is_null() {
return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
}
Ok(Self { inner })
}
pub fn width(&self) -> u32 {
unsafe { (*self.inner.as_ptr()).width as u32 }
}
pub fn height(&self) -> u32 {
unsafe { (*self.inner.as_ptr()).height as u32 }
}
pub fn pix_fmt(&self) -> PixelFormat {
boundary::from_av_pixel_format(unsafe { (*self.inner.as_ptr()).format })
}
pub fn pts(&self) -> Option<i64> {
self.inner.pts()
}
pub fn planes(&self) -> usize {
unsafe {
let linesize = &(*self.inner.as_ptr()).linesize;
for (i, ls) in linesize.iter().enumerate() {
if *ls == 0 {
return i;
}
}
linesize.len()
}
}
pub fn stride(&self, plane: usize) -> usize {
let n = self.planes();
assert!(
plane < n,
"stride: plane {plane} out of bounds (planes={n})"
);
let linesize: i32 = unsafe { (*self.inner.as_ptr()).linesize[plane] };
assert!(
linesize > 0,
"stride: non-positive linesize {linesize} for plane {plane} \
(negative linesize means vertically-flipped — not supported)"
);
linesize as usize
}
pub fn try_stride(&self, plane: usize) -> Option<usize> {
if plane >= self.planes() {
return None;
}
let linesize: i32 = unsafe { (*self.inner.as_ptr()).linesize[plane] };
if linesize <= 0 {
return None;
}
Some(linesize as usize)
}
pub fn row_bytes(&self, plane: usize) -> Option<usize> {
if plane >= self.planes() {
return None;
}
plane_row_bytes_for(self.pix_fmt(), plane, self.width() as usize)
}
pub fn row(&self, plane: usize, y: usize) -> Option<&[u8]> {
let info = self.plane_info(plane)?;
if y >= info.plane_h {
return None;
}
let offset = y * info.stride;
unsafe {
let row_ptr = info.plane_ptr.add(offset);
Some(slice::from_raw_parts(row_ptr, info.row_bytes))
}
}
pub fn rows(&self, plane: usize) -> Option<impl Iterator<Item = &[u8]> + '_> {
let info = self.plane_info(plane)?;
Some((0..info.plane_h).map(move |y| {
let offset = y * info.stride;
unsafe { slice::from_raw_parts(info.plane_ptr.add(offset), info.row_bytes) }
}))
}
pub fn as_ptr(&self, plane: usize) -> Option<*const u8> {
self.plane_info(plane).map(|info| info.plane_ptr)
}
fn plane_info(&self, plane: usize) -> Option<PlaneInfo> {
if plane >= self.planes() {
return None;
}
let (stride_int, height_int, plane_ptr) = unsafe {
let raw = self.inner.as_ptr();
((*raw).linesize[plane], (*raw).height, (*raw).data[plane])
};
if stride_int <= 0 || height_int <= 0 || plane_ptr.is_null() {
return None;
}
let stride = stride_int as usize;
let plane_h = plane_height_for(self.pix_fmt(), plane, height_int as usize)?;
let row_bytes = plane_row_bytes_for(self.pix_fmt(), plane, self.width() as usize)?;
if row_bytes > stride {
return None;
}
let plane_size = stride.checked_mul(plane_h)?;
if plane_size > isize::MAX as usize {
return None;
}
Some(PlaneInfo {
plane_ptr,
stride,
plane_h,
row_bytes,
})
}
pub(crate) fn as_inner_mut(&mut self) -> &mut frame::Video {
&mut self.inner
}
}
#[derive(Clone, Copy)]
struct PlaneInfo {
plane_ptr: *const u8,
stride: usize,
plane_h: usize,
row_bytes: usize,
}
pub(crate) fn is_supported_cpu_pix_fmt(pix_fmt: PixelFormat) -> bool {
matches!(
pix_fmt,
PixelFormat::Nv12
| PixelFormat::Nv21
| PixelFormat::Nv16
| PixelFormat::Nv24
| PixelFormat::P010Le
| PixelFormat::P012Le
| PixelFormat::P016Le
| PixelFormat::P210Le
| PixelFormat::P212Le
| PixelFormat::P216Le
| PixelFormat::P410Le
| PixelFormat::P412Le
| PixelFormat::P416Le
| PixelFormat::Yuv420p
| PixelFormat::Yuv422p
| PixelFormat::Yuv444p
| PixelFormat::Yuv420p10Le
| PixelFormat::Yuv420p12Le
| PixelFormat::Yuv420p16Le
| PixelFormat::Yuv422p10Le
| PixelFormat::Yuv422p12Le
| PixelFormat::Yuv422p16Le
| PixelFormat::Yuv444p10Le
| PixelFormat::Yuv444p12Le
| PixelFormat::Yuv444p16Le
| PixelFormat::Rgb24
| PixelFormat::Bgr24
| PixelFormat::Rgba
| PixelFormat::Bgra
| PixelFormat::Argb
| PixelFormat::Abgr
| PixelFormat::Gray8
| PixelFormat::Gray16Le
)
}
pub(crate) fn plane_row_bytes_for(
pix_fmt: PixelFormat,
plane: usize,
frame_width: usize,
) -> Option<usize> {
match pix_fmt {
PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Nv16 => match plane {
0 => Some(frame_width),
1 => Some(frame_width.div_ceil(2).checked_mul(2)?),
_ => None,
},
PixelFormat::Nv24 => match plane {
0 => Some(frame_width),
1 => Some(frame_width.checked_mul(2)?),
_ => None,
},
PixelFormat::P010Le
| PixelFormat::P012Le
| PixelFormat::P016Le
| PixelFormat::P210Le
| PixelFormat::P212Le
| PixelFormat::P216Le => match plane {
0 => Some(frame_width.checked_mul(2)?),
1 => Some(frame_width.div_ceil(2).checked_mul(4)?),
_ => None,
},
PixelFormat::P410Le | PixelFormat::P412Le | PixelFormat::P416Le => match plane {
0 => Some(frame_width.checked_mul(2)?),
1 => Some(frame_width.checked_mul(4)?),
_ => None,
},
PixelFormat::Yuv420p => match plane {
0 => Some(frame_width),
1 | 2 => Some(frame_width.div_ceil(2)),
_ => None,
},
PixelFormat::Yuv422p => match plane {
0 => Some(frame_width),
1 | 2 => Some(frame_width.div_ceil(2)),
_ => None,
},
PixelFormat::Yuv444p => match plane {
0..=2 => Some(frame_width),
_ => None,
},
PixelFormat::Yuv420p10Le | PixelFormat::Yuv420p12Le | PixelFormat::Yuv420p16Le => match plane {
0 => Some(frame_width.checked_mul(2)?),
1 | 2 => Some(frame_width.div_ceil(2).checked_mul(2)?),
_ => None,
},
PixelFormat::Yuv422p10Le | PixelFormat::Yuv422p12Le | PixelFormat::Yuv422p16Le => match plane {
0 => Some(frame_width.checked_mul(2)?),
1 | 2 => Some(frame_width.div_ceil(2).checked_mul(2)?),
_ => None,
},
PixelFormat::Yuv444p10Le | PixelFormat::Yuv444p12Le | PixelFormat::Yuv444p16Le => match plane {
0..=2 => Some(frame_width.checked_mul(2)?),
_ => None,
},
PixelFormat::Rgb24 | PixelFormat::Bgr24 => match plane {
0 => Some(frame_width.checked_mul(3)?),
_ => None,
},
PixelFormat::Rgba | PixelFormat::Bgra | PixelFormat::Argb | PixelFormat::Abgr => match plane {
0 => Some(frame_width.checked_mul(4)?),
_ => None,
},
PixelFormat::Gray8 => match plane {
0 => Some(frame_width),
_ => None,
},
PixelFormat::Gray16Le => match plane {
0 => Some(frame_width.checked_mul(2)?),
_ => None,
},
_ => None,
}
}
pub(crate) fn plane_height_for(
pix_fmt: PixelFormat,
plane: usize,
frame_height: usize,
) -> Option<usize> {
match pix_fmt {
PixelFormat::Nv12
| PixelFormat::Nv21
| PixelFormat::P010Le
| PixelFormat::P012Le
| PixelFormat::P016Le => match plane {
0 => Some(frame_height),
1 => Some(frame_height.div_ceil(2)),
_ => None,
},
PixelFormat::Nv16
| PixelFormat::Nv24
| PixelFormat::P210Le
| PixelFormat::P212Le
| PixelFormat::P216Le
| PixelFormat::P410Le
| PixelFormat::P412Le
| PixelFormat::P416Le => match plane {
0 | 1 => Some(frame_height),
_ => None,
},
PixelFormat::Yuv420p
| PixelFormat::Yuv420p10Le
| PixelFormat::Yuv420p12Le
| PixelFormat::Yuv420p16Le => match plane {
0 => Some(frame_height),
1 | 2 => Some(frame_height.div_ceil(2)),
_ => None,
},
PixelFormat::Yuv422p
| PixelFormat::Yuv422p10Le
| PixelFormat::Yuv422p12Le
| PixelFormat::Yuv422p16Le
| PixelFormat::Yuv444p
| PixelFormat::Yuv444p10Le
| PixelFormat::Yuv444p12Le
| PixelFormat::Yuv444p16Le => match plane {
0..=2 => Some(frame_height),
_ => None,
},
PixelFormat::Rgb24
| PixelFormat::Bgr24
| PixelFormat::Rgba
| PixelFormat::Bgra
| PixelFormat::Argb
| PixelFormat::Abgr
| PixelFormat::Gray8
| PixelFormat::Gray16Le => match plane {
0 => Some(frame_height),
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use ffmpeg_next::ffi::AVPixelFormat;
#[test]
fn empty_frame_has_zero_dimensions_and_no_pts() {
let f = Frame::empty().expect("alloc");
assert_eq!(f.width(), 0);
assert_eq!(f.height(), 0);
assert_eq!(f.pts(), None);
assert!(matches!(f.pix_fmt(), PixelFormat::Unknown(_)));
assert_eq!(f.planes(), 0);
}
#[test]
fn row_returns_none_for_unknown_format() {
let f = Frame::empty().expect("alloc");
assert!(f.row(0, 0).is_none());
assert!(f.rows(0).is_none());
assert!(f.row_bytes(0).is_none());
}
#[test]
fn row_returns_none_for_negative_linesize() {
let mut f = Frame::empty().expect("alloc");
unsafe {
let raw = f.inner.as_mut_ptr();
(*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*raw).width = 1920;
(*raw).height = 1080;
(*raw).linesize[0] = -1920; (*raw).linesize[1] = -1920;
}
assert!(f.row(0, 0).is_none());
assert!(f.row(1, 0).is_none());
assert!(f.rows(0).is_none());
assert!(
f.as_ptr(0).is_none(),
"as_ptr must share row()/rows() validation — a negative-stride \
frame must not leak a forward-readable plane pointer"
);
assert!(f.as_ptr(1).is_none());
}
#[test]
fn row_returns_none_for_non_positive_height() {
let mut f = Frame::empty().expect("alloc");
unsafe {
let raw = f.inner.as_mut_ptr();
(*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*raw).width = 1920;
(*raw).height = 0;
(*raw).linesize[0] = 1920;
(*raw).linesize[1] = 1920;
}
assert!(f.row(0, 0).is_none());
}
#[test]
fn row_clips_to_visible_width_not_stride() {
use std::alloc::{Layout, alloc, dealloc};
let width = 64usize;
let height = 4usize;
let stride = 80usize;
let plane_size = stride * height;
let layout = Layout::from_size_align(plane_size, 32).unwrap();
let buf = unsafe { alloc(layout) };
assert!(!buf.is_null());
for y in 0..height {
let row = unsafe { buf.add(y * stride) };
for x in 0..width {
unsafe { *row.add(x) = 0xAA };
}
for x in width..stride {
unsafe { *row.add(x) = 0xFF };
}
}
let mut f = Frame::empty().expect("alloc");
unsafe {
let raw = f.inner.as_mut_ptr();
(*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*raw).width = width as i32;
(*raw).height = height as i32;
(*raw).linesize[0] = stride as i32;
(*raw).data[0] = buf;
}
assert_eq!(f.row_bytes(0), Some(width));
assert_eq!(f.stride(0), stride);
let row0 = f.row(0, 0).expect("row 0");
assert_eq!(
row0.len(),
width,
"safe row must be clipped to visible width"
);
assert!(
row0.iter().all(|&b| b == 0xAA),
"row must not include padding sentinel 0xFF"
);
let collected: Vec<&[u8]> = f.rows(0).expect("rows iterator").collect();
assert_eq!(collected.len(), height);
for r in &collected {
assert_eq!(r.len(), width);
assert!(r.iter().all(|&b| b == 0xAA));
}
assert_eq!(
f.as_ptr(0),
Some(buf as *const u8),
"as_ptr must surface the plane base for a valid forward-stride frame"
);
assert!(f.row(0, height).is_none());
unsafe {
(*f.inner.as_mut_ptr()).data[0] = std::ptr::null_mut();
dealloc(buf, layout);
}
}
#[test]
#[should_panic(expected = "non-positive linesize")]
fn stride_panics_on_negative_linesize() {
let mut f = Frame::empty().expect("alloc");
unsafe {
let raw = f.inner.as_mut_ptr();
(*raw).linesize[0] = -1920;
}
let _ = f.stride(0);
}
#[test]
fn frame_is_send() {
fn check<T: Send>() {}
check::<Frame>();
}
#[test]
fn plane_height_table_covers_supported_formats() {
assert_eq!(plane_height_for(PixelFormat::Nv12, 0, 1080), Some(1080));
assert_eq!(plane_height_for(PixelFormat::Nv12, 1, 1080), Some(540));
assert_eq!(plane_height_for(PixelFormat::Nv12, 1, 1081), Some(541));
assert_eq!(plane_height_for(PixelFormat::P010Le, 1, 1080), Some(540));
assert_eq!(plane_height_for(PixelFormat::Nv16, 1, 1080), Some(1080));
assert_eq!(plane_height_for(PixelFormat::Nv24, 1, 1080), Some(1080));
assert_eq!(plane_height_for(PixelFormat::P416Le, 1, 1080), Some(1080));
assert_eq!(plane_height_for(PixelFormat::Unknown(0), 0, 1080), None);
assert_eq!(plane_height_for(PixelFormat::Nv12, 2, 1080), None);
}
#[test]
fn plane_row_bytes_rounds_up_chroma_for_odd_widths() {
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1921), Some(1922));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv21, 1, 1921), Some(1922));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv16, 1, 1921), Some(1922));
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P012Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P016Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P210Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P212Le, 1, 1921),
Some(3844)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P216Le, 1, 1921),
Some(3844)
);
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 0, 1921), Some(1921));
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 0, 1921),
Some(3842)
);
assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 1, 1921), Some(3842));
assert_eq!(
plane_row_bytes_for(PixelFormat::P410Le, 1, 1921),
Some(7684)
);
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1920), Some(1920));
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 1, 1920),
Some(3840)
);
}
#[test]
fn plane_row_bytes_table_covers_supported_formats() {
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 0, 1920), Some(1920));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1920), Some(1920));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv21, 1, 1920), Some(1920));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv16, 1, 1920), Some(1920));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 0, 1920), Some(1920));
assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 1, 1920), Some(3840));
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 0, 1920),
Some(3840)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P010Le, 1, 1920),
Some(3840)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P210Le, 1, 1920),
Some(3840)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P410Le, 0, 1920),
Some(3840)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P410Le, 1, 1920),
Some(7680)
);
assert_eq!(
plane_row_bytes_for(PixelFormat::P416Le, 1, 1920),
Some(7680)
);
assert_eq!(plane_row_bytes_for(PixelFormat::Unknown(0), 0, 1920), None);
assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 2, 1920), None);
}
#[test]
fn is_supported_cpu_pix_fmt_agrees_with_row_byte_table() {
let supported = [
PixelFormat::Nv12,
PixelFormat::Nv21,
PixelFormat::Nv16,
PixelFormat::Nv24,
PixelFormat::P010Le,
PixelFormat::P010Le,
PixelFormat::P012Le,
PixelFormat::P016Le,
PixelFormat::P210Le,
PixelFormat::P212Le,
PixelFormat::P216Le,
PixelFormat::P410Le,
PixelFormat::P412Le,
PixelFormat::P416Le,
];
for fmt in supported {
assert!(
is_supported_cpu_pix_fmt(fmt),
"is_supported_cpu_pix_fmt rejected pix_fmt {fmt:?}, but the row-byte \
table accepts it — the two are out of sync"
);
assert!(
plane_row_bytes_for(fmt, 0, 1920).is_some(),
"plane_row_bytes_for rejected pix_fmt {fmt:?}, but \
is_supported_cpu_pix_fmt accepts it — out of sync"
);
assert!(
plane_height_for(fmt, 0, 1080).is_some(),
"plane_height_for rejected pix_fmt {fmt:?} — out of sync"
);
}
}
#[test]
fn is_supported_cpu_pix_fmt_rejects_common_unsupported_formats() {
use ffmpeg_next::ffi::AVPixelFormat;
assert!(!is_supported_cpu_pix_fmt(PixelFormat::Unknown(0)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
AVPixelFormat::AV_PIX_FMT_VAAPI as i32
)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
AVPixelFormat::AV_PIX_FMT_CUDA as i32
)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
AVPixelFormat::AV_PIX_FMT_D3D11 as i32
)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
AVPixelFormat::AV_PIX_FMT_YUVJ420P as i32
)));
assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
99_999_999
)));
}
}