use dvb_bbframe::header::{Bbheader, Mode, BBHEADER_LEN};
use dvb_bbframe::packet::{CarryOverExtractor, NM_UP_SIZE};
use crate::payload::AnyPayload;
use crate::pump::{Stats, T2miPump};
pub struct InnerTsRecovery {
pump: T2miPump,
extractor: CarryOverExtractor,
out: Vec<[u8; NM_UP_SIZE]>,
up_buf: Vec<[u8; NM_UP_SIZE]>,
target_plp: Option<u8>,
filtered_out: u64,
}
impl InnerTsRecovery {
#[must_use]
pub fn new(t2mi_pid: u16) -> Self {
Self::build(t2mi_pid, None)
}
#[must_use]
pub fn new_for_plp(t2mi_pid: u16, plp_id: u8) -> Self {
Self::build(t2mi_pid, Some(plp_id))
}
fn build(t2mi_pid: u16, target_plp: Option<u8>) -> Self {
Self {
pump: T2miPump::new(t2mi_pid),
extractor: CarryOverExtractor::new(),
out: Vec::new(),
up_buf: Vec::new(),
target_plp,
filtered_out: 0,
}
}
pub fn feed(&mut self, ts_packet: &[u8]) -> &[[u8; NM_UP_SIZE]] {
self.out.clear();
let events: Vec<_> = self.pump.feed_ts(ts_packet).collect();
for event in events {
let Ok(AnyPayload::Bbframe(bb)) = event.payload() else {
continue;
};
if self.target_plp.is_some_and(|t| bb.plp_id != t) {
self.filtered_out += 1;
continue;
}
if bb.bbframe.len() < BBHEADER_LEN {
continue;
}
let Ok(hdr) = Bbheader::parse(bb.bbframe) else {
continue;
};
let header_bytes: [u8; BBHEADER_LEN] = match bb.bbframe[..BBHEADER_LEN].try_into() {
Ok(b) => b,
Err(_) => continue,
};
let data_field = &bb.bbframe[BBHEADER_LEN..];
match hdr.mode {
Mode::Normal => {
self.extractor
.feed_nm_into(&header_bytes, data_field, &mut self.up_buf);
}
Mode::HighEfficiency if !hdr.matype.npd => {
self.extractor.feed_hem_into(
&header_bytes,
data_field,
false,
&mut self.up_buf,
);
}
_ => continue,
}
self.out.append(&mut self.up_buf);
}
&self.out
}
#[must_use]
pub fn stats(&self) -> Stats {
self.pump.stats()
}
#[must_use]
pub fn filtered_bbframes(&self) -> u64 {
self.filtered_out
}
}
#[cfg(test)]
mod tests {
use super::*;
use dvb_bbframe::crc::crc8;
use dvb_bbframe::header::{Matype, TsGs};
use dvb_common::crc32_mpeg2;
const TS_SYNC: u8 = 0x47;
const TS_LEN: usize = 188;
fn inner_packet() -> [u8; TS_LEN] {
let mut p = [0xAAu8; TS_LEN];
p[0] = TS_SYNC;
p[1] = 0x41; p[2] = 0x00;
p[3] = 0x10; p
}
fn nm_bbframe(inner: &[u8; TS_LEN]) -> Vec<u8> {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: false,
npd: false,
ext: 0,
isi: 0,
},
upl: 1504,
sync: TS_SYNC,
dfl: 1504,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let mut frame = hdr.serialize().to_vec();
let mut data = [0u8; TS_LEN];
data[0] = crc8(&[0u8; TS_LEN]); data[1..].copy_from_slice(&inner[1..]);
frame.extend_from_slice(&data);
frame
}
fn t2mi_packet(bbframe: &[u8]) -> Vec<u8> {
let mut payload = vec![0x00, 0x05, 0x80]; payload.extend_from_slice(bbframe);
let mut pkt = vec![0x00u8, 0x01, 0x00, 0x00];
pkt.extend_from_slice(&((payload.len() * 8) as u16).to_be_bytes());
pkt.extend_from_slice(&payload);
let crc = crc32_mpeg2::compute(&pkt);
pkt.extend_from_slice(&crc.to_be_bytes());
pkt
}
fn outer_ts(pid: u16, data: &[u8]) -> Vec<[u8; TS_LEN]> {
let mut out = Vec::new();
let first_cap = TS_LEN - 5;
let cont_cap = TS_LEN - 4;
let mut off = 0;
let mut first = true;
while off < data.len() {
let mut pkt = [0xFFu8; TS_LEN];
pkt[0] = TS_SYNC;
let cap = if first { first_cap } else { cont_cap };
pkt[1] = (if first { 0x40 } else { 0x00 }) | (((pid >> 8) as u8) & 0x1F);
pkt[2] = (pid & 0xFF) as u8;
pkt[3] = 0x10;
let hdr_len = if first {
pkt[4] = 0x00; 5
} else {
4
};
let n = (data.len() - off).min(cap);
pkt[hdr_len..hdr_len + n].copy_from_slice(&data[off..off + n]);
out.push(pkt);
off += n;
first = false;
}
out
}
#[test]
fn recovers_inner_ts_from_nm_bbframe_chain() {
let pid = 0x1000;
let inner = inner_packet();
let outer = outer_ts(pid, &t2mi_packet(&nm_bbframe(&inner)));
let mut rec = InnerTsRecovery::new(pid);
let mut recovered: Vec<[u8; TS_LEN]> = Vec::new();
for pkt in &outer {
recovered.extend_from_slice(rec.feed(pkt));
}
assert_eq!(recovered.len(), 1, "exactly one inner TS packet expected");
assert_eq!(recovered[0][0], TS_SYNC, "sync byte restored");
assert_eq!(&recovered[0][1..], &inner[1..]);
}
#[test]
fn wrong_pid_yields_nothing() {
let inner = inner_packet();
let outer = outer_ts(0x1000, &t2mi_packet(&nm_bbframe(&inner)));
let mut rec = InnerTsRecovery::new(0x0064); let mut n = 0;
for pkt in &outer {
n += rec.feed(pkt).len();
}
assert_eq!(n, 0);
}
#[test]
fn garbage_packet_no_panic_no_output() {
let mut rec = InnerTsRecovery::new(0x1000);
let junk = [0u8; TS_LEN];
assert!(rec.feed(&junk).is_empty());
}
fn t2mi_packet_for_plp(bbframe: &[u8], plp_id: u8) -> Vec<u8> {
let mut payload = vec![0x00, plp_id, 0x80]; payload.extend_from_slice(bbframe);
let mut pkt = vec![0x00u8, 0x01, 0x00, 0x00];
pkt.extend_from_slice(&((payload.len() * 8) as u16).to_be_bytes());
pkt.extend_from_slice(&payload);
let crc = crc32_mpeg2::compute(&pkt);
pkt.extend_from_slice(&crc.to_be_bytes());
pkt
}
fn tagged_inner_packet(marker: u8) -> [u8; TS_LEN] {
let mut p = [0xAAu8; TS_LEN];
p[0] = TS_SYNC;
p[1] = 0x41;
p[2] = 0x00;
p[3] = 0x10;
p[4] = marker;
p
}
#[test]
fn plp_filter_keeps_only_target_plp() {
let pid = 0x1000;
let inner_plp0 = tagged_inner_packet(0xA0);
let inner_plp1 = tagged_inner_packet(0xB0);
let bb_plp0 = nm_bbframe(&inner_plp0);
let bb_plp1 = nm_bbframe(&inner_plp1);
let t2mi_plp0 = t2mi_packet_for_plp(&bb_plp0, 0);
let t2mi_plp1 = t2mi_packet_for_plp(&bb_plp1, 1);
let mut combined = t2mi_plp0;
combined.extend_from_slice(&t2mi_plp1);
let outer = outer_ts(pid, &combined);
let mut rec0 = InnerTsRecovery::new_for_plp(pid, 0);
let mut recovered_0: Vec<[u8; TS_LEN]> = Vec::new();
for pkt in &outer {
recovered_0.extend_from_slice(rec0.feed(pkt));
}
assert_eq!(
recovered_0.len(),
1,
"plp 0 filter should recover exactly one inner packet"
);
assert_eq!(recovered_0[0][4], 0xA0, "should be the plp 0 packet");
assert_eq!(
rec0.filtered_bbframes(),
1,
"one BBFRAME (plp 1) filtered out"
);
let mut rec1 = InnerTsRecovery::new_for_plp(pid, 1);
let mut recovered_1: Vec<[u8; TS_LEN]> = Vec::new();
for pkt in &outer {
recovered_1.extend_from_slice(rec1.feed(pkt));
}
assert_eq!(
recovered_1.len(),
1,
"plp 1 filter should recover exactly one inner packet"
);
assert_eq!(recovered_1[0][4], 0xB0, "should be the plp 1 packet");
assert_eq!(
rec1.filtered_bbframes(),
1,
"one BBFRAME (plp 0) filtered out"
);
let mut all = InnerTsRecovery::new(pid);
let mut recovered_all: Vec<[u8; TS_LEN]> = Vec::new();
for pkt in &outer {
recovered_all.extend_from_slice(all.feed(pkt));
}
assert_eq!(
recovered_all.len(),
2,
"unfiltered should recover both inner packets"
);
assert_eq!(
all.filtered_bbframes(),
0,
"no filtering when target is None"
);
}
}