Skip to main content

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}