Skip to main content

dvb_t2mi/
inner_ts.rs

1//! Inner-TS recovery — the single driver from a T2-MI PID to the inner MPEG-TS.
2//!
3//! ETSI TS 102 773 carries a DVB-T2 modulator feed as T2-MI packets inside an
4//! outer MPEG-TS; the payload TS that a receiver ultimately decodes lives inside
5//! the BBFrames of those T2-MI packets (EN 302 755 §5.1, baseband framing).
6//! Recovering it is a fixed three-stage pipeline — [`T2miPump`] (PID filter +
7//! CRC-validated T2-MI packets) → [`AnyPayload::Bbframe`] → `dvb_bbframe`
8//! `Bbheader` + [`CarryOverExtractor`] (BBHEADER parse, mode handling, SYNCD
9//! carry-over across frames). [`InnerTsRecovery`] folds that whole chain into
10//! one feed-and-collect type so callers don't re-wire it.
11//!
12//! ```no_run
13//! # #[cfg(feature = "ts")] {
14//! use dvb_t2mi::inner_ts::InnerTsRecovery;
15//! let mut rec = InnerTsRecovery::new(0x1000); // the T2-MI PID
16//! for ts_packet in outer_stream() {            // 188-byte outer TS packets
17//!     for inner in rec.feed(&ts_packet) {       // recovered inner TS packets
18//!         feed_to_si_demux(inner);
19//!     }
20//! }
21//! # fn outer_stream() -> Vec<[u8; 188]> { vec![] }
22//! # fn feed_to_si_demux(_p: &[u8; 188]) {}
23//! # }
24//! ```
25
26use dvb_bbframe::header::{Bbheader, Mode, BBHEADER_LEN};
27use dvb_bbframe::packet::{CarryOverExtractor, NM_UP_SIZE};
28
29use crate::payload::AnyPayload;
30use crate::pump::{Stats, T2miPump};
31
32/// Recovers the inner MPEG-TS carried inside a T2-MI stream.
33///
34/// Feed outer 188-byte TS packets from the T2-MI PID with [`feed`](Self::feed);
35/// each call returns the inner TS packets recovered from that input packet
36/// (often empty — a BBFrame spans several T2-MI packets). The driver owns the
37/// pump, the carry-over extractor, and the NM/HEM mode handling.
38///
39/// Normal Mode and High-Efficiency Mode (without Null-Packet-Deletion) frames
40/// are recovered; HEM frames with `MATYPE.NPD` set are skipped (DNP-byte
41/// reinsertion is not modelled by the extractor) rather than mis-decoded.
42pub struct InnerTsRecovery {
43    pump: T2miPump,
44    extractor: CarryOverExtractor,
45    out: Vec<[u8; NM_UP_SIZE]>,
46    up_buf: Vec<[u8; NM_UP_SIZE]>,
47    target_plp: Option<u8>,
48    filtered_out: u64,
49}
50
51impl InnerTsRecovery {
52    /// Create a recovery driver filtering the outer TS for `t2mi_pid`.
53    /// All PLPs are unfiltered.
54    #[must_use]
55    pub fn new(t2mi_pid: u16) -> Self {
56        Self::build(t2mi_pid, None)
57    }
58
59    /// Create a recovery driver that only recovers inner TS from the given
60    /// baseband frame PLP (`plp_id`). BBFrames from other PLPs are counted
61    /// by [`filtered_bbframes`](Self::filtered_bbframes).
62    #[must_use]
63    pub fn new_for_plp(t2mi_pid: u16, plp_id: u8) -> Self {
64        Self::build(t2mi_pid, Some(plp_id))
65    }
66
67    fn build(t2mi_pid: u16, target_plp: Option<u8>) -> Self {
68        Self {
69            pump: T2miPump::new(t2mi_pid),
70            extractor: CarryOverExtractor::new(),
71            out: Vec::new(),
72            up_buf: Vec::new(),
73            target_plp,
74            filtered_out: 0,
75        }
76    }
77
78    /// Feed one outer 188-byte TS packet; returns the inner TS packets recovered
79    /// from it. The returned slice borrows an internal buffer that is cleared on
80    /// every call, so copy out anything you need to keep.
81    pub fn feed(&mut self, ts_packet: &[u8]) -> &[[u8; NM_UP_SIZE]] {
82        self.out.clear();
83        // Collect the pump's events first: `feed_ts` borrows `self.pump`, and the
84        // loop body mutates `self.extractor`/`self.up_buf`/`self.out`. T2miEvent
85        // owns its bytes, so collecting is cheap and releases the pump borrow.
86        let events: Vec<_> = self.pump.feed_ts(ts_packet).collect();
87        for event in events {
88            let Ok(AnyPayload::Bbframe(bb)) = event.payload() else {
89                continue;
90            };
91            if self.target_plp.is_some_and(|t| bb.plp_id != t) {
92                self.filtered_out += 1;
93                continue;
94            }
95            if bb.bbframe.len() < BBHEADER_LEN {
96                continue;
97            }
98            let Ok(hdr) = Bbheader::parse(bb.bbframe) else {
99                continue;
100            };
101            let header_bytes: [u8; BBHEADER_LEN] = match bb.bbframe[..BBHEADER_LEN].try_into() {
102                Ok(b) => b,
103                Err(_) => continue,
104            };
105            let data_field = &bb.bbframe[BBHEADER_LEN..];
106            match hdr.mode {
107                Mode::Normal => {
108                    self.extractor
109                        .feed_nm_into(&header_bytes, data_field, &mut self.up_buf);
110                }
111                Mode::HighEfficiency if !hdr.matype.npd => {
112                    self.extractor.feed_hem_into(
113                        &header_bytes,
114                        data_field,
115                        false,
116                        &mut self.up_buf,
117                    );
118                }
119                // HEM with NPD, or any future mode: skip (not recoverable here).
120                _ => continue,
121            }
122            self.out.append(&mut self.up_buf);
123        }
124        &self.out
125    }
126
127    /// Pump statistics (packets seen, CRC failures, …) — passthrough to the
128    /// underlying [`T2miPump::stats`].
129    #[must_use]
130    pub fn stats(&self) -> Stats {
131        self.pump.stats()
132    }
133
134    /// Number of BBFrames filtered out because their PLP did not match the
135    /// target set via [`new_for_plp`](Self::new_for_plp). Always zero when
136    /// constructed with [`new`](Self::new).
137    #[must_use]
138    pub fn filtered_bbframes(&self) -> u64 {
139        self.filtered_out
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use dvb_bbframe::crc::crc8;
147    use dvb_bbframe::header::{Matype, TsGs};
148    use dvb_common::crc32_mpeg2;
149
150    const TS_SYNC: u8 = 0x47;
151    const TS_LEN: usize = 188;
152
153    /// One inner TS packet: PID 0x0100, PUSI, all-0xAA payload (distinguishable).
154    fn inner_packet() -> [u8; TS_LEN] {
155        let mut p = [0xAAu8; TS_LEN];
156        p[0] = TS_SYNC;
157        p[1] = 0x41; // PUSI | PID hi = 0x0100
158        p[2] = 0x00;
159        p[3] = 0x10; // payload only
160        p
161    }
162
163    /// Wrap one inner TS packet in a Normal-Mode BBFrame (mirrors tests/chain.rs).
164    fn nm_bbframe(inner: &[u8; TS_LEN]) -> Vec<u8> {
165        let hdr = Bbheader {
166            matype: Matype {
167                ts_gs: TsGs::Ts,
168                sis: true,
169                ccm: true,
170                issyi: false,
171                npd: false,
172                ext: 0,
173                isi: 0,
174            },
175            upl: 1504,
176            sync: TS_SYNC,
177            dfl: 1504,
178            syncd: 0,
179            mode: Mode::Normal,
180            issy_in_header: None,
181        };
182        let mut frame = hdr.serialize().to_vec();
183        let mut data = [0u8; TS_LEN];
184        data[0] = crc8(&[0u8; TS_LEN]); // CRC-8 of the (all-zero) previous UP
185        data[1..].copy_from_slice(&inner[1..]);
186        frame.extend_from_slice(&data);
187        frame
188    }
189
190    /// Wrap a BBFrame in a T2-MI BBFrame packet (type 0x00) with CRC-32.
191    fn t2mi_packet(bbframe: &[u8]) -> Vec<u8> {
192        let mut payload = vec![0x00, 0x05, 0x80]; // frame_idx, plp_id, intl_frame_start
193        payload.extend_from_slice(bbframe);
194        let mut pkt = vec![0x00u8, 0x01, 0x00, 0x00];
195        pkt.extend_from_slice(&((payload.len() * 8) as u16).to_be_bytes());
196        pkt.extend_from_slice(&payload);
197        let crc = crc32_mpeg2::compute(&pkt);
198        pkt.extend_from_slice(&crc.to_be_bytes());
199        pkt
200    }
201
202    /// Wrap T2-MI data in outer TS packets on `pid` (PUSI + continuation).
203    fn outer_ts(pid: u16, data: &[u8]) -> Vec<[u8; TS_LEN]> {
204        let mut out = Vec::new();
205        let first_cap = TS_LEN - 5;
206        let cont_cap = TS_LEN - 4;
207        let mut off = 0;
208        let mut first = true;
209        while off < data.len() {
210            let mut pkt = [0xFFu8; TS_LEN];
211            pkt[0] = TS_SYNC;
212            let cap = if first { first_cap } else { cont_cap };
213            pkt[1] = (if first { 0x40 } else { 0x00 }) | (((pid >> 8) as u8) & 0x1F);
214            pkt[2] = (pid & 0xFF) as u8;
215            pkt[3] = 0x10;
216            let hdr_len = if first {
217                pkt[4] = 0x00; // pointer_field
218                5
219            } else {
220                4
221            };
222            let n = (data.len() - off).min(cap);
223            pkt[hdr_len..hdr_len + n].copy_from_slice(&data[off..off + n]);
224            out.push(pkt);
225            off += n;
226            first = false;
227        }
228        out
229    }
230
231    #[test]
232    fn recovers_inner_ts_from_nm_bbframe_chain() {
233        let pid = 0x1000;
234        let inner = inner_packet();
235        let outer = outer_ts(pid, &t2mi_packet(&nm_bbframe(&inner)));
236
237        let mut rec = InnerTsRecovery::new(pid);
238        let mut recovered: Vec<[u8; TS_LEN]> = Vec::new();
239        for pkt in &outer {
240            recovered.extend_from_slice(rec.feed(pkt));
241        }
242
243        assert_eq!(recovered.len(), 1, "exactly one inner TS packet expected");
244        assert_eq!(recovered[0][0], TS_SYNC, "sync byte restored");
245        // Bytes 1..188 survive the NM round-trip verbatim (byte 0 is re-synced).
246        assert_eq!(&recovered[0][1..], &inner[1..]);
247    }
248
249    #[test]
250    fn wrong_pid_yields_nothing() {
251        let inner = inner_packet();
252        let outer = outer_ts(0x1000, &t2mi_packet(&nm_bbframe(&inner)));
253        let mut rec = InnerTsRecovery::new(0x0064); // different PID
254        let mut n = 0;
255        for pkt in &outer {
256            n += rec.feed(pkt).len();
257        }
258        assert_eq!(n, 0);
259    }
260
261    #[test]
262    fn garbage_packet_no_panic_no_output() {
263        let mut rec = InnerTsRecovery::new(0x1000);
264        let junk = [0u8; TS_LEN];
265        assert!(rec.feed(&junk).is_empty());
266    }
267
268    /// Wrap a BBFrame in a T2-MI BBFrame packet with a chosen PLP.
269    fn t2mi_packet_for_plp(bbframe: &[u8], plp_id: u8) -> Vec<u8> {
270        let mut payload = vec![0x00, plp_id, 0x80]; // frame_idx=0, plp_id, intl_frame_start
271        payload.extend_from_slice(bbframe);
272        let mut pkt = vec![0x00u8, 0x01, 0x00, 0x00];
273        pkt.extend_from_slice(&((payload.len() * 8) as u16).to_be_bytes());
274        pkt.extend_from_slice(&payload);
275        let crc = crc32_mpeg2::compute(&pkt);
276        pkt.extend_from_slice(&crc.to_be_bytes());
277        pkt
278    }
279
280    /// A distinguishable inner TS packet with a marker byte at offset 4.
281    fn tagged_inner_packet(marker: u8) -> [u8; TS_LEN] {
282        let mut p = [0xAAu8; TS_LEN];
283        p[0] = TS_SYNC;
284        p[1] = 0x41;
285        p[2] = 0x00;
286        p[3] = 0x10;
287        p[4] = marker;
288        p
289    }
290
291    #[test]
292    fn plp_filter_keeps_only_target_plp() {
293        let pid = 0x1000;
294        let inner_plp0 = tagged_inner_packet(0xA0);
295        let inner_plp1 = tagged_inner_packet(0xB0);
296
297        // Build two BBFrames (one per PLP), each carrying one inner TS packet.
298        let bb_plp0 = nm_bbframe(&inner_plp0);
299        let bb_plp1 = nm_bbframe(&inner_plp1);
300
301        // Wrap each in a T2-MI packet with the correct PLP id.
302        let t2mi_plp0 = t2mi_packet_for_plp(&bb_plp0, 0);
303        let t2mi_plp1 = t2mi_packet_for_plp(&bb_plp1, 1);
304
305        // Interleave: PLP 0 then PLP 1 (two separate T2-MI packets).
306        let mut combined = t2mi_plp0;
307        combined.extend_from_slice(&t2mi_plp1);
308        let outer = outer_ts(pid, &combined);
309
310        // --- Test: new_for_plp(pid, 0) should only get plp 0 ---
311        let mut rec0 = InnerTsRecovery::new_for_plp(pid, 0);
312        let mut recovered_0: Vec<[u8; TS_LEN]> = Vec::new();
313        for pkt in &outer {
314            recovered_0.extend_from_slice(rec0.feed(pkt));
315        }
316        assert_eq!(
317            recovered_0.len(),
318            1,
319            "plp 0 filter should recover exactly one inner packet"
320        );
321        assert_eq!(recovered_0[0][4], 0xA0, "should be the plp 0 packet");
322        assert_eq!(
323            rec0.filtered_bbframes(),
324            1,
325            "one BBFRAME (plp 1) filtered out"
326        );
327
328        // --- Test: new_for_plp(pid, 1) should only get plp 1 ---
329        let mut rec1 = InnerTsRecovery::new_for_plp(pid, 1);
330        let mut recovered_1: Vec<[u8; TS_LEN]> = Vec::new();
331        for pkt in &outer {
332            recovered_1.extend_from_slice(rec1.feed(pkt));
333        }
334        assert_eq!(
335            recovered_1.len(),
336            1,
337            "plp 1 filter should recover exactly one inner packet"
338        );
339        assert_eq!(recovered_1[0][4], 0xB0, "should be the plp 1 packet");
340        assert_eq!(
341            rec1.filtered_bbframes(),
342            1,
343            "one BBFRAME (plp 0) filtered out"
344        );
345
346        // --- Test: new(pid) should get BOTH ---
347        let mut all = InnerTsRecovery::new(pid);
348        let mut recovered_all: Vec<[u8; TS_LEN]> = Vec::new();
349        for pkt in &outer {
350            recovered_all.extend_from_slice(all.feed(pkt));
351        }
352        assert_eq!(
353            recovered_all.len(),
354            2,
355            "unfiltered should recover both inner packets"
356        );
357        assert_eq!(
358            all.filtered_bbframes(),
359            0,
360            "no filtering when target is None"
361        );
362    }
363}