ts_fix/lib.rs
1//! MPEG-2 TS repair / remux — container-layer operations, no codec parsing.
2//!
3//! `ts-fix` provides a **builder-driven streaming engine** that feeds 188-byte
4//! TS packets in and emits repaired packets out. Repair operations are opt-in
5//! via builder methods; the engine owns and enforces the canonical ordering.
6//!
7//! # Operations
8//!
9//! | Operation | Builder method | What it does |
10//! |---|---|---|
11//! | Continuity repair | [`repair_continuity`](TsFixBuilder::repair_continuity) | Renumber per-PID continuity counters (§2.4.3.3). |
12//! | PID filter / service extract | [`filter_pids`](TsFixBuilder::filter_pids) | Keep specified PIDs or extract a single programme by `program_number`. |
13//! | PAT/PMT regeneration | [`regen_psi`](TsFixBuilder::regen_psi) | Rebuild PAT from observed PMT PIDs on flush. |
14//! | PCR restamp | [`restamp_pcr`](TsFixBuilder::restamp_pcr) | Recompute PCR values on the PCR PID (§2.4.3.5). |
15//! | PCR-discontinuity honor | [`honor_pcr_discontinuity`](TsFixBuilder::honor_pcr_discontinuity) | Set `discontinuity_indicator` on genuine, unflagged PCR breaks (TR 101 290 §5.2.2 2.3b) without rewriting values. |
16//! | Stuffing | [`stuffing`](TsFixBuilder::stuffing) | Drop null packets or pad to a target packet rate. |
17//!
18//! # Forward compatibility
19//!
20//! The public API is designed so that adding a new repair operation in a future
21//! minor release is a **purely additive** change:
22//!
23//! - There is no public `enum Operation` (adding a variant is breaking).
24//! - There is no public `trait Operation` (locking the contract before all ops'
25//! needs are known).
26//! - Operations are exposed exclusively through [`TsFixBuilder`] methods.
27//! - All configuration and error enums are `#[non_exhaustive]`.
28//!
29//! # Quick start
30//!
31//! ```rust,no_run
32//! use ts_fix::{PcrRestamp, PidFilter, Stuffing, TsFix};
33//!
34//! let mut engine = TsFix::builder()
35//! .repair_continuity()
36//! .filter_pids(PidFilter::keep([0x0100, 0x0101]))
37//! .regen_psi()
38//! .restamp_pcr(PcrRestamp::interpolate())
39//! .stuffing(Stuffing::drop_nulls())
40//! .build()
41//! .unwrap();
42//! ```
43//!
44//! # Spec
45//!
46//! ISO/IEC 13818-1 (= ITU-T H.222.0) — §2.4.3.2 (TS packet), §2.4.3.3
47//! (adaptation field / continuity counter), §2.4.3.4 (PCR), §2.4.4 (PSI).
48
49#![cfg_attr(not(feature = "std"), no_std)]
50#![forbid(unsafe_code)]
51
52extern crate alloc;
53
54pub mod discontinuity;
55pub mod error;
56pub mod pes;
57
58mod engine;
59mod ops;
60
61use ops::OpKind;
62
63pub use error::Error;
64pub use ops::pcr_restamp::PcrRestamp;
65pub use ops::pid_filter::PidFilter;
66pub use ops::stuffing::Stuffing;
67
68/// A repair / remux engine for MPEG-2 TS byte streams.
69///
70/// Constructed via [`TsFix::builder`] → [`TsFixBuilder::build`].
71///
72/// Feed 188-byte TS packets one at a time with [`push`](TsFix::push); call
73/// [`finish`](TsFix::finish) at end-of-stream to flush any buffered state.
74pub struct TsFix {
75 engine: engine::Engine,
76}
77
78impl TsFix {
79 /// Create a new builder for configuring a [`TsFix`] engine.
80 pub fn builder() -> TsFixBuilder {
81 TsFixBuilder::new()
82 }
83
84 /// Feed one 188-byte TS packet into the engine.
85 ///
86 /// `out` is called once per emitted packet (may be called zero or more times
87 /// if an op suppresses or multiplies packets).
88 ///
89 /// Returns `Err` if `packet` is not exactly 188 bytes or lacks the `0x47`
90 /// sync byte (ISO/IEC 13818-1 §2.4.3.2).
91 pub fn push(&mut self, packet: &[u8], out: impl FnMut(&[u8])) -> Result<(), Error> {
92 self.engine.push(packet, out)
93 }
94
95 /// Flush any internally buffered state at end-of-stream.
96 ///
97 /// Must be called after the last [`push`](TsFix::push) to ensure that
98 /// buffering operations (e.g. PCR interpolation) emit their final packets.
99 pub fn finish(&mut self, out: impl FnMut(&[u8])) {
100 self.engine.finish(out);
101 }
102}
103
104/// Builder for [`TsFix`].
105///
106/// Each repair operation is opt-in via a dedicated method. Methods that
107/// correspond to later tasks are listed here for documentation purposes but will
108/// be implemented in subsequent releases — attempting to call them will cause a
109/// compile error until they ship.
110///
111/// # Forward-compat guarantee
112///
113/// Adding a new builder method in v0.2/v0.3 is an additive change. Callers who
114/// construct `TsFix::builder().build()?` (with no additional methods) will
115/// compile and behave identically across versions.
116pub struct TsFixBuilder {
117 /// Ops paired with their `OpKind` for canonical ordering at `build()` time.
118 ops: alloc::vec::Vec<(OpKind, ops::BoxedOp)>,
119}
120
121impl TsFixBuilder {
122 fn new() -> Self {
123 Self {
124 ops: alloc::vec::Vec::new(),
125 }
126 }
127
128 /// Build the configured engine.
129 ///
130 /// When no operations have been registered the engine is an **identity
131 /// pass-through**: every packet is emitted unchanged.
132 ///
133 /// The engine applies operations in the canonical ordering:
134 /// filter_pids → regen_psi → repair_continuity → restamp_pcr →
135 /// honor_pcr_discontinuity → stuffing. The `build()` method sorts ops by
136 /// this order regardless of the order in which builder methods were
137 /// called.
138 pub fn build(mut self) -> Result<TsFix, Error> {
139 // If no ops were configured, install the identity no-op so the engine
140 // always has something to call. This keeps `engine::Engine::push`
141 // simple and ensures zero-op builds are provably correct.
142 if self.ops.is_empty() {
143 self.ops = alloc::vec![(OpKind::Identity, alloc::boxed::Box::new(ops::IdentityOp))];
144 }
145
146 // Sort by canonical ordering, then discard the OpKind tag.
147 self.ops.sort_by_key(|(kind, _)| *kind);
148 let ops: alloc::vec::Vec<ops::BoxedOp> = self.ops.into_iter().map(|(_, op)| op).collect();
149
150 Ok(TsFix {
151 engine: engine::Engine::new(ops),
152 })
153 }
154
155 /// Enable continuity counter repair.
156 ///
157 /// Renumbers the 4-bit `continuity_counter` per PID to a correct monotonic
158 /// sequence (mod 16), respecting the ISO/IEC 13818-1 §2.4.3.3 rule that the
159 /// counter increments **only** on payload-bearing packets.
160 pub fn repair_continuity(mut self) -> Self {
161 self.ops.push((
162 OpKind::Continuity,
163 alloc::boxed::Box::new(ops::continuity::ContinuityOp::new()),
164 ));
165 self
166 }
167
168 /// Enable PID filtering / service extraction.
169 ///
170 /// Two modes:
171 ///
172 /// - [`PidFilter::keep`] — pass only packets whose PID is in the supplied
173 /// set. PAT PID 0x0000 is always implicitly included.
174 /// - [`PidFilter::service`] — observe the live PAT/PMT and keep exactly
175 /// the PIDs that belong to the given program_number
176 /// (PAT + PMT PID + PCR PID + all ES PIDs); everything else is dropped.
177 ///
178 /// # Example — extract service 1 from a multi-program mux
179 ///
180 /// ```rust,no_run
181 /// use ts_fix::{TsFix, PidFilter};
182 ///
183 /// let mut engine = TsFix::builder()
184 /// .filter_pids(PidFilter::service(1))
185 /// .build()
186 /// .unwrap();
187 /// ```
188 pub fn filter_pids(mut self, cfg: PidFilter) -> Self {
189 self.ops.push((
190 OpKind::PidFilter,
191 alloc::boxed::Box::new(ops::pid_filter::PidFilterOp::new(cfg)),
192 ));
193 self
194 }
195
196 /// Enable PAT/PMT regeneration.
197 ///
198 /// Rebuilds the Program Association Table (PAT) to be consistent with the
199 /// actual programs present in the stream output. This is particularly useful
200 /// after [`filter_pids`](Self::filter_pids) to ensure the PAT lists only the
201 /// programs that survived the filter.
202 ///
203 /// The engine observes PAT sections as packets pass through, collecting the
204 /// program → PMT PID mappings. On flush (end of stream), it emits a
205 /// freshly-generated PAT listing exactly the observed programs.
206 ///
207 /// # Example — filter to one service, then regenerate PAT
208 ///
209 /// ```rust,no_run
210 /// use ts_fix::{TsFix, PidFilter};
211 ///
212 /// let mut engine = TsFix::builder()
213 /// .filter_pids(PidFilter::service(1))
214 /// .regen_psi()
215 /// .build()
216 /// .unwrap();
217 /// ```
218 pub fn regen_psi(mut self) -> Self {
219 self.ops.push((
220 OpKind::PsiRegen,
221 alloc::boxed::Box::new(ops::psi_regen::PsiRegenOp::new()),
222 ));
223 self
224 }
225
226 /// Enable PCR restamping.
227 ///
228 /// Recomputes the 42-bit Program Clock Reference on the PCR PID using a
229 /// timing model (ISO/IEC 13818-1 §2.4.3.5). Two modes:
230 ///
231 /// - [`PcrRestamp::interpolate`] — interpolate PCRs between observed anchors.
232 /// - [`PcrRestamp::from_bitrate`] — recompute from a fixed bitrate.
233 ///
234 /// PCR values are written in-place via mpeg-ts editors; the adaptation field
235 /// layout is preserved.
236 ///
237 /// # Example — restore a plausible PCR timeline
238 ///
239 /// ```rust,no_run
240 /// use ts_fix::{TsFix, PcrRestamp};
241 ///
242 /// let mut engine = TsFix::builder()
243 /// .restamp_pcr(PcrRestamp::interpolate())
244 /// .build()
245 /// .unwrap();
246 /// ```
247 pub fn restamp_pcr(mut self, cfg: PcrRestamp) -> Self {
248 self.ops.push((
249 OpKind::PcrRestamp,
250 alloc::boxed::Box::new(ops::pcr_restamp::PcrRestampOp::new(cfg)),
251 ));
252 self
253 }
254
255 /// Enable PCR-discontinuity **honor** mode (#562).
256 ///
257 /// The alternative to [`restamp_pcr`](Self::restamp_pcr): leaves every
258 /// timestamp byte — including the PCR field itself — untouched, and
259 /// instead sets `discontinuity_indicator` (ISO/IEC 13818-1 §2.4.3.5) on
260 /// packets where a genuine, unflagged PCR break exists.
261 ///
262 /// "Genuine, unflagged" means the PCR delta exceeds the ETSI TR 101 290
263 /// v1.4.1 §5.2.2 Table 5.0b indicator 2.3b
264 /// (`PCR_discontinuity_indicator_error`) threshold with
265 /// `discontinuity_indicator == 0` — reused verbatim from
266 /// [`dvb_conformance::ConformanceMonitor`], never re-derived here. A
267 /// packet that already carries `discontinuity_indicator == 1` is a legal
268 /// break and is left alone.
269 ///
270 /// # Example — flag genuine PCR defects without rewriting values
271 ///
272 /// ```rust,no_run
273 /// use ts_fix::TsFix;
274 ///
275 /// let mut engine = TsFix::builder()
276 /// .honor_pcr_discontinuity()
277 /// .build()
278 /// .unwrap();
279 /// ```
280 pub fn honor_pcr_discontinuity(mut self) -> Self {
281 self.ops.push((
282 OpKind::PcrHonor,
283 alloc::boxed::Box::new(ops::pcr_honor::PcrHonorOp::new()),
284 ));
285 self
286 }
287
288 /// Enable null packet stuffing or drop.
289 ///
290 /// Two modes:
291 ///
292 /// - [`Stuffing::drop_nulls`] — strip all null packets (PID 0x1FFF)
293 /// from the output.
294 /// - [`Stuffing::pad_to`] — insert null packets to reach a target
295 /// packet rate (e.g. `pad_to(2.0)` doubles the output packet count).
296 ///
297 /// # Example — drop all null packets
298 ///
299 /// ```rust,no_run
300 /// use ts_fix::{TsFix, Stuffing};
301 ///
302 /// let mut engine = TsFix::builder()
303 /// .stuffing(Stuffing::drop_nulls())
304 /// .build()
305 /// .unwrap();
306 /// ```
307 pub fn stuffing(mut self, cfg: Stuffing) -> Self {
308 self.ops.push((
309 OpKind::Stuffing,
310 alloc::boxed::Box::new(ops::stuffing::StuffingOp::new(cfg)),
311 ));
312 self
313 }
314}