use std::sync::Arc;
use std::time::Duration;
use crate::color::ColorSpace;
use crate::error::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PixelFormat {
I420,
I422,
I444,
Nv12,
P010,
Rgba8,
Bgra8,
}
impl PixelFormat {
pub fn plane_count(self) -> usize {
match self {
PixelFormat::I420 | PixelFormat::I422 | PixelFormat::I444 => 3,
PixelFormat::Nv12 | PixelFormat::P010 => 2,
PixelFormat::Rgba8 | PixelFormat::Bgra8 => 1,
}
}
pub fn bit_depth(self) -> u32 {
match self {
PixelFormat::P010 => 10,
_ => 8,
}
}
pub fn bytes_per_sample(self) -> usize {
match self {
PixelFormat::P010 => 2,
_ => 1,
}
}
pub fn is_yuv(self) -> bool {
!matches!(self, PixelFormat::Rgba8 | PixelFormat::Bgra8)
}
pub fn chroma_shift(self) -> (u32, u32) {
match self {
PixelFormat::I420 | PixelFormat::Nv12 | PixelFormat::P010 => (1, 1),
PixelFormat::I422 => (1, 0),
_ => (0, 0),
}
}
pub fn plane_dimensions(self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
if plane >= self.plane_count() {
return None;
}
if plane == 0 {
return Some((width, height));
}
let (h_shift, v_shift) = self.chroma_shift();
let chroma_width = width.div_ceil(1 << h_shift);
let chroma_height = height.div_ceil(1 << v_shift);
Some((chroma_width, chroma_height))
}
pub fn samples_per_pixel(self, plane: usize) -> usize {
match (self, plane) {
(PixelFormat::Rgba8 | PixelFormat::Bgra8, _) => 4,
(PixelFormat::Nv12 | PixelFormat::P010, 1) => 2,
_ => 1,
}
}
pub fn tight_stride(self, plane: usize, width: u32) -> usize {
let (plane_width, _) = self.plane_dimensions(plane, width, 1).unwrap_or((width, 1));
plane_width as usize * self.samples_per_pixel(plane) * self.bytes_per_sample()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Plane {
pub offset: usize,
pub stride: usize,
}
#[derive(Clone, Debug)]
pub struct Frame {
width: u32,
height: u32,
format: PixelFormat,
color: ColorSpace,
pts: Duration,
planes: Vec<Plane>,
data: Arc<Vec<u8>>,
}
impl Frame {
pub fn packed(
width: u32,
height: u32,
format: PixelFormat,
color: ColorSpace,
pts: Duration,
data: Vec<u8>,
) -> Result<Self> {
let (planes, needed) = packed_layout(format, width, height)?;
if data.len() < needed {
return Err(Error::BadFrame {
reason: format!(
"{width}×{height} {format:?} needs {needed} bytes, got {}",
data.len()
),
});
}
Ok(Frame {
width,
height,
format,
color,
pts,
planes,
data: Arc::new(data),
})
}
pub fn with_planes(
width: u32,
height: u32,
format: PixelFormat,
color: ColorSpace,
pts: Duration,
planes: Vec<Plane>,
data: Vec<u8>,
) -> Result<Self> {
if width == 0 || height == 0 {
return Err(Error::BadFrame {
reason: format!("a {width}×{height} picture has no pixels"),
});
}
if planes.len() != format.plane_count() {
return Err(Error::BadFrame {
reason: format!(
"{format:?} has {} planes, {} given",
format.plane_count(),
planes.len()
),
});
}
for (index, plane) in planes.iter().enumerate() {
let (plane_width, plane_height) = format
.plane_dimensions(index, width, height)
.expect("plane count was checked");
let row_bytes =
plane_width as usize * format.samples_per_pixel(index) * format.bytes_per_sample();
if plane.stride < row_bytes {
return Err(Error::BadFrame {
reason: format!(
"plane {index}: stride {} is shorter than a {row_bytes}-byte row",
plane.stride
),
});
}
let span = plane
.stride
.checked_mul(plane_height.saturating_sub(1) as usize)
.and_then(|full| full.checked_add(row_bytes))
.and_then(|span| span.checked_add(plane.offset))
.ok_or_else(|| Error::BadFrame {
reason: format!("plane {index}: the layout overflows a usize"),
})?;
if span > data.len() {
return Err(Error::BadFrame {
reason: format!(
"plane {index} runs to byte {span} of a {}-byte buffer",
data.len()
),
});
}
}
Ok(Frame {
width,
height,
format,
color,
pts,
planes,
data: Arc::new(data),
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn format(&self) -> PixelFormat {
self.format
}
pub fn color(&self) -> ColorSpace {
self.color
}
pub fn pts(&self) -> Duration {
self.pts
}
pub fn with_pts(&self, pts: Duration) -> Self {
Frame {
pts,
data: Arc::clone(&self.data),
planes: self.planes.clone(),
..*self
}
}
pub fn planes(&self) -> &[Plane] {
&self.planes
}
pub fn plane_data(&self, plane: usize) -> Option<&[u8]> {
let descriptor = self.planes.get(plane)?;
let (plane_width, plane_height) =
self.format
.plane_dimensions(plane, self.width, self.height)?;
let row_bytes = plane_width as usize
* self.format.samples_per_pixel(plane)
* self.format.bytes_per_sample();
let span = descriptor.stride * (plane_height as usize - 1) + row_bytes;
self.data.get(descriptor.offset..descriptor.offset + span)
}
pub fn row(&self, plane: usize, row: u32) -> Option<&[u8]> {
let descriptor = self.planes.get(plane)?;
let (plane_width, plane_height) =
self.format
.plane_dimensions(plane, self.width, self.height)?;
if row >= plane_height {
return None;
}
let row_bytes = plane_width as usize
* self.format.samples_per_pixel(plane)
* self.format.bytes_per_sample();
let start = descriptor.offset + descriptor.stride * row as usize;
self.data.get(start..start + row_bytes)
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn byte_len(&self) -> usize {
self.data.len()
}
}
fn packed_layout(format: PixelFormat, width: u32, height: u32) -> Result<(Vec<Plane>, usize)> {
if width == 0 || height == 0 {
return Err(Error::BadFrame {
reason: format!("a {width}×{height} picture has no pixels"),
});
}
let mut planes = Vec::with_capacity(format.plane_count());
let mut offset = 0_usize;
for index in 0..format.plane_count() {
let (plane_width, plane_height) = format
.plane_dimensions(index, width, height)
.expect("index is below the plane count");
let stride =
plane_width as usize * format.samples_per_pixel(index) * format.bytes_per_sample();
planes.push(Plane { offset, stride });
offset = stride
.checked_mul(plane_height as usize)
.and_then(|size| size.checked_add(offset))
.ok_or_else(|| Error::BadFrame {
reason: format!("{width}×{height} {format:?} does not fit in memory"),
})?;
}
Ok((planes, offset))
}
pub fn packed_size(format: PixelFormat, width: u32, height: u32) -> Result<usize> {
packed_layout(format, width, height).map(|(_, size)| size)
}
#[derive(Debug, Default)]
pub struct FramePool {
buffers: Vec<Vec<u8>>,
capacity: usize,
reused: u64,
allocated: u64,
}
impl FramePool {
pub fn new(capacity: usize) -> Self {
FramePool {
buffers: Vec::with_capacity(capacity),
capacity,
reused: 0,
allocated: 0,
}
}
pub fn take(&mut self, size: usize) -> Vec<u8> {
if let Some(index) = self
.buffers
.iter()
.position(|buffer| buffer.capacity() >= size)
{
let mut buffer = self.buffers.swap_remove(index);
buffer.clear();
buffer.resize(size, 0);
self.reused += 1;
return buffer;
}
self.allocated += 1;
vec![0; size]
}
pub fn give(&mut self, buffer: Vec<u8>) {
if self.buffers.len() < self.capacity {
self.buffers.push(buffer);
}
}
pub fn reclaim(&mut self, frame: Frame) {
if let Ok(buffer) = Arc::try_unwrap(frame.data) {
self.give(buffer);
}
}
pub fn statistics(&self) -> (u64, u64) {
(self.reused, self.allocated)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn frame_of(format: PixelFormat, width: u32, height: u32) -> Frame {
let size = packed_size(format, width, height).unwrap();
Frame::packed(
width,
height,
format,
ColorSpace::default(),
Duration::ZERO,
vec![0; size],
)
.unwrap()
}
#[test]
fn i420_is_one_and_a_half_bytes_a_pixel() {
let size = packed_size(PixelFormat::I420, 1920, 1080).unwrap();
assert_eq!(size, 1920 * 1080 * 3 / 2);
}
#[test]
fn odd_dimensions_round_chroma_up() {
let format = PixelFormat::I420;
assert_eq!(format.plane_dimensions(1, 1921, 1081), Some((961, 541)));
let size = packed_size(format, 1921, 1081).unwrap();
assert_eq!(size, 1921 * 1081 + 2 * (961 * 541));
}
#[test]
fn nv12_has_two_planes_and_interleaved_chroma() {
assert_eq!(PixelFormat::Nv12.plane_count(), 2);
assert_eq!(PixelFormat::Nv12.samples_per_pixel(1), 2);
let size = packed_size(PixelFormat::Nv12, 640, 480).unwrap();
assert_eq!(size, 640 * 480 * 3 / 2);
}
#[test]
fn p010_takes_two_bytes_a_sample() {
assert_eq!(PixelFormat::P010.bit_depth(), 10);
assert_eq!(
packed_size(PixelFormat::P010, 640, 480).unwrap(),
640 * 480 * 3
);
}
#[test]
fn a_frame_too_small_for_its_dimensions_is_refused() {
let error = Frame::packed(
1920,
1080,
PixelFormat::I420,
ColorSpace::default(),
Duration::ZERO,
vec![0; 100],
);
assert!(matches!(error, Err(Error::BadFrame { .. })));
}
#[test]
fn a_zero_sized_picture_is_refused() {
assert!(matches!(
packed_size(PixelFormat::I420, 0, 1080),
Err(Error::BadFrame { .. })
));
}
#[test]
fn padded_strides_are_accepted_and_the_last_row_need_not_be_padded() {
let width = 100_u32;
let height = 10_u32;
let stride = 128_usize;
let luma = stride * (height as usize - 1) + width as usize;
let chroma_rows = height.div_ceil(2) as usize;
let chroma = stride * (chroma_rows - 1) + width.div_ceil(2) as usize;
let planes = vec![
Plane { offset: 0, stride },
Plane {
offset: luma,
stride,
},
Plane {
offset: luma + chroma,
stride,
},
];
let total = luma + 2 * chroma;
let frame = Frame::with_planes(
width,
height,
PixelFormat::I420,
ColorSpace::default(),
Duration::ZERO,
planes,
vec![0; total],
)
.unwrap();
assert_eq!(frame.row(0, 9).unwrap().len(), width as usize);
assert!(frame.row(0, 10).is_none(), "there is no eleventh row");
}
#[test]
fn a_stride_shorter_than_a_row_is_refused() {
let planes = vec![
Plane {
offset: 0,
stride: 10,
},
Plane {
offset: 0,
stride: 10,
},
Plane {
offset: 0,
stride: 10,
},
];
let error = Frame::with_planes(
100,
10,
PixelFormat::I420,
ColorSpace::default(),
Duration::ZERO,
planes,
vec![0; 10_000],
);
let Err(Error::BadFrame { reason }) = error else {
panic!("a 10-byte stride cannot hold a 100-byte row");
};
assert!(reason.contains("stride"), "{reason}");
}
#[test]
fn a_plane_that_runs_past_the_buffer_is_refused() {
let planes = vec![
Plane {
offset: 0,
stride: 1024,
},
Plane {
offset: 0,
stride: 1024,
},
Plane {
offset: 0,
stride: 1024,
},
];
let error = Frame::with_planes(
100,
100,
PixelFormat::I420,
ColorSpace::default(),
Duration::ZERO,
planes,
vec![0; 1000],
);
assert!(matches!(error, Err(Error::BadFrame { .. })));
}
#[test]
fn retiming_shares_the_pixels() {
let frame = frame_of(PixelFormat::I420, 64, 64);
let moved = frame.with_pts(Duration::from_millis(500));
assert_eq!(moved.pts(), Duration::from_millis(500));
assert_eq!(frame.pts(), Duration::ZERO);
assert!(
Arc::ptr_eq(&frame.data, &moved.data),
"retiming copied the picture"
);
}
#[test]
fn the_pool_stops_allocating_once_it_is_warm() {
let mut pool = FramePool::new(4);
let size = packed_size(PixelFormat::I420, 320, 240).unwrap();
for _ in 0..10 {
let buffer = pool.take(size);
pool.give(buffer);
}
let (reused, allocated) = pool.statistics();
assert_eq!(allocated, 1, "one allocation should have served all ten");
assert_eq!(reused, 9);
}
#[test]
fn the_pool_will_not_take_a_buffer_still_being_read() {
let mut pool = FramePool::new(4);
let frame = frame_of(PixelFormat::I420, 64, 64);
let still_held = frame.clone();
pool.reclaim(frame);
assert_eq!(pool.buffers.len(), 0, "that buffer is still in use");
drop(still_held);
}
}