pub use videoframe::frame::{Dimensions, Plane, Rect};
use derive_more::IsVariant;
use thiserror::Error;
use crate::{Timestamp, color::ColorInfo, subtitle::SubtitlePayload};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)]
#[non_exhaustive]
pub enum FrameError {
#[error(transparent)]
TooManyVideoPlanes(#[from] TooManyVideoPlanes),
#[error(transparent)]
TooManyAudioPlanes(#[from] TooManyAudioPlanes),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
#[error("VideoFrame: plane_count {plane_count} exceeds the fixed 4-plane array")]
pub struct TooManyVideoPlanes {
plane_count: u8,
}
impl TooManyVideoPlanes {
#[inline]
pub const fn new(plane_count: u8) -> Self {
Self { plane_count }
}
#[inline]
pub const fn plane_count(&self) -> u8 {
self.plane_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
#[error("AudioFrame: plane_count {plane_count} exceeds the fixed 8-plane array")]
pub struct TooManyAudioPlanes {
plane_count: u8,
}
impl TooManyAudioPlanes {
#[inline]
pub const fn new(plane_count: u8) -> Self {
Self { plane_count }
}
#[inline]
pub const fn plane_count(&self) -> u8 {
self.plane_count
}
}
pub struct VideoFrame<P, E, D> {
pts: Option<Timestamp>,
duration: Option<Timestamp>,
dimensions: Dimensions,
visible_rect: Option<Rect>,
pixel_format: P,
plane_count: u8,
planes: [Plane<D>; 4],
color: ColorInfo,
extra: E,
}
impl<P, E, D> VideoFrame<P, E, D> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(
dimensions: Dimensions,
pixel_format: P,
planes: [Plane<D>; 4],
plane_count: u8,
extra: E,
) -> Self {
assert!(
plane_count as usize <= 4,
"VideoFrame::new: plane_count exceeds the fixed 4-plane array",
);
Self {
pts: None,
duration: None,
dimensions,
visible_rect: None,
pixel_format,
plane_count,
planes,
color: ColorInfo::UNSPECIFIED,
extra,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn try_new(
dimensions: Dimensions,
pixel_format: P,
planes: [Plane<D>; 4],
plane_count: u8,
extra: E,
) -> Result<Self, FrameError> {
if plane_count as usize > 4 {
return Err(FrameError::TooManyVideoPlanes(TooManyVideoPlanes::new(
plane_count,
)));
}
Ok(Self {
pts: None,
duration: None,
dimensions,
visible_rect: None,
pixel_format,
plane_count,
planes,
color: ColorInfo::UNSPECIFIED,
extra,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn pts(&self) -> Option<Timestamp> {
self.pts
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn duration(&self) -> Option<Timestamp> {
self.duration
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn dimensions(&self) -> Dimensions {
self.dimensions
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.dimensions.width()
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.dimensions.height()
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn visible_rect(&self) -> Option<Rect> {
self.visible_rect
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn pixel_format(&self) -> &P {
&self.pixel_format
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn plane_count(&self) -> u8 {
self.plane_count
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn planes(&self) -> &[Plane<D>] {
&self.planes[..self.plane_count as usize]
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn plane(&self, i: usize) -> Option<&Plane<D>> {
if i < self.plane_count as usize {
self.planes.get(i)
} else {
None
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn color(&self) -> ColorInfo {
self.color
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra(&self) -> &E {
&self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra_mut(&mut self) -> &mut E {
&mut self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
self.duration = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_visible_rect(mut self, v: Option<Rect>) -> Self {
self.visible_rect = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_color(mut self, v: ColorInfo) -> Self {
self.color = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
self.duration = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_visible_rect(&mut self, v: Option<Rect>) -> &mut Self {
self.visible_rect = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_color(&mut self, v: ColorInfo) -> &mut Self {
self.color = v;
self
}
}
pub struct AudioFrame<S, C, E, D> {
pts: Option<Timestamp>,
duration: Option<Timestamp>,
sample_rate: u32,
nb_samples: u32,
channel_count: u8,
sample_format: S,
channel_layout: C,
plane_count: u8,
planes: [Plane<D>; 8],
extra: E,
}
impl<S, C, E, D> AudioFrame<S, C, E, D> {
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(
sample_rate: u32,
nb_samples: u32,
channel_count: u8,
sample_format: S,
channel_layout: C,
planes: [Plane<D>; 8],
plane_count: u8,
extra: E,
) -> Self {
assert!(
plane_count as usize <= 8,
"AudioFrame::new: plane_count exceeds the fixed 8-plane array",
);
Self {
pts: None,
duration: None,
sample_rate,
nb_samples,
channel_count,
sample_format,
channel_layout,
plane_count,
planes,
extra,
}
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn try_new(
sample_rate: u32,
nb_samples: u32,
channel_count: u8,
sample_format: S,
channel_layout: C,
planes: [Plane<D>; 8],
plane_count: u8,
extra: E,
) -> Result<Self, FrameError> {
if plane_count as usize > 8 {
return Err(FrameError::TooManyAudioPlanes(TooManyAudioPlanes::new(
plane_count,
)));
}
Ok(Self {
pts: None,
duration: None,
sample_rate,
nb_samples,
channel_count,
sample_format,
channel_layout,
plane_count,
planes,
extra,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn pts(&self) -> Option<Timestamp> {
self.pts
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn duration(&self) -> Option<Timestamp> {
self.duration
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn nb_samples(&self) -> u32 {
self.nb_samples
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn channel_count(&self) -> u8 {
self.channel_count
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn sample_format(&self) -> &S {
&self.sample_format
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn channel_layout(&self) -> &C {
&self.channel_layout
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn plane_count(&self) -> u8 {
self.plane_count
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn planes(&self) -> &[Plane<D>] {
&self.planes[..self.plane_count as usize]
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra(&self) -> &E {
&self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra_mut(&mut self) -> &mut E {
&mut self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
self.duration = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
self.duration = v;
self
}
}
pub struct SubtitleFrame<E, D> {
pts: Option<Timestamp>,
duration: Option<Timestamp>,
payload: SubtitlePayload<D>,
extra: E,
}
impl<E, D> SubtitleFrame<E, D> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(payload: SubtitlePayload<D>, extra: E) -> Self {
Self {
pts: None,
duration: None,
payload,
extra,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn pts(&self) -> Option<Timestamp> {
self.pts
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn duration(&self) -> Option<Timestamp> {
self.duration
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn payload(&self) -> &SubtitlePayload<D> {
&self.payload
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra(&self) -> &E {
&self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn extra_mut(&mut self) -> &mut E {
&mut self.extra
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
#[must_use]
pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
self.duration = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
self.pts = v;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
self.duration = v;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color::{ColorInfo, ColorMatrix},
subtitle::SubtitlePayload,
};
fn empty_planes() -> [Plane<&'static [u8]>; 4] {
[
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
]
}
#[test]
fn rect_construct_and_access() {
let r = Rect::new(10, 20, 1920, 1080);
assert_eq!(r.x(), 10);
assert_eq!(r.y(), 20);
assert_eq!(r.width(), 1920);
assert_eq!(r.height(), 1080);
}
#[test]
fn rect_default_is_zero() {
let r = Rect::default();
assert_eq!((r.x(), r.y(), r.width(), r.height()), (0, 0, 0, 0));
}
#[test]
fn rect_builders_chain() {
let r = Rect::default()
.with_x(1)
.with_y(2)
.with_width(3)
.with_height(4);
assert_eq!((r.x(), r.y(), r.width(), r.height()), (1, 2, 3, 4));
}
#[test]
fn rect_setters_chain() {
let mut r = Rect::default();
r.set_x(5).set_y(6).set_width(7).set_height(8);
assert_eq!((r.x(), r.y(), r.width(), r.height()), (5, 6, 7, 8));
}
#[test]
fn rect_const_construction() {
const R: Rect = Rect::new(0, 0, 1920, 1080);
assert_eq!(R.width(), 1920);
}
#[test]
fn plane_construct_and_access_borrowed() {
let buf: [u8; 4] = [1, 2, 3, 4];
let p: Plane<&[u8]> = Plane::new(&buf, 4);
assert_eq!(p.stride(), 4);
assert_eq!(p.data(), &&buf[..]);
}
#[test]
fn plane_with_and_set_stride() {
let buf: [u8; 0] = [];
let p = Plane::new(&buf[..], 16).with_stride(32);
assert_eq!(p.stride(), 32);
let mut p2 = p;
p2.set_stride(64);
assert_eq!(p2.stride(), 64);
}
#[test]
fn plane_into_data() {
let buf: [u8; 4] = [1, 2, 3, 4];
let p: Plane<&[u8]> = Plane::new(&buf, 4);
let recovered = p.into_data();
assert_eq!(recovered, &buf[..]);
}
#[test]
fn video_frame_construct_and_access() {
let f: VideoFrame<u32, (), &[u8]> = VideoFrame::new(
Dimensions::new(1920, 1080),
0u32,
empty_planes(),
1,
(),
);
assert_eq!(f.width(), 1920);
assert_eq!(f.height(), 1080);
assert_eq!(f.dimensions(), Dimensions::new(1920, 1080));
assert_eq!(f.plane_count(), 1);
assert!(f.color().matrix().is_bt_709());
assert_eq!(f.planes().len(), 1);
}
#[test]
fn video_frame_plane_index_clamped() {
let f: VideoFrame<u32, (), &[u8]> =
VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 2, ());
assert!(f.plane(0).is_some());
assert!(f.plane(1).is_some());
assert!(f.plane(2).is_none());
assert!(f.plane(3).is_none());
}
#[test]
fn video_frame_builders_chain() {
let ci = ColorInfo::UNSPECIFIED.with_matrix(ColorMatrix::Bt2020Ncl);
let f: VideoFrame<u32, (), &[u8]> =
VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 1, ())
.with_color(ci)
.with_visible_rect(Some(Rect::new(0, 0, 64, 64)));
assert!(f.color().matrix().is_bt_2020_ncl());
assert!(f.visible_rect().is_some());
}
fn audio_planes() -> [Plane<&'static [u8]>; 8] {
[
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
Plane::new(&[][..], 0),
]
}
#[test]
#[should_panic(expected = "plane_count exceeds the fixed 4-plane array")]
fn video_frame_rejects_plane_count_above_array_size() {
let _f: VideoFrame<u32, (), &[u8]> =
VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ());
}
#[test]
fn video_frame_try_new_returns_err_for_too_many_planes() {
let res: Result<VideoFrame<u32, (), &[u8]>, FrameError> =
VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ());
assert!(matches!(
res,
Err(FrameError::TooManyVideoPlanes(p)) if p.plane_count() == 5,
));
}
#[test]
fn video_frame_try_new_accepts_valid_plane_count() {
let f: VideoFrame<u32, (), &[u8]> =
VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 2, ())
.expect("plane_count = 2 is within the 4-slot capacity");
assert_eq!(f.plane_count(), 2);
}
#[test]
#[should_panic(expected = "plane_count exceeds the fixed 8-plane array")]
fn audio_frame_rejects_plane_count_above_array_size() {
let _f: AudioFrame<u32, u32, (), &[u8]> =
AudioFrame::new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ());
}
#[test]
fn audio_frame_try_new_returns_err_for_too_many_planes() {
let res: Result<AudioFrame<u32, u32, (), &[u8]>, FrameError> =
AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ());
assert!(matches!(
res,
Err(FrameError::TooManyAudioPlanes(p)) if p.plane_count() == 9,
));
}
#[test]
fn audio_frame_try_new_accepts_valid_plane_count() {
let f: AudioFrame<u32, u32, (), &[u8]> =
AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 8, ())
.expect("plane_count = 8 is the 8-slot capacity boundary");
assert_eq!(f.plane_count(), 8);
}
#[test]
fn audio_frame_construct_and_access() {
let f: AudioFrame<u32, u32, (), &[u8]> = AudioFrame::new(
48_000,
1024,
2,
0u32,
0u32,
audio_planes(),
2,
(),
);
assert_eq!(f.sample_rate(), 48_000);
assert_eq!(f.nb_samples(), 1024);
assert_eq!(f.channel_count(), 2);
assert_eq!(f.plane_count(), 2);
assert_eq!(f.planes().len(), 2);
}
#[test]
fn subtitle_frame_text_payload() {
let payload: SubtitlePayload<&[u8]> = SubtitlePayload::Text {
text: b"hi",
language: None,
};
let f: SubtitleFrame<(), &[u8]> = SubtitleFrame::new(payload, ());
match f.payload() {
SubtitlePayload::Text { text, .. } => assert_eq!(text, &&b"hi"[..]),
#[cfg(any(feature = "std", feature = "alloc"))]
_ => panic!("unexpected variant"),
}
}
}