use std::fmt;
use crate::error::FrameError;
use crate::{PixelFormat, PooledBuffer, Timestamp};
mod planar;
#[derive(Clone)]
pub struct VideoFrame {
planes: Vec<PooledBuffer>,
strides: Vec<usize>,
width: u32,
height: u32,
format: PixelFormat,
timestamp: Timestamp,
key_frame: bool,
}
impl VideoFrame {
pub fn new(
planes: Vec<PooledBuffer>,
strides: Vec<usize>,
width: u32,
height: u32,
format: PixelFormat,
timestamp: Timestamp,
key_frame: bool,
) -> Result<Self, FrameError> {
if planes.len() != strides.len() {
return Err(FrameError::MismatchedPlaneStride {
planes: planes.len(),
strides: strides.len(),
});
}
Ok(Self {
planes,
strides,
width,
height,
format,
timestamp,
key_frame,
})
}
pub fn empty(width: u32, height: u32, format: PixelFormat) -> Result<Self, FrameError> {
let (planes, strides) = Self::allocate_planes(width, height, format)?;
Ok(Self {
planes,
strides,
width,
height,
format,
timestamp: Timestamp::default(),
key_frame: false,
})
}
#[doc(hidden)]
#[must_use]
pub fn new_black(width: u32, height: u32, format: PixelFormat, pts_ms: i64) -> Self {
let y_w = width as usize;
let y_h = height as usize;
let uv_w = (width as usize).div_ceil(2);
let uv_h = (height as usize).div_ceil(2);
let timestamp = Timestamp::from_millis(pts_ms, crate::Rational::new(1, 1000));
Self {
planes: vec![
PooledBuffer::standalone(vec![0u8; y_w * y_h]),
PooledBuffer::standalone(vec![0x80u8; uv_w * uv_h]),
PooledBuffer::standalone(vec![0x80u8; uv_w * uv_h]),
],
strides: vec![y_w, uv_w, uv_w],
width,
height,
format,
timestamp,
key_frame: true,
}
}
#[must_use]
#[inline]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
#[inline]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
#[inline]
pub const fn format(&self) -> PixelFormat {
self.format
}
#[must_use]
#[inline]
pub const fn timestamp(&self) -> Timestamp {
self.timestamp
}
#[must_use]
#[inline]
pub const fn is_key_frame(&self) -> bool {
self.key_frame
}
#[inline]
pub fn set_key_frame(&mut self, key_frame: bool) {
self.key_frame = key_frame;
}
#[inline]
pub fn set_timestamp(&mut self, timestamp: Timestamp) {
self.timestamp = timestamp;
}
#[must_use]
#[inline]
pub fn num_planes(&self) -> usize {
self.planes.len()
}
#[must_use]
#[inline]
pub fn planes(&self) -> &[PooledBuffer] {
&self.planes
}
#[must_use]
#[inline]
pub fn plane(&self, index: usize) -> Option<&[u8]> {
self.planes.get(index).map(std::convert::AsRef::as_ref)
}
#[must_use]
#[inline]
pub fn plane_mut(&mut self, index: usize) -> Option<&mut [u8]> {
self.planes.get_mut(index).map(std::convert::AsMut::as_mut)
}
#[must_use]
#[inline]
pub fn strides(&self) -> &[usize] {
&self.strides
}
#[must_use]
#[inline]
pub fn stride(&self, plane: usize) -> Option<usize> {
self.strides.get(plane).copied()
}
#[must_use]
pub fn data(&self) -> Vec<u8> {
let total_size: usize = self.planes.iter().map(PooledBuffer::len).sum();
let mut result = Vec::with_capacity(total_size);
for plane in &self.planes {
result.extend_from_slice(plane.as_ref());
}
result
}
#[must_use]
#[inline]
pub fn data_ref(&self) -> Option<&[u8]> {
if self.format.is_packed() && self.planes.len() == 1 {
Some(self.planes[0].as_ref())
} else {
None
}
}
#[must_use]
#[inline]
pub fn data_mut(&mut self) -> Option<&mut [u8]> {
if self.format.is_packed() && self.planes.len() == 1 {
Some(self.planes[0].as_mut())
} else {
None
}
}
#[must_use]
pub fn total_size(&self) -> usize {
self.planes.iter().map(PooledBuffer::len).sum()
}
#[must_use]
#[inline]
pub const fn resolution(&self) -> (u32, u32) {
(self.width, self.height)
}
#[must_use]
#[inline]
pub fn aspect_ratio(&self) -> f64 {
if self.height == 0 {
log::warn!(
"aspect_ratio unavailable, height is 0, returning 0.0 \
width={} height=0 fallback=0.0",
self.width
);
0.0
} else {
f64::from(self.width) / f64::from(self.height)
}
}
}
impl fmt::Debug for VideoFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VideoFrame")
.field("width", &self.width)
.field("height", &self.height)
.field("format", &self.format)
.field("timestamp", &self.timestamp)
.field("key_frame", &self.key_frame)
.field("num_planes", &self.planes.len())
.field(
"plane_sizes",
&self
.planes
.iter()
.map(PooledBuffer::len)
.collect::<Vec<_>>(),
)
.field("strides", &self.strides)
.finish()
}
}
impl fmt::Display for VideoFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"VideoFrame({}x{} {} @ {}{})",
self.width,
self.height,
self.format,
self.timestamp,
if self.key_frame { " [KEY]" } else { "" }
)
}
}
impl Default for VideoFrame {
fn default() -> Self {
Self {
planes: vec![
PooledBuffer::standalone(vec![0u8; 1]),
PooledBuffer::standalone(vec![0u8; 1]),
PooledBuffer::standalone(vec![0u8; 1]),
],
strides: vec![1, 1, 1],
width: 1,
height: 1,
format: PixelFormat::Yuv420p,
timestamp: Timestamp::default(),
key_frame: false,
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::redundant_closure_for_method_calls,
clippy::float_cmp
)]
mod tests {
use super::*;
use crate::Rational;
#[test]
fn test_new_rgba_frame() {
let width = 640u32;
let height = 480u32;
let stride = width as usize * 4;
let data = vec![0u8; stride * height as usize];
let ts = Timestamp::new(1000, Rational::new(1, 1000));
let frame = VideoFrame::new(
vec![PooledBuffer::standalone(data)],
vec![stride],
width,
height,
PixelFormat::Rgba,
ts,
true,
)
.unwrap();
assert_eq!(frame.width(), 640);
assert_eq!(frame.height(), 480);
assert_eq!(frame.format(), PixelFormat::Rgba);
assert_eq!(frame.timestamp(), ts);
assert!(frame.is_key_frame());
assert_eq!(frame.num_planes(), 1);
assert_eq!(frame.stride(0), Some(640 * 4));
}
#[test]
fn test_new_yuv420p_frame() {
let width = 640u32;
let height = 480u32;
let y_stride = width as usize;
let uv_stride = (width / 2) as usize;
let uv_height = (height / 2) as usize;
let y_data = vec![128u8; y_stride * height as usize];
let u_data = vec![128u8; uv_stride * uv_height];
let v_data = vec![128u8; uv_stride * uv_height];
let frame = VideoFrame::new(
vec![
PooledBuffer::standalone(y_data),
PooledBuffer::standalone(u_data),
PooledBuffer::standalone(v_data),
],
vec![y_stride, uv_stride, uv_stride],
width,
height,
PixelFormat::Yuv420p,
Timestamp::default(),
false,
)
.unwrap();
assert_eq!(frame.width(), 640);
assert_eq!(frame.height(), 480);
assert_eq!(frame.format(), PixelFormat::Yuv420p);
assert!(!frame.is_key_frame());
assert_eq!(frame.num_planes(), 3);
assert_eq!(frame.stride(0), Some(640));
assert_eq!(frame.stride(1), Some(320));
assert_eq!(frame.stride(2), Some(320));
}
#[test]
fn test_new_mismatched_planes_strides() {
let result = VideoFrame::new(
vec![PooledBuffer::standalone(vec![0u8; 100])],
vec![10, 10], 10,
10,
PixelFormat::Rgba,
Timestamp::default(),
false,
);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
FrameError::MismatchedPlaneStride {
planes: 1,
strides: 2
}
);
}
#[test]
fn test_resolution() {
let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
assert_eq!(frame.resolution(), (1920, 1080));
}
#[test]
fn test_aspect_ratio() {
let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
let aspect = frame.aspect_ratio();
assert!((aspect - 16.0 / 9.0).abs() < 0.001);
let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
let aspect_4_3 = frame_4_3.aspect_ratio();
assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
}
#[test]
fn test_aspect_ratio_zero_height() {
let frame = VideoFrame::new(
vec![PooledBuffer::standalone(vec![])],
vec![0],
100,
0,
PixelFormat::Rgba,
Timestamp::default(),
false,
)
.unwrap();
assert_eq!(frame.aspect_ratio(), 0.0);
}
#[test]
fn test_total_size_rgba() {
let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
assert_eq!(frame.total_size(), 1920 * 1080 * 4);
}
#[test]
fn test_total_size_yuv420p() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
let expected = 640 * 480 + 320 * 240 * 2;
assert_eq!(frame.total_size(), expected);
}
#[test]
fn test_set_key_frame() {
let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
assert!(!frame.is_key_frame());
frame.set_key_frame(true);
assert!(frame.is_key_frame());
frame.set_key_frame(false);
assert!(!frame.is_key_frame());
}
#[test]
fn test_set_timestamp() {
let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
let ts = Timestamp::new(90000, Rational::new(1, 90000));
frame.set_timestamp(ts);
assert_eq!(frame.timestamp(), ts);
}
#[test]
fn test_plane_access() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
assert!(frame.plane(0).is_some());
assert!(frame.plane(1).is_some());
assert!(frame.plane(2).is_some());
assert!(frame.plane(3).is_none());
}
#[test]
fn test_plane_mut_access() {
let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
if let Some(data) = frame.plane_mut(0) {
for chunk in data.chunks_exact_mut(4) {
chunk[0] = 255;
chunk[1] = 0;
chunk[2] = 0;
chunk[3] = 255;
}
}
let plane = frame.plane(0).unwrap();
assert_eq!(plane[0], 255); assert_eq!(plane[1], 0); assert_eq!(plane[2], 0); assert_eq!(plane[3], 255); }
#[test]
fn test_planes_slice() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
let planes = frame.planes();
assert_eq!(planes.len(), 3);
}
#[test]
fn test_strides_slice() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
let strides = frame.strides();
assert_eq!(strides.len(), 3);
assert_eq!(strides[0], 640);
assert_eq!(strides[1], 320);
assert_eq!(strides[2], 320);
}
#[test]
fn test_stride_out_of_bounds() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
assert!(frame.stride(0).is_some());
assert!(frame.stride(1).is_none());
}
#[test]
fn test_data_contiguous() {
let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
let data = frame.data();
assert_eq!(data.len(), 4 * 4 * 4);
}
#[test]
fn test_data_yuv420p_concatenation() {
let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
let data = frame.data();
assert_eq!(data.len(), 24);
}
#[test]
fn test_data_ref_packed() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
assert!(frame.data_ref().is_some());
assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
}
#[test]
fn test_data_ref_planar() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
assert!(frame.data_ref().is_none());
}
#[test]
fn test_data_mut_packed() {
let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
assert!(frame.data_mut().is_some());
if let Some(data) = frame.data_mut() {
data[0] = 123;
}
assert_eq!(frame.plane(0).unwrap()[0], 123);
}
#[test]
fn test_data_mut_planar() {
let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
assert!(frame.data_mut().is_none());
}
#[test]
fn test_clone() {
let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
original.set_key_frame(true);
original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
if let Some(data) = original.plane_mut(0) {
data[0] = 42;
}
let cloned = original.clone();
assert_eq!(cloned.width(), original.width());
assert_eq!(cloned.height(), original.height());
assert_eq!(cloned.format(), original.format());
assert_eq!(cloned.timestamp(), original.timestamp());
assert_eq!(cloned.is_key_frame(), original.is_key_frame());
assert_eq!(cloned.plane(0).unwrap()[0], 42);
let mut cloned = cloned;
if let Some(data) = cloned.plane_mut(0) {
data[0] = 99;
}
assert_eq!(original.plane(0).unwrap()[0], 42);
assert_eq!(cloned.plane(0).unwrap()[0], 99);
}
#[test]
fn video_frame_clone_should_have_identical_data() {
let width = 320u32;
let height = 240u32;
let stride = width as usize;
let y_data = vec![42u8; stride * height as usize];
let uv_stride = (width / 2) as usize;
let uv_data = vec![128u8; uv_stride * (height / 2) as usize];
let ts = Timestamp::new(1000, Rational::new(1, 1000));
let original = VideoFrame::new(
vec![
PooledBuffer::standalone(y_data.clone()),
PooledBuffer::standalone(uv_data.clone()),
PooledBuffer::standalone(uv_data.clone()),
],
vec![stride, uv_stride, uv_stride],
width,
height,
PixelFormat::Yuv420p,
ts,
false,
)
.unwrap();
let clone = original.clone();
assert_eq!(clone.width(), original.width());
assert_eq!(clone.height(), original.height());
assert_eq!(clone.format(), original.format());
assert_eq!(clone.timestamp(), original.timestamp());
assert_eq!(clone.is_key_frame(), original.is_key_frame());
assert_eq!(clone.num_planes(), original.num_planes());
assert_eq!(clone.plane(0), original.plane(0));
}
#[test]
fn test_debug() {
let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
let debug = format!("{frame:?}");
assert!(debug.contains("VideoFrame"));
assert!(debug.contains("640"));
assert!(debug.contains("480"));
assert!(debug.contains("Rgba"));
}
#[test]
fn test_display() {
let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
frame.set_key_frame(true);
let display = format!("{frame}");
assert!(display.contains("1920x1080"));
assert!(display.contains("yuv420p"));
assert!(display.contains("[KEY]"));
}
#[test]
fn test_display_non_keyframe() {
let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
let display = format!("{frame}");
assert!(!display.contains("[KEY]"));
}
}