use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::vec::Vec;
use bytes::Bytes;
use mpeg_ts::pid::Pid;
use mpeg_ts::ts::{SectionReassembler, TsPacket};
const PAT_TABLE_ID: u8 = 0x00;
const TOT_TABLE_ID: u8 = 0x73;
const MIN_SECTION_LEN: usize = 3;
const LONG_FORM_EXTRA: usize = 5;
const CURRENT_NEXT_MASK: u8 = 0x01;
const KEY_CURRENT_NEXT_SHIFT: u32 = 45;
const KEY_EXT_SHIFT: u32 = 21;
const KEY_SECTION_NUMBER_SHIFT: u32 = 37;
const KEY_TABLE_ID_SHIFT: u32 = 13;
const SECTION_NUMBER_OFFSET: usize = 6;
const LAST_SECTION_NUMBER_OFFSET: usize = 7;
const CRC_LEN: usize = 4;
#[derive(Debug, Clone)]
pub struct SectionEvent {
pid: Pid,
bytes: Bytes,
}
impl SectionEvent {
#[must_use]
pub fn pid(&self) -> Pid {
self.pid
}
#[must_use]
pub fn bytes(&self) -> &Bytes {
&self.bytes
}
#[must_use]
pub fn table_id(&self) -> u8 {
self.bytes[0]
}
#[must_use]
fn is_long_form(&self) -> bool {
(self.bytes[1] & 0x80) != 0
}
#[must_use]
pub fn version(&self) -> Option<u8> {
if self.is_long_form() && self.bytes.len() > 5 {
Some((self.bytes[5] >> 1) & 0x1F)
} else {
None
}
}
#[must_use]
pub fn table_id_extension(&self) -> Option<u16> {
if self.is_long_form() && self.bytes.len() > 4 {
Some(((self.bytes[3] as u16) << 8) | self.bytes[4] as u16)
} else {
None
}
}
#[must_use]
pub fn section_number(&self) -> Option<u8> {
if self.is_long_form() && self.bytes.len() > 6 {
Some(self.bytes[SECTION_NUMBER_OFFSET])
} else {
None
}
}
#[must_use]
pub fn crc_ok(&self) -> bool {
true
}
pub fn table_section(&self) -> crate::Result<crate::tables::AnyTableSection<'_>> {
crate::tables::AnyTableSection::parse(&self.bytes)
}
pub fn parse<'s, T: crate::traits::TableDef<'s>>(&'s self) -> crate::Result<T> {
<T as broadcast_common::Parse>::parse(&self.bytes)
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Stats {
pub packets: u64,
pub sections_completed: u64,
pub emitted: u64,
pub suppressed: u64,
pub crc_failures: u64,
pub malformed_packets: u64,
pub gate_evictions: u64,
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct GateEntry {
version: u8,
crc: u32,
}
struct Config {
follow_pat: bool,
emit_repeats: bool,
gate_capacity: usize,
}
pub struct SiDemuxBuilder {
follow_pat: bool,
dvb_si_pids: bool,
emit_repeats: bool,
gate_capacity: usize,
extra_pids: Vec<Pid>,
}
impl Default for SiDemuxBuilder {
fn default() -> Self {
Self {
follow_pat: true,
dvb_si_pids: true,
emit_repeats: false,
gate_capacity: 65_536,
extra_pids: Vec::new(),
}
}
}
impl SiDemuxBuilder {
#[must_use]
pub fn follow_pat(mut self, on: bool) -> Self {
self.follow_pat = on;
self
}
#[must_use]
pub fn dvb_si_pids(mut self, on: bool) -> Self {
self.dvb_si_pids = on;
self
}
#[must_use]
pub fn pid(mut self, pid: Pid) -> Self {
self.extra_pids.push(pid);
self
}
#[must_use]
pub fn emit_repeats(mut self, on: bool) -> Self {
self.emit_repeats = on;
self
}
#[must_use]
pub fn gate_capacity(mut self, cap: usize) -> Self {
self.gate_capacity = cap;
self
}
#[must_use]
pub fn build(self) -> SiDemux {
let mut pids: BTreeMap<Pid, SectionReassembler> = BTreeMap::new();
let mut protected_pids: BTreeSet<Pid> = BTreeSet::new();
if self.dvb_si_pids {
use mpeg_ts::pid::well_known as wk;
for pid in [
wk::PAT,
wk::CAT,
wk::NIT,
wk::SDT_BAT,
wk::EIT,
wk::RST,
wk::TDT_TOT,
wk::SAT,
] {
pids.entry(pid).or_default();
protected_pids.insert(pid);
}
}
for p in self.extra_pids {
pids.entry(p).or_default();
protected_pids.insert(p);
}
SiDemux {
pids,
gate: BTreeMap::new(),
gate_order: VecDeque::new(),
cfg: Config {
follow_pat: self.follow_pat,
emit_repeats: self.emit_repeats,
gate_capacity: self.gate_capacity,
},
stats: Stats::default(),
scratch: Vec::new(),
completed_scratch: Vec::new(),
protected_pids,
pmt_pids: BTreeSet::new(),
pat_generation: None,
pat_sections: BTreeMap::new(),
}
}
}
pub struct SiDemux {
pids: BTreeMap<Pid, SectionReassembler>,
gate: BTreeMap<u64, GateEntry>,
gate_order: VecDeque<u64>,
cfg: Config,
stats: Stats,
scratch: Vec<SectionEvent>,
completed_scratch: Vec<Bytes>,
protected_pids: BTreeSet<Pid>,
pmt_pids: BTreeSet<Pid>,
pat_generation: Option<(u16, u8, bool)>,
pat_sections: BTreeMap<u8, BTreeSet<Pid>>,
}
impl SiDemux {
#[must_use]
pub fn builder() -> SiDemuxBuilder {
SiDemuxBuilder::default()
}
#[must_use]
pub fn stats(&self) -> Stats {
self.stats
}
pub fn feed(&mut self, packet: &[u8]) -> impl Iterator<Item = SectionEvent> + '_ {
self.scratch.clear();
self.stats.packets += 1;
match TsPacket::parse(packet) {
Err(_) => self.stats.malformed_packets += 1,
Ok(ts) => {
let pid = ts.header.pid;
let payload = ts.payload.unwrap_or(&[]);
let mut completed = core::mem::take(&mut self.completed_scratch);
if let Some(reasm) = self.pids.get_mut(&pid) {
match ts.payload {
Some(_) => reasm.feed_with_continuity(
payload,
ts.header.pusi,
ts.header.continuity_counter,
ts.adaptation_field()
.and_then(|r| r.ok())
.map(|af| af.discontinuity_indicator)
.unwrap_or(false),
),
None => reasm.feed(payload, ts.header.pusi),
}
while let Some(section) = reasm.pop_section() {
completed.push(section);
}
}
self.stats.sections_completed += completed.len() as u64;
for section in completed.drain(..) {
self.consider(pid, section);
}
self.completed_scratch = completed;
}
}
self.scratch.drain(..)
}
fn consider(&mut self, pid: Pid, section: Bytes) {
if section.len() < MIN_SECTION_LEN {
self.stats.crc_failures += 1;
return;
}
let table_id = section[0];
let long_form = (section[1] & 0x80) != 0;
let has_crc = long_form || table_id == TOT_TABLE_ID;
if has_crc {
if section.len() < CRC_LEN {
self.stats.crc_failures += 1;
return;
}
let (covered, crc_bytes) = section.split_last_chunk::<CRC_LEN>().unwrap();
let declared = u32::from_be_bytes(*crc_bytes);
let computed = broadcast_common::crc32_mpeg2::compute(covered);
if computed != declared {
self.stats.crc_failures += 1;
return;
}
}
let (ext, section_number, version, current, change_crc) =
if long_form && section.len() >= MIN_SECTION_LEN + LONG_FORM_EXTRA + CRC_LEN {
let ext = ((section[3] as u16) << 8) | section[4] as u16;
let version = (section[5] >> 1) & 0x1F;
let current = section[5] & CURRENT_NEXT_MASK != 0;
let section_number = section[6];
let (_, crc_bytes) = section.split_last_chunk::<CRC_LEN>().unwrap();
let crc = u32::from_be_bytes(*crc_bytes);
(ext, section_number, version, current, crc)
} else {
(
0u16,
0u8,
0u8,
true,
broadcast_common::crc32_mpeg2::compute(§ion),
)
};
let key = (pid.value() as u64)
| ((table_id as u64) << KEY_TABLE_ID_SHIFT)
| ((ext as u64) << KEY_EXT_SHIFT)
| ((section_number as u64) << KEY_SECTION_NUMBER_SHIFT)
| (u64::from(current) << KEY_CURRENT_NEXT_SHIFT);
let entry = GateEntry {
version,
crc: change_crc,
};
let changed = match self.gate.get(&key) {
Some(prev) => *prev != entry,
None => true,
};
let is_new = !self.gate.contains_key(&key);
if is_new
&& self.gate.len() >= self.cfg.gate_capacity
&& let Some(old) = self.gate_order.pop_front()
{
self.gate.remove(&old);
self.stats.gate_evictions += 1;
}
if is_new {
self.gate_order.push_back(key);
}
match self.gate.entry(key) {
alloc::collections::btree_map::Entry::Occupied(mut oe) => {
*oe.get_mut() = entry;
}
alloc::collections::btree_map::Entry::Vacant(ve) => {
ve.insert(entry);
}
}
if changed || self.cfg.emit_repeats {
let event = SectionEvent {
pid,
bytes: section,
};
if self.cfg.follow_pat
&& changed
&& current
&& table_id == PAT_TABLE_ID
&& pid == mpeg_ts::pid::well_known::PAT
{
self.follow_pat(&event);
}
self.stats.emitted += 1;
self.scratch.push(event);
} else {
self.stats.suppressed += 1;
}
}
fn follow_pat(&mut self, event: &SectionEvent) {
use crate::tables::pat::PatSection;
use broadcast_common::Parse;
let bytes = &event.bytes;
if bytes.len() < MIN_SECTION_LEN + LONG_FORM_EXTRA + CRC_LEN {
return;
}
let Ok(pat) = PatSection::parse(bytes) else {
return;
};
if !pat.current_next_indicator {
return;
}
let generation = (
pat.transport_stream_id,
pat.version_number,
pat.current_next_indicator,
);
if self.pat_generation != Some(generation) {
self.pat_generation = Some(generation);
self.pat_sections.clear();
}
let section_pids: BTreeSet<Pid> = pat
.entries
.iter()
.filter(|e| e.program_number != 0)
.map(|e| e.pid)
.collect();
for &pid in §ion_pids {
self.pids.entry(pid).or_default();
}
self.pat_sections
.insert(bytes[SECTION_NUMBER_OFFSET], section_pids);
let last_section = bytes[LAST_SECTION_NUMBER_OFFSET];
let complete = (0..=last_section).all(|n| self.pat_sections.contains_key(&n));
let mut union: BTreeSet<Pid> = self.pat_sections.values().flatten().copied().collect();
if complete {
for old_pid in self.pmt_pids.difference(&union) {
if !self.protected_pids.contains(old_pid) {
self.pids.remove(old_pid);
}
}
self.pmt_pids = union;
} else {
union.extend(self.pmt_pids.iter().copied());
self.pmt_pids = union;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mpeg_ts::ts::TS_PACKET_SIZE;
use test_fixtures::psi::{long_section, pat_section, pmt_section, ts_packet};
#[test]
fn pat_emits_once_suppresses_repeat_reemits_on_version_change() {
let mut demux = SiDemux::builder().build();
let pat_v0 = pat_section(0x0001, 0, &[(1, 0x0100)]);
let pat_v1 = pat_section(0x0001, 1, &[(1, 0x0100)]);
let pkt_v0 = ts_packet(0x0000, &pat_v0);
let pkt_v1 = ts_packet(0x0000, &pat_v1);
let n0: Vec<_> = demux.feed(&pkt_v0).collect();
assert_eq!(n0.len(), 1, "PAT v0 should emit one event");
assert_eq!(n0[0].table_id(), 0x00);
assert_eq!(n0[0].version(), Some(0));
let n1: Vec<_> = demux.feed(&pkt_v0).collect();
assert_eq!(n1.len(), 0, "repeat PAT should be suppressed");
let n2: Vec<_> = demux.feed(&pkt_v1).collect();
assert_eq!(n2.len(), 1, "PAT v1 should re-emit");
assert_eq!(n2[0].version(), Some(1));
let s = demux.stats();
assert_eq!(s.sections_completed, 3);
assert_eq!(s.emitted, 2);
assert_eq!(s.suppressed, 1);
assert_eq!(s.crc_failures, 0);
}
#[test]
fn follow_pat_registers_pmt_pid_and_emits_typed_pmt() {
use crate::tables::AnyTableSection;
let mut demux = SiDemux::builder().build();
let pat = pat_section(0x0001, 0, &[(1, 0x0100)]);
let pat_evts: Vec<_> = demux.feed(&ts_packet(0x0000, &pat)).collect();
assert_eq!(pat_evts.len(), 1);
let pmt = pmt_section(1, 0, 0x0100);
let pmt_evts: Vec<_> = demux.feed(&ts_packet(0x0100, &pmt)).collect();
assert_eq!(pmt_evts.len(), 1, "PMT on the followed PID should emit");
assert_eq!(pmt_evts[0].pid(), Pid::new(0x0100));
match pmt_evts[0].table_section().unwrap() {
AnyTableSection::PmtSection(p) => assert_eq!(p.program_number, 1),
other => panic!("expected PmtSection, got {other:?}"),
}
}
#[test]
fn follow_pat_stops_watching_pmt_pid_dropped_by_a_later_pat_version() {
use crate::tables::AnyTableSection;
let mut demux = SiDemux::builder().build();
let pat_v0 = pat_section(0x0001, 0, &[(1, 0x0100)]);
demux.feed(&ts_packet(0x0000, &pat_v0)).for_each(drop);
let pat_v1 = pat_section(0x0001, 1, &[(1, 0x0200)]);
demux.feed(&ts_packet(0x0000, &pat_v1)).for_each(drop);
let stale_pmt = pmt_section(1, 0, 0x0100);
let stale_evts: Vec<_> = demux.feed(&ts_packet(0x0100, &stale_pmt)).collect();
assert_eq!(stale_evts.len(), 0, "stale PMT PID must no longer emit");
let new_pmt = pmt_section(1, 0, 0x0200);
let new_evts: Vec<_> = demux.feed(&ts_packet(0x0200, &new_pmt)).collect();
assert_eq!(new_evts.len(), 1, "new PMT PID must emit");
match new_evts[0].table_section().unwrap() {
AnyTableSection::PmtSection(p) => assert_eq!(p.program_number, 1),
other => panic!("expected PmtSection, got {other:?}"),
}
}
#[test]
fn follow_pat_never_drops_a_protected_pid() {
let mut demux = SiDemux::builder().pid(Pid::new(0x0100)).build();
let pat_v0 = pat_section(0x0001, 0, &[(1, 0x0100)]);
demux.feed(&ts_packet(0x0000, &pat_v0)).for_each(drop);
let pat_v1 = pat_section(0x0001, 1, &[(1, 0x0200)]);
demux.feed(&ts_packet(0x0000, &pat_v1)).for_each(drop);
let pmt = pmt_section(1, 0, 0x0100);
let evts: Vec<_> = demux.feed(&ts_packet(0x0100, &pmt)).collect();
assert_eq!(evts.len(), 1, "explicitly-added PID must stay watched");
}
#[test]
fn corrupted_crc_sdt_dropped_and_counted() {
let mut demux = SiDemux::builder().build();
let mut sdt = long_section(0x42, 0x0001, 0, 0, &[0xDE, 0xAD, 0xBE, 0xEF]);
sdt[8] ^= 0xFF;
let evts: Vec<_> = demux.feed(&ts_packet(0x0011, &sdt)).collect();
assert_eq!(evts.len(), 0, "corrupted SDT must not emit");
let s = demux.stats();
assert_eq!(s.crc_failures, 1);
assert_eq!(s.emitted, 0);
assert_eq!(s.sections_completed, 1);
}
#[test]
fn gate_capacity_evicts_fifo_and_reemits() {
let mut demux = SiDemux::builder().gate_capacity(2).build();
let a = long_section(0x4E, 0x0001, 0, 0, &[0x01]);
let b = long_section(0x4E, 0x0002, 0, 0, &[0x02]);
let c = long_section(0x4E, 0x0003, 0, 0, &[0x03]);
assert_eq!(demux.feed(&ts_packet(0x0012, &a)).count(), 1);
assert_eq!(demux.feed(&ts_packet(0x0012, &b)).count(), 1);
assert_eq!(demux.feed(&ts_packet(0x0012, &c)).count(), 1);
assert_eq!(demux.stats().gate_evictions, 1);
assert_eq!(demux.feed(&ts_packet(0x0012, &a)).count(), 1);
}
#[test]
fn garbage_packet_counted_no_panic() {
let mut demux = SiDemux::builder().build();
let garbage = [0x00u8; TS_PACKET_SIZE]; let evts: Vec<_> = demux.feed(&garbage).collect();
assert_eq!(evts.len(), 0);
assert_eq!(demux.stats().malformed_packets, 1);
assert_eq!(demux.stats().packets, 1);
}
#[test]
fn emit_repeats_bypasses_suppression() {
let mut demux = SiDemux::builder().emit_repeats(true).build();
let pat = pat_section(0x0001, 0, &[(1, 0x0100)]);
let pkt = ts_packet(0x0000, &pat);
assert_eq!(demux.feed(&pkt).count(), 1);
assert_eq!(demux.feed(&pkt).count(), 1, "emit_repeats re-emits");
assert_eq!(demux.stats().suppressed, 0);
assert_eq!(demux.stats().emitted, 2);
}
#[test]
fn a_next_pat_does_not_change_the_followed_pmt_pids() {
use crate::tables::pat::{PatEntry, PatSection};
use broadcast_common::Serialize;
use mpeg_ts::pid::Pid;
fn pat_bytes(v: u8, cur: bool, sn: u8, last: u8, pid: u16) -> Vec<u8> {
let pat = PatSection::new(
0x1111,
v,
cur,
sn,
last,
vec![PatEntry::new(sn as u16 + 1, Pid::new(pid))],
);
let mut b = vec![0u8; pat.serialized_len()];
pat.serialize_into(&mut b).unwrap();
b
}
let mut d = SiDemux::builder().build();
d.feed(&ts_packet(0, &pat_bytes(0, true, 0, 1, 0x0100)))
.for_each(drop);
d.feed(&ts_packet(0, &pat_bytes(0, true, 1, 1, 0x0101)))
.for_each(drop);
d.feed(&ts_packet(0, &pat_bytes(1, false, 0, 0, 0x0300)))
.for_each(drop);
for &p in &[0x0100u16, 0x0101] {
let sec = pmt_section(1, 0, 0x0111);
let evts: Vec<_> = d.feed(&ts_packet(p, &sec)).collect();
assert_eq!(
evts.len(),
1,
"PMT 0x{:04X} should stay followed after next PAT",
p
);
}
}
}