dvb-t2mi 6.0.0

ETSI TS 102 773 DVB-T2 Modulator Interface parser + builder.
Documentation
//! Inner-TS recovery — the single driver from a T2-MI PID to the inner MPEG-TS.
//!
//! ETSI TS 102 773 carries a DVB-T2 modulator feed as T2-MI packets inside an
//! outer MPEG-TS; the payload TS that a receiver ultimately decodes lives inside
//! the BBFrames of those T2-MI packets (EN 302 755 §5.1, baseband framing).
//! Recovering it is a fixed three-stage pipeline — [`T2miPump`] (PID filter +
//! CRC-validated T2-MI packets) → [`AnyPayload::Bbframe`] → `dvb_bbframe`
//! `Bbheader` + [`CarryOverExtractor`] (BBHEADER parse, mode handling, SYNCD
//! carry-over across frames). [`InnerTsRecovery`] folds that whole chain into
//! one feed-and-collect type so callers don't re-wire it.
//!
//! ```no_run
//! # #[cfg(feature = "ts")] {
//! use dvb_t2mi::inner_ts::InnerTsRecovery;
//! let mut rec = InnerTsRecovery::new(0x1000); // the T2-MI PID
//! for ts_packet in outer_stream() {            // 188-byte outer TS packets
//!     for inner in rec.feed(&ts_packet) {       // recovered inner TS packets
//!         feed_to_si_demux(inner);
//!     }
//! }
//! # fn outer_stream() -> Vec<[u8; 188]> { vec![] }
//! # fn feed_to_si_demux(_p: &[u8; 188]) {}
//! # }
//! ```

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};

/// Recovers the inner MPEG-TS carried inside a T2-MI stream.
///
/// Feed outer 188-byte TS packets from the T2-MI PID with [`feed`](Self::feed);
/// each call returns the inner TS packets recovered from that input packet
/// (often empty — a BBFrame spans several T2-MI packets). The driver owns the
/// pump, the carry-over extractor, and the NM/HEM mode handling.
///
/// Normal Mode and High-Efficiency Mode (without Null-Packet-Deletion) frames
/// are recovered; HEM frames with `MATYPE.NPD` set are skipped (DNP-byte
/// reinsertion is not modelled by the extractor) rather than mis-decoded.
pub struct InnerTsRecovery {
    pump: T2miPump,
    extractor: CarryOverExtractor,
    out: Vec<[u8; NM_UP_SIZE]>,
    up_buf: Vec<[u8; NM_UP_SIZE]>,
}

impl InnerTsRecovery {
    /// Create a recovery driver filtering the outer TS for `t2mi_pid`.
    #[must_use]
    pub fn new(t2mi_pid: u16) -> Self {
        Self {
            pump: T2miPump::new(t2mi_pid),
            extractor: CarryOverExtractor::new(),
            out: Vec::new(),
            up_buf: Vec::new(),
        }
    }

    /// Feed one outer 188-byte TS packet; returns the inner TS packets recovered
    /// from it. The returned slice borrows an internal buffer that is cleared on
    /// every call, so copy out anything you need to keep.
    pub fn feed(&mut self, ts_packet: &[u8]) -> &[[u8; NM_UP_SIZE]] {
        self.out.clear();
        // Collect the pump's events first: `feed_ts` borrows `self.pump`, and the
        // loop body mutates `self.extractor`/`self.up_buf`/`self.out`. T2miEvent
        // owns its bytes, so collecting is cheap and releases the pump borrow.
        let events: Vec<_> = self.pump.feed_ts(ts_packet).collect();
        for event in events {
            let Ok(AnyPayload::Bbframe(bb)) = event.payload() else {
                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,
                    );
                }
                // HEM with NPD, or any future mode: skip (not recoverable here).
                _ => continue,
            }
            self.out.append(&mut self.up_buf);
        }
        &self.out
    }

    /// Pump statistics (packets seen, CRC failures, …) — passthrough to the
    /// underlying [`T2miPump::stats`].
    #[must_use]
    pub fn stats(&self) -> Stats {
        self.pump.stats()
    }
}

#[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;

    /// One inner TS packet: PID 0x0100, PUSI, all-0xAA payload (distinguishable).
    fn inner_packet() -> [u8; TS_LEN] {
        let mut p = [0xAAu8; TS_LEN];
        p[0] = TS_SYNC;
        p[1] = 0x41; // PUSI | PID hi = 0x0100
        p[2] = 0x00;
        p[3] = 0x10; // payload only
        p
    }

    /// Wrap one inner TS packet in a Normal-Mode BBFrame (mirrors tests/chain.rs).
    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]); // CRC-8 of the (all-zero) previous UP
        data[1..].copy_from_slice(&inner[1..]);
        frame.extend_from_slice(&data);
        frame
    }

    /// Wrap a BBFrame in a T2-MI BBFrame packet (type 0x00) with CRC-32.
    fn t2mi_packet(bbframe: &[u8]) -> Vec<u8> {
        let mut payload = vec![0x00, 0x05, 0x80]; // frame_idx, plp_id, intl_frame_start
        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
    }

    /// Wrap T2-MI data in outer TS packets on `pid` (PUSI + continuation).
    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; // pointer_field
                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");
        // Bytes 1..188 survive the NM round-trip verbatim (byte 0 is re-synced).
        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); // different PID
        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());
    }
}