use super::{CodecParser, Frame, PesPacket, pts_to_ns};
const MAX_SPU_BYTES: usize = 0xFFFF;
pub struct DvdSubParser {
codec_data: Option<Vec<u8>>,
pending: Option<(i64, usize, Vec<u8>)>,
}
impl DvdSubParser {
pub fn new(codec_data: Option<Vec<u8>>) -> Self {
Self {
codec_data,
pending: None,
}
}
fn take_if_complete(&mut self, force: bool) -> Option<Frame> {
let (_, size, buf) = self.pending.as_ref()?;
if force || buf.len() >= *size {
let (pts_ns, _, data) = self.pending.take().unwrap();
return Some(Frame {
pts_ns,
keyframe: true,
data,
duration_ns: None,
});
}
None
}
}
impl CodecParser for DvdSubParser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
if pes.data.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
if pes.pts.is_none() {
if self.pending.is_some() {
if let Some((_, _, buf)) = self.pending.as_mut() {
let room = MAX_SPU_BYTES.saturating_sub(buf.len());
let take = room.min(pes.data.len());
buf.extend_from_slice(&pes.data[..take]);
}
if let Some(frame) = self.take_if_complete(false) {
out.push(frame);
}
return out;
}
} else if let Some(frame) = self.take_if_complete(true) {
out.push(frame);
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
let declared = if pes.data.len() >= 2 {
let d = ((pes.data[0] as usize) << 8) | pes.data[1] as usize;
if d < 2 {
out.push(Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
duration_ns: None,
});
return out;
}
d
} else {
out.push(Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
duration_ns: None,
});
return out;
};
let mut buf = pes.data.clone();
if buf.len() > MAX_SPU_BYTES {
buf.truncate(MAX_SPU_BYTES);
}
self.pending = Some((pts_ns, declared, buf));
if let Some(frame) = self.take_if_complete(false) {
out.push(frame);
}
out
}
fn flush(&mut self) -> Vec<Frame> {
self.take_if_complete(true).into_iter().collect()
}
fn codec_private(&self) -> Option<Vec<u8>> {
self.codec_data.clone()
}
}
pub fn ycbcr_to_rgb(color: &[u8; 4]) -> [u8; 3] {
let y = color[1] as f64;
let cb = color[2] as f64;
let cr = color[3] as f64;
let r = y + 1.402 * (cr - 128.0);
let g = y - 0.344 * (cb - 128.0) - 0.714 * (cr - 128.0);
let b = y + 1.772 * (cb - 128.0);
[clamp_u8(r), clamp_u8(g), clamp_u8(b)]
}
fn clamp_u8(v: f64) -> u8 {
if v < 0.0 {
0
} else if v > 255.0 {
255
} else {
v.round() as u8
}
}
pub fn format_palette(palette: &[[u8; 4]]) -> Vec<u8> {
let mut parts: Vec<String> = Vec::with_capacity(palette.len());
for color in palette {
let [r, g, b] = ycbcr_to_rgb(color);
parts.push(format!("{r:02x}{g:02x}{b:02x}"));
}
let line = format!("palette: {}\n", parts.join(", "));
line.into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket {
pid: 0x1200,
pts,
dts: None,
data,
}
}
#[test]
fn passthrough_data() {
let mut parser = DvdSubParser::new(None);
let sub_data = vec![0x00, 0x0A, 0x00, 0x08, 0x01, 0xFF, 0x02, 0x03, 0x04, 0x05];
let pes = make_pes(sub_data.clone(), Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].data, sub_data,
"VobSub data should pass through unmodified"
);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
}
#[test]
fn always_keyframe() {
let mut parser = DvdSubParser::new(None);
for i in 0..3u8 {
let data = vec![0x00, i, 0x00, i + 1];
let pes = make_pes(data, Some(90000 * i as i64));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(
frames[0].keyframe,
"DVD subtitle frames should always be keyframes"
);
}
}
#[test]
fn empty_pes_returns_no_frames() {
let mut parser = DvdSubParser::new(None);
let pes = make_pes(Vec::new(), Some(0));
assert!(parser.parse(&pes).is_empty());
}
#[test]
fn codec_private_none_by_default() {
let parser = DvdSubParser::new(None);
assert!(parser.codec_private().is_none());
}
#[test]
fn codec_private_returns_palette_when_set() {
let palette_data = b"palette: 000000, ffffff\n".to_vec();
let parser = DvdSubParser::new(Some(palette_data.clone()));
let cp = parser.codec_private();
assert!(cp.is_some());
assert_eq!(cp.unwrap(), palette_data);
}
#[test]
fn no_pts_defaults_to_zero() {
let mut parser = DvdSubParser::new(None);
let pes = make_pes(vec![0x00, 0x02], None);
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pts_ns, 0);
}
#[test]
fn multi_pes_spu_reassembled() {
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x0C, 0xAA, 0xBB, 0xCC, 0xDD];
let cont1 = vec![0x11, 0x22, 0x33];
let cont2 = vec![0x44, 0x55, 0x66];
let f = parser.parse(&make_pes(head.clone(), Some(90000)));
assert!(f.is_empty(), "incomplete SPU should not emit yet");
let f = parser.parse(&make_pes(cont1.clone(), None));
assert!(f.is_empty(), "still incomplete");
let frames = parser.parse(&make_pes(cont2.clone(), None));
assert_eq!(frames.len(), 1, "completed SPU emits exactly one frame");
let mut expected = head;
expected.extend_from_slice(&cont1);
expected.extend_from_slice(&cont2);
assert_eq!(frames[0].data, expected);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
assert!(frames[0].keyframe);
}
#[test]
fn flush_emits_truncated_trailing_spu() {
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x64, 0xDE, 0xAD, 0xBE, 0xEF];
let f = parser.parse(&make_pes(head.clone(), Some(90000)));
assert!(f.is_empty(), "incomplete SPU should not emit during parse");
let frames = parser.flush();
assert_eq!(frames.len(), 1, "EOF flush emits the partial SPU");
assert_eq!(frames[0].data, head);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
}
#[test]
fn real_pts_pes_force_emits_stale_pending_and_starts_new_spu() {
let mut parser = DvdSubParser::new(None);
let head1 = vec![0x00, 0x64, 0xDE, 0xAD, 0xBE, 0xEF];
assert!(
parser
.parse(&make_pes(head1.clone(), Some(90000)))
.is_empty(),
"SPU 1 incomplete, held pending"
);
let head2 = vec![0x00, 0x04, 0x11, 0x22];
let frames = parser.parse(&make_pes(head2.clone(), Some(180000)));
assert_eq!(frames.len(), 2, "stale flushed + new emitted");
assert_eq!(frames[0].data, head1, "stale SPU 1 emitted truncated");
assert_eq!(frames[0].pts_ns, 1_000_000_000, "SPU 1 keeps its PTS");
assert_eq!(frames[1].data, head2, "SPU 2 emitted fresh");
assert_eq!(frames[1].pts_ns, 2_000_000_000, "SPU 2 keeps its own PTS");
}
#[test]
fn corrupt_oversized_size_recovers_on_next_real_pts() {
let mut parser = DvdSubParser::new(None);
let bad = vec![0xFF, 0xFF, 0x01, 0x02, 0x03];
assert!(parser.parse(&make_pes(bad.clone(), Some(90000))).is_empty());
assert!(parser.parse(&make_pes(vec![0x04, 0x05], None)).is_empty());
let good = vec![0x00, 0x04, 0xAA, 0xBB];
let frames = parser.parse(&make_pes(good.clone(), Some(270000)));
assert_eq!(frames.len(), 2, "track recovers, not swallowed to EOF");
assert_eq!(frames[1].data, good);
assert_eq!(frames[1].pts_ns, 3_000_000_000);
}
#[test]
fn declared_size_below_two_passes_through_as_lone_frame() {
let mut parser = DvdSubParser::new(None);
let data = vec![0x00, 0x00, 0xAB, 0xCD]; let frames = parser.parse(&make_pes(data.clone(), Some(90000)));
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data, data, "passed through whole");
assert!(parser.pending.is_none(), "no pending left open");
}
#[test]
fn ycbcr_to_rgb_white() {
let color = [0x00, 235, 128, 128];
let [r, g, b] = ycbcr_to_rgb(&color);
assert_eq!(r, 235);
assert_eq!(g, 235);
assert_eq!(b, 235);
}
#[test]
fn ycbcr_to_rgb_black() {
let color = [0x00, 16, 128, 128];
let [r, g, b] = ycbcr_to_rgb(&color);
assert_eq!(r, 16);
assert_eq!(g, 16);
assert_eq!(b, 16);
}
#[test]
fn ycbcr_to_rgb_clamps_overflow() {
let color = [0x00, 255, 128, 255];
let [r, _g, _b] = ycbcr_to_rgb(&color);
assert_eq!(r, 255);
}
#[test]
fn ycbcr_to_rgb_clamps_underflow() {
let color = [0x00, 0, 128, 0];
let [r, _g, _b] = ycbcr_to_rgb(&color);
assert_eq!(r, 0);
}
#[test]
fn ycbcr_to_rgb_red() {
let color = [0x00, 82, 90, 240];
let [r, g, b] = ycbcr_to_rgb(&color);
assert!(r > 200, "R should be high for red, got {}", r);
assert!(g < 30, "G should be low for red, got {}", g);
assert!(b < 30, "B should be low for red, got {}", b);
}
#[test]
fn format_palette_basic() {
let palette = vec![
[0x00, 0, 128, 128], [0x00, 255, 128, 128], ];
let result = format_palette(&palette);
let text = String::from_utf8(result).unwrap();
assert!(
text.starts_with("palette: "),
"should start with 'palette: '"
);
assert!(text.ends_with('\n'), "should end with newline");
assert!(
text.contains("000000"),
"black should be 000000, got: {}",
text
);
assert!(
text.contains("ffffff"),
"white should be ffffff, got: {}",
text
);
}
#[test]
fn format_palette_16_colors() {
let palette: Vec<[u8; 4]> = (0..16).map(|i| [0x00, (i * 16) as u8, 128, 128]).collect();
let result = format_palette(&palette);
let text = String::from_utf8(result).unwrap();
let comma_count = text.matches(", ").count();
assert_eq!(
comma_count, 15,
"16 colors should have 15 separators, got {}",
comma_count
);
}
#[test]
fn format_palette_hex_format() {
let palette = vec![[0x00, 128, 128, 128]];
let result = format_palette(&palette);
let text = String::from_utf8(result).unwrap();
assert_eq!(text, "palette: 808080\n");
}
#[test]
fn spu_completes_exactly_at_declared_size() {
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x06, 0xAA, 0xBB, 0xCC, 0xDD]; let f = parser.parse(&make_pes(head.clone(), Some(90000)));
assert_eq!(f.len(), 1, "complete-on-arrival SPU emits at once");
assert_eq!(f[0].data, head);
assert!(parser.pending.is_none(), "nothing left pending");
}
#[test]
fn spu_one_byte_short_waits_then_completes() {
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x07, 0xAA, 0xBB, 0xCC, 0xDD]; assert!(
parser
.parse(&make_pes(head.clone(), Some(90000)))
.is_empty()
);
let f = parser.parse(&make_pes(vec![0xEE], None)); assert_eq!(f.len(), 1);
let mut expect = head;
expect.push(0xEE);
assert_eq!(
f[0].data, expect,
"reassembled to exactly the declared size"
);
}
#[test]
fn spu_overshoot_emits_all_buffered_bytes() {
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x05, 0xAA, 0xBB]; assert!(parser.parse(&make_pes(head, Some(90000))).is_empty());
let f = parser.parse(&make_pes(vec![0xCC, 0xDD, 0xEE, 0xFF], None));
assert_eq!(f.len(), 1);
assert_eq!(
f[0].data,
vec![0x00, 0x05, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
"all buffered bytes emitted, not truncated to declared size"
);
}
#[test]
fn head_pes_larger_than_max_spu_is_truncated() {
let mut parser = DvdSubParser::new(None);
let mut head = vec![0xFF, 0xFF]; head.extend(std::iter::repeat_n(0xAB, MAX_SPU_BYTES + 100));
let f = parser.parse(&make_pes(head, Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(
f[0].data.len(),
MAX_SPU_BYTES,
"head buffer truncated to MAX_SPU_BYTES"
);
}
#[test]
fn continuation_appends_bounded_by_max_spu() {
let mut parser = DvdSubParser::new(None);
let mut head = vec![0xFF, 0xFE]; head.extend(std::iter::repeat_n(0x11, 1000));
assert!(parser.parse(&make_pes(head, Some(90000))).is_empty());
for _ in 0..100 {
let _ = parser.parse(&make_pes(vec![0x22u8; 2000], None));
}
let pending_len = parser
.pending
.as_ref()
.map(|(_, _, b)| b.len())
.unwrap_or(0);
assert!(
pending_len <= MAX_SPU_BYTES,
"pending {pending_len} exceeded MAX_SPU_BYTES {MAX_SPU_BYTES}"
);
}
#[test]
fn single_byte_head_passes_through_as_lone_frame() {
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0xAB], Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, vec![0xAB]);
assert!(parser.pending.is_none());
}
#[test]
fn declared_size_one_passes_through() {
let mut parser = DvdSubParser::new(None);
let data = vec![0x00, 0x01, 0xAB];
let f = parser.parse(&make_pes(data.clone(), Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, data);
assert!(parser.pending.is_none());
}
#[test]
fn no_pts_short_segment_without_pending_passes_through() {
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0xAA], None));
assert_eq!(f.len(), 1);
assert_eq!(f[0].pts_ns, 0, "lone no-PTS segment falls back to pts 0");
assert_eq!(f[0].data, vec![0xAA]);
}
#[test]
fn no_pts_sized_segment_without_pending_starts_new_spu() {
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0x00, 0x10, 0xAA], None));
assert!(f.is_empty(), "incomplete sized segment held, not emitted");
assert!(parser.pending.is_some(), "started a new pending SPU");
assert_eq!(parser.pending.as_ref().unwrap().0, 0, "pts 0 (no PTS)");
}
#[test]
fn flush_empty_when_nothing_pending() {
let mut parser = DvdSubParser::new(None);
assert!(parser.flush().is_empty());
}
#[test]
fn ycbcr_green_channel_formula() {
let [r, g, b] = ycbcr_to_rgb(&[0x00, 145, 54, 34]);
assert!(g > 200, "G high for green, got {g}");
assert!(r < 80, "R low for green, got {r}");
assert!(b < 80, "B low for green, got {b}");
}
#[test]
fn ycbcr_neutral_chroma_is_grey() {
for y in [0u8, 64, 128, 200, 255] {
let [r, g, b] = ycbcr_to_rgb(&[0x00, y, 128, 128]);
assert_eq!([r, g, b], [y, y, y], "neutral chroma → grey at Y={y}");
}
}
#[test]
fn ycbcr_blue_channel_clamps_high() {
let [_r, _g, b] = ycbcr_to_rgb(&[0x00, 128, 255, 128]);
assert_eq!(b, 255, "blue clamps at 255");
}
#[test]
fn format_palette_empty_is_just_prefix() {
let result = format_palette(&[]);
assert_eq!(String::from_utf8(result).unwrap(), "palette: \n");
}
#[test]
fn format_palette_pads_each_channel_to_two_hex_digits() {
let result = format_palette(&[[0x00, 16, 128, 128]]);
assert_eq!(String::from_utf8(result).unwrap(), "palette: 101010\n");
}
}