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}