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