use super::{CodecParser, Frame, PesPacket, pts_to_ns};
const SEGMENT_PCS: u8 = 0x16;
const MAX_PGS_PENDING_BYTES: usize = 4 * 1024 * 1024;
const PCS_NUM_OBJECTS_OFFSET: usize = 13;
pub struct PgsParser {
pending: Option<(i64, Vec<u8>)>,
}
impl Default for PgsParser {
fn default() -> Self {
Self::new()
}
}
impl PgsParser {
pub fn new() -> Self {
Self { pending: None }
}
fn emit_pending(&mut self, end_pts_ns: i64) -> Option<Frame> {
let (start_pts, data) = self.pending.take()?;
let duration = end_pts_ns.saturating_sub(start_pts).max(0) as u64;
Some(Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: Some(duration),
})
}
}
impl CodecParser for PgsParser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
if pes.data.is_empty() {
return Vec::new();
}
let pts = pes.pts.map(pts_to_ns);
let is_pcs = pes.data[0] == SEGMENT_PCS;
if is_pcs && pes.data.len() <= PCS_NUM_OBJECTS_OFFSET {
return self
.pending
.take()
.map(|(start_pts, data)| {
vec![Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: None,
}]
})
.unwrap_or_default();
}
let pcs_num_objects = if is_pcs {
Some(pes.data[PCS_NUM_OBJECTS_OFFSET])
} else {
None
};
let mut out = Vec::new();
match pcs_num_objects {
Some(0) => {
let frame = match pts {
Some(end) => self.emit_pending(end),
None => self.pending.take().map(|(start_pts, data)| Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: None,
}),
};
out.extend(frame);
}
Some(_) => match pts {
Some(start) => {
out.extend(self.emit_pending(start));
self.pending = Some((start, pes.data.clone()));
}
None => {
out.extend(self.pending.take().map(|(start_pts, data)| Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: None,
}));
}
},
None => {
if let Some((_, ref mut buf)) = self.pending {
if buf.len() + pes.data.len() <= MAX_PGS_PENDING_BYTES {
buf.extend_from_slice(&pes.data);
}
} else if pes.pts.is_some() {
out.push(Frame {
pts_ns: pts.unwrap_or(0),
keyframe: true,
data: pes.data.clone(),
duration_ns: None,
});
}
}
}
out
}
fn flush(&mut self) -> Vec<Frame> {
match self.pending.take() {
Some((start_pts, data)) => vec![Frame {
pts_ns: start_pts,
keyframe: true,
data,
duration_ns: None,
}],
None => Vec::new(),
}
}
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[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,
}
}
fn pcs_bytes(num_objects: u8) -> Vec<u8> {
let mut v = vec![SEGMENT_PCS, 0x00, 0x0B];
v.extend_from_slice(&[0x07, 0x80, 0x04, 0x38]); v.push(0x10); v.extend_from_slice(&[0x00, 0x01]); v.push(0x80); v.push(0x00); v.push(0x00); v.push(num_objects);
v
}
#[test]
fn display_then_clear_yields_duration() {
let mut parser = PgsParser::new();
let display = pcs_bytes(1);
let frames = parser.parse(&make_pes(display.clone(), Some(90000)));
assert!(frames.is_empty(), "display PCS should be pending");
let clear = pcs_bytes(0);
let frames = parser.parse(&make_pes(clear, Some(270000)));
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
assert_eq!(frames[0].duration_ns, Some(2_000_000_000));
assert_eq!(frames[0].data, display);
}
#[test]
fn replace_without_clear_still_emits_prior_with_duration() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let frames = parser.parse(&make_pes(pcs_bytes(1), Some(180000)));
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
assert_eq!(frames[0].duration_ns, Some(1_000_000_000));
}
#[test]
fn non_pcs_segment_appends_to_pending() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let frames = parser.parse(&make_pes(vec![0x15, 0x00, 0x02, 0xAA, 0xBB], Some(90000)));
assert!(frames.is_empty());
let frames = parser.parse(&make_pes(pcs_bytes(0), Some(180000)));
assert_eq!(frames.len(), 1);
let data = &frames[0].data;
assert!(data.windows(5).any(|w| w == [0x15, 0x00, 0x02, 0xAA, 0xBB]));
}
#[test]
fn pending_buffer_is_capped() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let chunk = vec![0x15u8; 256 * 1024]; let floods = (MAX_PGS_PENDING_BYTES / chunk.len()) + 32;
for _ in 0..floods {
let frames = parser.parse(&make_pes(chunk.clone(), Some(90000)));
assert!(frames.is_empty(), "non-PCS appends should not emit");
}
let pending_len = parser.pending.as_ref().map(|(_, b)| b.len()).unwrap_or(0);
assert!(
pending_len <= MAX_PGS_PENDING_BYTES,
"pending buffer {pending_len} exceeded cap {MAX_PGS_PENDING_BYTES}"
);
let frames = parser.parse(&make_pes(pcs_bytes(0), Some(180000)));
assert_eq!(frames.len(), 1);
}
#[test]
fn flush_emits_final_pending_subtitle() {
let mut parser = PgsParser::new();
let display = pcs_bytes(1);
let frames = parser.parse(&make_pes(display.clone(), Some(90000)));
assert!(frames.is_empty(), "display PCS should be pending");
let frames = parser.flush();
assert_eq!(frames.len(), 1, "final pending subtitle must flush");
assert_eq!(frames[0].pts_ns, 1_000_000_000);
assert_eq!(frames[0].data, display);
assert_eq!(frames[0].duration_ns, None);
}
#[test]
fn display_pcs_without_pts_is_not_stored_with_zero_start() {
let mut parser = PgsParser::new();
let frames = parser.parse(&make_pes(pcs_bytes(1), None));
assert!(frames.is_empty(), "no-PTS display PCS emits nothing");
assert!(
parser.pending.is_none(),
"no-PTS display PCS must not be stored as pending"
);
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let f = parser.parse(&make_pes(pcs_bytes(0), Some(270000)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].pts_ns, 1_000_000_000);
assert_eq!(f[0].duration_ns, Some(2_000_000_000));
}
#[test]
fn clear_pcs_without_pts_emits_pending_undurated() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let f = parser.parse(&make_pes(pcs_bytes(0), None));
assert_eq!(f.len(), 1);
assert_eq!(f[0].pts_ns, 1_000_000_000, "pending keeps its real start");
assert_eq!(f[0].duration_ns, None, "no duration without a clear PTS");
}
#[test]
fn truncated_pcs_flushes_pending_and_resyncs() {
let mut parser = PgsParser::new();
let display = pcs_bytes(1);
assert!(
parser
.parse(&make_pes(display.clone(), Some(90000)))
.is_empty()
);
let truncated = vec![SEGMENT_PCS; PCS_NUM_OBJECTS_OFFSET];
let frames = parser.parse(&make_pes(truncated, Some(180000)));
assert_eq!(frames.len(), 1, "pending display flushed on truncated PCS");
assert_eq!(frames[0].data, display, "pending bitmap not polluted");
assert_eq!(frames[0].duration_ns, None, "flushed undurated");
assert!(parser.pending.is_none(), "parser resynced");
}
#[test]
fn lone_non_pcs_without_pts_is_dropped() {
let mut parser = PgsParser::new();
let frames = parser.parse(&make_pes(vec![0x15, 0x00, 0x02, 0xAA], None));
assert!(frames.is_empty(), "no pending + no PTS → dropped");
}
#[test]
fn lone_non_pcs_with_pts_passes_through() {
let mut parser = PgsParser::new();
let frames = parser.parse(&make_pes(vec![0x15, 0x00, 0x02, 0xAA], Some(90000)));
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
}
#[test]
fn flush_with_nothing_pending_is_empty() {
let mut parser = PgsParser::new();
assert!(parser.flush().is_empty());
}
#[test]
fn codec_private_none() {
let parser = PgsParser::new();
assert!(parser.codec_private().is_none());
}
#[test]
fn parse_empty_pes() {
let mut parser = PgsParser::new();
let pes = make_pes(Vec::new(), Some(0));
assert!(parser.parse(&pes).is_empty());
}
#[test]
fn num_objects_read_from_offset_13() {
let mut display = vec![SEGMENT_PCS];
display.extend_from_slice(&[0xFF; 12]); display.push(1); let mut parser = PgsParser::new();
assert!(
parser.parse(&make_pes(display, Some(90000))).is_empty(),
"byte 13 == 1 → display PCS (pending), no emit yet"
);
let mut clear = vec![SEGMENT_PCS];
clear.extend_from_slice(&[0xFF; 12]);
clear.push(0); let f = parser.parse(&make_pes(clear, Some(270000)));
assert_eq!(f.len(), 1, "byte 13 == 0 closes the pending display");
}
#[test]
fn duration_clamps_to_zero_when_clear_precedes_display() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(270000))); let f = parser.parse(&make_pes(pcs_bytes(0), Some(90000))); assert_eq!(f.len(), 1);
assert_eq!(f[0].pts_ns, 3_000_000_000, "keeps display start");
assert_eq!(
f[0].duration_ns,
Some(0),
"clear-before-display clamps to 0, no u64 wrap"
);
}
#[test]
fn duration_zero_when_equal_pts() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let f = parser.parse(&make_pes(pcs_bytes(0), Some(90000)));
assert_eq!(f[0].duration_ns, Some(0));
}
#[test]
fn clear_with_no_pending_emits_nothing() {
let mut parser = PgsParser::new();
let f = parser.parse(&make_pes(pcs_bytes(0), Some(90000)));
assert!(f.is_empty(), "clear with nothing pending → no frame");
assert!(parser.pending.is_none());
}
#[test]
fn three_displays_each_close_the_previous() {
let mut parser = PgsParser::new();
let f0 = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
assert!(f0.is_empty());
let f1 = parser.parse(&make_pes(pcs_bytes(1), Some(180000)));
assert_eq!(f1.len(), 1);
assert_eq!(f1[0].pts_ns, 1_000_000_000);
assert_eq!(f1[0].duration_ns, Some(1_000_000_000));
let f2 = parser.parse(&make_pes(pcs_bytes(1), Some(270000)));
assert_eq!(f2.len(), 1);
assert_eq!(f2[0].pts_ns, 2_000_000_000);
assert_eq!(f2[0].duration_ns, Some(1_000_000_000));
let tail = parser.flush();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].pts_ns, 3_000_000_000);
assert_eq!(tail[0].duration_ns, None);
}
#[test]
fn pcs_exactly_at_offset_boundary_is_truncated() {
let mut parser = PgsParser::new();
let display = pcs_bytes(1);
let _ = parser.parse(&make_pes(display.clone(), Some(90000)));
let exactly_13 = vec![SEGMENT_PCS; PCS_NUM_OBJECTS_OFFSET]; let f = parser.parse(&make_pes(exactly_13, Some(180000)));
assert_eq!(f.len(), 1, "13-byte PCS is truncated → flush pending");
assert_eq!(f[0].duration_ns, None);
assert!(parser.pending.is_none());
}
#[test]
fn pcs_one_byte_past_offset_reads_num_objects() {
let mut parser = PgsParser::new();
let mut display = vec![SEGMENT_PCS; PCS_NUM_OBJECTS_OFFSET];
display.push(1); assert!(
parser.parse(&make_pes(display, Some(90000))).is_empty(),
"14-byte display PCS is pending (not truncated)"
);
assert!(parser.pending.is_some(), "stored as pending display");
}
#[test]
fn non_pcs_without_pending_with_pts_passes_through_keyframe() {
let mut parser = PgsParser::new();
let f = parser.parse(&make_pes(vec![0x14, 0x00, 0x01, 0xAA], Some(90000)));
assert_eq!(f.len(), 1);
assert!(f[0].keyframe);
assert_eq!(f[0].pts_ns, 1_000_000_000);
assert_eq!(f[0].duration_ns, None);
}
#[test]
fn display_pcs_data_preserved_verbatim() {
let mut parser = PgsParser::new();
let display = pcs_bytes(2); let _ = parser.parse(&make_pes(display.clone(), Some(90000)));
let f = parser.parse(&make_pes(pcs_bytes(0), Some(180000)));
assert_eq!(f[0].data, display, "display PCS data emitted verbatim");
}
}