Skip to main content

ts_fix/ops/
pid_filter.rs

1//! PID filter / service extract operation.
2//!
3//! Filters a TS to a configured set of PIDs.  Two modes:
4//!
5//! - **Keep-set** ([`PidFilter::keep`]) — pass only packets whose PID is in
6//!   the caller-supplied set (PAT PID 0x0000 is always added automatically).
7//! - **Service extract** ([`PidFilter::service`]) — observe the PAT to find
8//!   the PMT PID for the requested program_number, then observe that PMT to
9//!   collect the PCR PID and all ES PIDs; keep exactly
10//!   `{0x0000, pmt_pid, pcr_pid, …es_pids}` and drop everything else.
11//!
12//! The op is **stateful**: in service-extract mode the keep-set is initially
13//! unknown.  While waiting for the PAT and PMT the op passes all PSI PIDs
14//! through unchanged (conservative: avoids dropping PAT/PMT packets that carry
15//! the metadata it needs), and buffers no non-PSI packets.
16//!
17//! # Spec
18//!
19//! ISO/IEC 13818-1 (= ITU-T H.222.0) §2.4.4.3 (PAT) / §2.4.4.8 (PMT).
20
21use alloc::collections::BTreeSet;
22
23use broadcast_common::traits::Parse;
24use dvb_si::tables::pat::PatSection;
25use dvb_si::tables::pmt::PmtSection;
26use mpeg_ts::ts::{SectionReassembler, TS_PACKET_SIZE, TsHeader, extract_ts_payload};
27
28use crate::ops::{Op, StreamModel};
29
30/// PAT well-known PID (ISO/IEC 13818-1 §2.4.4.3).
31const PAT_PID: u16 = 0x0000;
32/// Null-packet PID (ISO/IEC 13818-1 §2.4.1).
33const NULL_PID: u16 = 0x1FFF;
34
35/// Configuration for [`TsFixBuilder::filter_pids`](crate::TsFixBuilder::filter_pids).
36///
37/// `#[non_exhaustive]` — future modes may be added without a breaking change.
38#[non_exhaustive]
39#[derive(Debug, Clone)]
40pub enum PidFilter {
41    /// Keep only packets whose PID is in `pids` (PAT PID 0x0000 is always added).
42    ///
43    /// Constructed via [`PidFilter::keep`].
44    Keep {
45        /// The set of PIDs to retain.
46        pids: BTreeSet<u16>,
47    },
48
49    /// Extract one programme: resolve its PMT PID via the PAT, then keep
50    /// `{PAT, pmt_pid, pcr_pid, …es_pids}` and drop all other PIDs.
51    ///
52    /// Constructed via [`PidFilter::service`].
53    Service {
54        /// `program_number` to extract (as signalled in the PAT).
55        program_number: u16,
56    },
57}
58
59impl PidFilter {
60    /// Build a keep-set filter.
61    ///
62    /// PAT PID 0x0000 is always implicitly included regardless of the supplied
63    /// set — the PAT must be preserved for any downstream demuxer to work.
64    ///
65    /// # Example
66    /// ```
67    /// use ts_fix::PidFilter;
68    /// let cfg = PidFilter::keep([0x0101, 0x0102]);
69    /// ```
70    pub fn keep(pids: impl IntoIterator<Item = u16>) -> Self {
71        let mut set: BTreeSet<u16> = pids.into_iter().collect();
72        set.insert(PAT_PID);
73        Self::Keep { pids: set }
74    }
75
76    /// Build a service-extract filter.
77    ///
78    /// The engine will observe the live PAT/PMT to discover the program's PIDs
79    /// and then drop everything else.
80    ///
81    /// # Example
82    /// ```
83    /// use ts_fix::PidFilter;
84    /// let cfg = PidFilter::service(1);
85    /// ```
86    pub fn service(program_number: u16) -> Self {
87        Self::Service { program_number }
88    }
89}
90
91// ── Internal state machine ───────────────────────────────────────────────────
92
93/// State for service-extract mode.
94///
95/// PSI sections (PAT, PMT) are reassembled with the canonical
96/// [`mpeg_ts::ts::SectionReassembler`] rather than a bespoke buffer — it
97/// handles pointer_field, multi-packet sections, and multiple sections per
98/// payload correctly and is the better-tested code path.
99enum ServiceState {
100    /// Waiting to see the PAT; we know which program_number we want.
101    WaitingPat {
102        program_number: u16,
103        /// Reassembles PAT sections on PID 0x0000.
104        pat_reasm: SectionReassembler,
105    },
106    /// PAT seen; waiting for the PMT on `pmt_pid`.
107    WaitingPmt {
108        pmt_pid: u16,
109        /// Reassembles PMT sections on `pmt_pid`.
110        pmt_reasm: SectionReassembler,
111    },
112    /// PMT seen; keep-set fully resolved.
113    Resolved { keep: BTreeSet<u16> },
114}
115
116/// Extract `(payload, pusi)` from a raw 188-byte packet, or `None` if it has
117/// no payload. Payload extraction defers to [`mpeg_ts::ts::extract_ts_payload`]
118/// (handles the adaptation-field offset); PUSI comes from the parsed header.
119fn ts_payload_and_pusi(packet: &[u8]) -> Option<(&[u8], bool)> {
120    let header = TsHeader::parse(&packet[..4]).ok()?;
121    let payload = extract_ts_payload(packet)?;
122    Some((payload, header.pusi))
123}
124
125// ── The operation ────────────────────────────────────────────────────────────
126
127/// PID filter / service-extract operation.
128pub(crate) struct PidFilterOp {
129    /// Current filter state.
130    state: FilterState,
131}
132
133enum FilterState {
134    /// Keep exactly this set of PIDs.
135    KeepSet(BTreeSet<u16>),
136    /// Service extract — stateful.
137    Service(ServiceState),
138}
139
140impl PidFilterOp {
141    pub(crate) fn new(cfg: PidFilter) -> Self {
142        let state = match cfg {
143            PidFilter::Keep { pids } => FilterState::KeepSet(pids),
144            PidFilter::Service { program_number } => {
145                FilterState::Service(ServiceState::WaitingPat {
146                    program_number,
147                    pat_reasm: SectionReassembler::default(),
148                })
149            }
150        };
151        Self { state }
152    }
153
154    /// Decide whether a packet on `pid` should pass the filter.
155    fn should_keep(&self, pid: u16) -> bool {
156        match &self.state {
157            FilterState::KeepSet(set) => set.contains(&pid),
158            FilterState::Service(svc_state) => match svc_state {
159                ServiceState::WaitingPat { .. } => {
160                    // Before PAT seen: only let PAT through.
161                    pid == PAT_PID
162                }
163                ServiceState::WaitingPmt { pmt_pid, .. } => {
164                    // PAT seen but PMT not yet: let PAT + target PMT PID through.
165                    pid == PAT_PID || pid == *pmt_pid
166                }
167                ServiceState::Resolved { keep } => keep.contains(&pid),
168            },
169        }
170    }
171
172    /// Observe a packet and potentially advance the service-extract state machine.
173    fn observe(&mut self, packet: &[u8]) {
174        let state = match &mut self.state {
175            FilterState::KeepSet(_) => return,
176            FilterState::Service(s) => s,
177        };
178
179        match state {
180            ServiceState::WaitingPat {
181                program_number,
182                pat_reasm,
183            } => {
184                // Listen on PID 0x0000 for the PAT.
185                let pid = (((packet[1] & 0x1F) as u16) << 8) | packet[2] as u16;
186                if pid != PAT_PID {
187                    return;
188                }
189                let Some((payload, pusi)) = ts_payload_and_pusi(packet) else {
190                    return;
191                };
192                pat_reasm.feed(payload, pusi);
193
194                let pn = *program_number;
195                while let Some(section) = pat_reasm.pop_section() {
196                    let Ok(pat) = PatSection::parse(&section) else {
197                        continue;
198                    };
199                    // Find the PMT PID for our program_number.
200                    if let Some(entry) = pat.entries.iter().find(|e| e.program_number == pn) {
201                        let pmt_pid = entry.pid;
202                        *state = ServiceState::WaitingPmt {
203                            pmt_pid,
204                            pmt_reasm: SectionReassembler::default(),
205                        };
206                        return;
207                    }
208                }
209            }
210
211            ServiceState::WaitingPmt { pmt_pid, pmt_reasm } => {
212                let pid = (((packet[1] & 0x1F) as u16) << 8) | packet[2] as u16;
213                if pid != *pmt_pid {
214                    return;
215                }
216                let Some((payload, pusi)) = ts_payload_and_pusi(packet) else {
217                    return;
218                };
219                pmt_reasm.feed(payload, pusi);
220
221                let pmt_pid = *pmt_pid;
222                while let Some(section) = pmt_reasm.pop_section() {
223                    let Ok(pmt) = PmtSection::parse(&section) else {
224                        continue;
225                    };
226                    // Resolve the keep-set.
227                    let mut keep = BTreeSet::new();
228                    keep.insert(PAT_PID);
229                    keep.insert(pmt_pid);
230                    keep.insert(pmt.pcr_pid);
231                    for stream in &pmt.streams {
232                        keep.insert(stream.elementary_pid);
233                    }
234                    *state = ServiceState::Resolved { keep };
235                    return;
236                }
237            }
238
239            ServiceState::Resolved { .. } => {
240                // Nothing more to observe.
241            }
242        }
243    }
244}
245
246impl Op for PidFilterOp {
247    fn process(&mut self, packet: &[u8], _model: &mut StreamModel, out: &mut dyn FnMut(&[u8])) {
248        if packet.len() != TS_PACKET_SIZE {
249            // Should not happen (engine validated), but be safe.
250            out(packet);
251            return;
252        }
253
254        // Extract PID before potential state mutation.
255        let pid = (((packet[1] & 0x1F) as u16) << 8) | packet[2] as u16;
256
257        // Always skip null packets.
258        if pid == NULL_PID {
259            return;
260        }
261
262        // Advance the service-extract state machine by observing this packet.
263        self.observe(packet);
264
265        if self.should_keep(pid) {
266            out(packet);
267        }
268    }
269
270    fn flush(&mut self, _model: &mut StreamModel, _out: &mut dyn FnMut(&[u8])) {
271        // Nothing buffered.
272    }
273}