use crate::subtitle::SubtitleCue;
use crate::vector::VectorFrame;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Frame {
Audio(AudioFrame),
Video(VideoFrame),
Subtitle(SubtitleCue),
Vector(VectorFrame),
}
impl Frame {
pub fn pts(&self) -> Option<i64> {
match self {
Self::Audio(a) => a.pts,
Self::Video(v) => v.pts,
Self::Subtitle(s) => Some(s.start_us),
Self::Vector(v) => v.pts,
}
}
}
#[derive(Clone, Debug)]
pub struct AudioFrame {
pub samples: u32,
pub pts: Option<i64>,
pub data: Vec<Vec<u8>>,
}
#[derive(Clone, Debug)]
pub struct VideoFrame {
pub pts: Option<i64>,
pub planes: Vec<VideoPlane>,
}
impl VideoFrame {
fn trailing_plane_is_palette(&self) -> bool {
self.planes
.last()
.is_some_and(|p| p.stride == 0 && !p.data.is_empty())
}
pub fn palette(&self) -> Option<&[u8]> {
if self.trailing_plane_is_palette() {
self.planes.last().map(|p| p.data.as_slice())
} else {
None
}
}
pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> {
let pal = self.palette()?;
let at = usize::from(index) * 3;
let entry = pal.get(at..at + 3)?;
Some([entry[0], entry[1], entry[2]])
}
pub fn set_palette(&mut self, rgb: Vec<u8>) {
if self.trailing_plane_is_palette() {
self.planes.pop();
}
if !rgb.is_empty() {
self.planes.push(VideoPlane {
stride: 0,
data: rgb,
});
}
}
pub fn with_palette(mut self, rgb: Vec<u8>) -> Self {
self.set_palette(rgb);
self
}
pub fn take_palette(&mut self) -> Option<Vec<u8>> {
if self.trailing_plane_is_palette() {
self.planes.pop().map(|p| p.data)
} else {
None
}
}
pub fn image_planes(&self) -> &[VideoPlane] {
let n = self.image_plane_count();
&self.planes[..n]
}
pub fn image_plane_count(&self) -> usize {
self.planes.len() - usize::from(self.trailing_plane_is_palette())
}
}
#[derive(Clone, Debug)]
pub struct VideoPlane {
pub stride: usize,
pub data: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
fn gray_frame() -> VideoFrame {
VideoFrame {
pts: Some(7),
planes: vec![VideoPlane {
stride: 4,
data: vec![0u8; 8],
}],
}
}
fn full_palette() -> Vec<u8> {
(0u16..256)
.flat_map(|i| {
let i = i as u8;
[i, !i, i ^ 0x55]
})
.collect()
}
#[test]
fn frame_without_palette_reports_none_and_full_image_planes() {
let f = gray_frame();
assert_eq!(f.palette(), None);
assert_eq!(f.palette_rgb(0), None);
assert_eq!(f.image_plane_count(), 1);
assert_eq!(f.image_planes().len(), 1);
assert_eq!(f.image_planes()[0].stride, 4);
}
#[test]
fn set_palette_round_trips_and_keeps_image_planes_intact() {
let mut f = gray_frame();
let pal = full_palette();
f.set_palette(pal.clone());
assert_eq!(f.palette(), Some(pal.as_slice()));
assert_eq!(f.image_plane_count(), 1);
assert_eq!(f.image_planes()[0].data.len(), 8);
assert_eq!(f.planes.len(), 2);
assert_eq!(f.planes[1].stride, 0);
assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE]));
assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA]));
}
#[test]
fn set_palette_replaces_existing_table() {
let mut f = gray_frame();
f.set_palette(vec![1, 2, 3]);
f.set_palette(vec![9, 8, 7, 6, 5, 4]);
assert_eq!(f.planes.len(), 2);
assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..]));
assert_eq!(f.palette_rgb(1), Some([6, 5, 4]));
}
#[test]
fn short_palette_covers_only_its_entries() {
let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]);
assert_eq!(f.palette_rgb(0), Some([10, 20, 30]));
assert_eq!(f.palette_rgb(1), Some([40, 50, 60]));
assert_eq!(f.palette_rgb(2), None);
assert_eq!(f.palette_rgb(255), None);
}
#[test]
fn empty_palette_clears_and_take_palette_detaches() {
let mut f = gray_frame();
f.set_palette(vec![1, 2, 3]);
assert!(f.palette().is_some());
f.set_palette(Vec::new());
assert_eq!(f.palette(), None);
assert_eq!(f.planes.len(), 1);
f.set_palette(vec![4, 5, 6]);
assert_eq!(f.take_palette(), Some(vec![4, 5, 6]));
assert_eq!(f.palette(), None);
assert_eq!(f.take_palette(), None);
assert_eq!(f.planes.len(), 1);
}
#[test]
fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() {
let f = VideoFrame {
pts: None,
planes: vec![
VideoPlane {
stride: 4,
data: vec![0u8; 8],
},
VideoPlane {
stride: 0,
data: Vec::new(),
},
],
};
assert_eq!(f.palette(), None);
assert_eq!(f.image_plane_count(), 2);
}
#[test]
fn palette_on_frame_without_image_planes() {
let f = VideoFrame {
pts: None,
planes: Vec::new(),
}
.with_palette(vec![1, 2, 3]);
assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
assert_eq!(f.image_plane_count(), 0);
assert!(f.image_planes().is_empty());
}
#[test]
fn palette_survives_clone_and_frame_wrapping() {
let f = gray_frame().with_palette(full_palette());
let cloned = f.clone();
assert_eq!(cloned.palette(), f.palette());
let wrapped = Frame::Video(cloned);
assert_eq!(wrapped.pts(), Some(7));
if let Frame::Video(v) = wrapped {
assert_eq!(v.palette().map(<[u8]>::len), Some(768));
} else {
unreachable!("wrapped as Video above");
}
}
}