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(§ion) 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(§ion) 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}