mpeg_ts/resync.rs
1//! Stateful TS byte-stream resynchroniser — ITU-T H.222.0 §2.4.3.2 (= ISO/IEC 13818-1 §2.4.3.2).
2//!
3//! Recovers 188-byte MPEG-TS packet alignment from an arbitrary byte stream
4//! (file reads, UDP payloads) that may start mid-packet or contain leading
5//! garbage. Also detects 204-byte Reed-Solomon-coded packets (DVB RS-coded
6//! outer forward-error-correction layer) and strips the 16 parity bytes,
7//! yielding standard 188-byte TS packets in both cases.
8//!
9//! # Feature gate
10//!
11//! This module is only compiled when the `ts` feature is enabled (the
12//! default), because it depends on the TS constants in [`crate::ts`].
13//!
14//! # Example
15//!
16//! ```
17//! use mpeg_ts::resync::TsResync;
18//!
19//! let mut r = TsResync::new();
20//! // Feed arbitrary bytes (file chunks, UDP datagrams, etc.).
21//! let packets: Vec<[u8; 188]> = r.feed(b"some raw bytes");
22//! let stats = r.stats();
23//! ```
24
25use alloc::vec::Vec;
26
27use crate::ts::{TS_PACKET_SIZE, TS_SYNC_BYTE};
28
29/// Reed-Solomon-coded TS packet size: 188-byte payload + 16 parity bytes
30/// (DVB RS outer FEC, ISO/IEC 13818-1 §2.4.3.2 informative note).
31pub const RS_PACKET_SIZE: usize = 204;
32
33/// Number of Reed-Solomon parity bytes appended to a 204-byte packet.
34pub const RS_PARITY_LEN: usize = RS_PACKET_SIZE - TS_PACKET_SIZE;
35
36/// Consecutive sync bytes at the candidate stride required to declare lock.
37pub const LOCK_CONFIRMATIONS: usize = 5;
38
39/// Detected packet size after locking.
40#[non_exhaustive]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize))]
43pub enum PacketStride {
44 /// Standard 188-byte TS packets (ISO/IEC 13818-1 §2.4.3.2).
45 Ts188,
46 /// 204-byte packets (188-byte TS + 16 Reed-Solomon parity bytes).
47 Rs204,
48}
49
50impl PacketStride {
51 /// Short machine-readable label for this stride.
52 pub fn name(&self) -> &'static str {
53 match self {
54 PacketStride::Ts188 => "ts_188",
55 PacketStride::Rs204 => "rs_204",
56 }
57 }
58}
59
60broadcast_common::impl_spec_display!(PacketStride);
61
62/// Counters accumulated during resynchronisation.
63#[non_exhaustive]
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub struct ResyncStats {
67 /// Total 188-byte TS packets emitted.
68 pub packets: u64,
69 /// Times sync was lost and reacquired.
70 pub resyncs: u64,
71 /// Bytes skipped/dropped (junk before lock + sync-loss bytes).
72 pub dropped_bytes: u64,
73}
74
75/// Stateful TS byte-stream resynchroniser (ISO/IEC 13818-1 §2.4.3.2).
76///
77/// Recovers 188-byte MPEG-TS packet alignment from an arbitrary byte stream
78/// that may start mid-packet or contain junk, and detects 204-byte
79/// Reed-Solomon-coded packets (stripping the 16 parity bytes).
80///
81/// Feed raw bytes with [`feed`](Self::feed); each call returns a `Vec` of
82/// aligned 188-byte TS packets. Bytes are buffered across calls so that
83/// packet boundaries may span call boundaries freely.
84///
85/// Lock is declared after [`LOCK_CONFIRMATIONS`] consecutive sync bytes are
86/// found at the candidate stride (188 or 204). On sync loss the resynchroniser
87/// re-scans from the byte after the lost position.
88///
89/// # Stats
90///
91/// [`stats`](Self::stats) returns cumulative counters (packets emitted,
92/// resyncs, dropped bytes).
93#[derive(Debug, Default)]
94pub struct TsResync {
95 buf: Vec<u8>,
96 /// Logical read head into `buf`; compacted periodically.
97 head: usize,
98 stride: Option<PacketStride>,
99 stats: ResyncStats,
100}
101
102impl TsResync {
103 /// Create a new resynchroniser with an empty internal buffer.
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 /// Feed `data` and emit every newly-aligned 188-byte TS packet.
109 ///
110 /// For a 204-byte stream the 16 Reed-Solomon parity bytes are stripped;
111 /// only the 188-byte TS payload is returned. Bytes that cannot yet form
112 /// a complete packet (or fall before lock) are buffered for the next call.
113 pub fn feed(&mut self, data: &[u8]) -> Vec<[u8; TS_PACKET_SIZE]> {
114 let mut emitted = Vec::new();
115 self.feed_into(data, &mut emitted);
116 emitted
117 }
118
119 /// [`feed`](Self::feed) appending to a caller-owned `out` (not cleared),
120 /// so a steady-state caller that reuses one `Vec` allocates nothing per
121 /// call (audit r01-O4). The output capacity is reserved once from the
122 /// buffered byte count instead of growing by doubling.
123 ///
124 /// Returns the number of packets appended.
125 pub fn feed_into(&mut self, data: &[u8], emitted: &mut Vec<[u8; TS_PACKET_SIZE]>) -> usize {
126 self.buf.extend_from_slice(data);
127 let before = emitted.len();
128 emitted.reserve((self.buf.len() - self.head) / TS_PACKET_SIZE);
129
130 loop {
131 match self.stride {
132 None => {
133 if let Some((offset, s)) = find_sync(&self.buf[self.head..]) {
134 self.stats.dropped_bytes += offset as u64;
135 self.head += offset;
136 self.stride = Some(s);
137 } else {
138 // Keep at most enough bytes to detect a future lock.
139 let keep = LOCK_CONFIRMATIONS * RS_PACKET_SIZE;
140 let tail_len = self.buf.len() - self.head;
141 if tail_len > keep {
142 let excess = tail_len - keep;
143 self.stats.dropped_bytes += excess as u64;
144 self.head += excess;
145 }
146 self.compact();
147 return emitted.len() - before;
148 }
149 }
150 Some(stride) => {
151 let s = match stride {
152 PacketStride::Ts188 => TS_PACKET_SIZE,
153 PacketStride::Rs204 => RS_PACKET_SIZE,
154 };
155 let tail_len = self.buf.len() - self.head;
156 if tail_len < s {
157 self.compact();
158 return emitted.len() - before;
159 }
160 if self.buf[self.head] == TS_SYNC_BYTE {
161 let mut packet = [0u8; TS_PACKET_SIZE];
162 packet.copy_from_slice(&self.buf[self.head..self.head + TS_PACKET_SIZE]);
163 emitted.push(packet);
164 self.head += s;
165 self.stats.packets += 1;
166 } else {
167 // Sync byte missing — record the loss and re-scan.
168 self.stats.resyncs += 1;
169 self.stats.dropped_bytes += 1;
170 self.head += 1;
171 self.stride = None;
172 }
173 }
174 }
175 }
176 }
177
178 /// Detected packet stride, or [`None`] before the stream has locked.
179 pub fn stride(&self) -> Option<PacketStride> {
180 self.stride
181 }
182
183 /// Accumulated statistics.
184 pub fn stats(&self) -> ResyncStats {
185 self.stats
186 }
187
188 /// Compact the internal buffer by discarding consumed bytes.
189 fn compact(&mut self) {
190 if self.head > 0 {
191 self.buf.drain(..self.head);
192 self.head = 0;
193 }
194 }
195}
196
197/// Scan `buf` for the smallest offset `o` such that stride `S` (tried 188
198/// first, then 204) yields [`LOCK_CONFIRMATIONS`] consecutive sync bytes at
199/// positions `o + k*S` for `k = 0 .. LOCK_CONFIRMATIONS`.
200///
201/// Returns `(offset, stride)` on success, or `None` if no lock is found.
202fn find_sync(buf: &[u8]) -> Option<(usize, PacketStride)> {
203 for o in 0..buf.len() {
204 if buf[o] != TS_SYNC_BYTE {
205 continue;
206 }
207 if try_stride(buf, o, TS_PACKET_SIZE) {
208 return Some((o, PacketStride::Ts188));
209 }
210 if try_stride(buf, o, RS_PACKET_SIZE) {
211 return Some((o, PacketStride::Rs204));
212 }
213 }
214 None
215}
216
217/// Return `true` if `LOCK_CONFIRMATIONS` consecutive sync bytes exist at
218/// stride `s` starting from `offset` (the first is already known to be
219/// [`TS_SYNC_BYTE`]).
220fn try_stride(buf: &[u8], offset: usize, s: usize) -> bool {
221 for k in 1..LOCK_CONFIRMATIONS {
222 let pos = offset + k * s;
223 if pos >= buf.len() || buf[pos] != TS_SYNC_BYTE {
224 return false;
225 }
226 }
227 true
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use alloc::vec;
234 use alloc::vec::Vec;
235 extern crate std;
236
237 /// Build a 188-byte TS packet starting with `TS_SYNC_BYTE` followed by
238 /// `tag` (repeated). All non-sync bytes are kept away from `0x47` so
239 /// that no false lock occurs in the fixture data.
240 fn ts_packet(tag: u8) -> [u8; TS_PACKET_SIZE] {
241 assert_ne!(tag, TS_SYNC_BYTE, "tag must not equal sync byte");
242 let mut pkt = [tag; TS_PACKET_SIZE];
243 pkt[0] = TS_SYNC_BYTE;
244 pkt
245 }
246
247 /// Build a 204-byte RS-coded packet: 188-byte TS payload (with `ts_tag`)
248 /// plus 16 parity bytes filled with `parity` (must not be `0x47`).
249 fn rs_packet(ts_tag: u8, parity: u8) -> [u8; RS_PACKET_SIZE] {
250 assert_ne!(ts_tag, TS_SYNC_BYTE);
251 assert_ne!(parity, TS_SYNC_BYTE);
252 let mut pkt = [parity; RS_PACKET_SIZE];
253 pkt[0] = TS_SYNC_BYTE;
254 pkt[1..TS_PACKET_SIZE].fill(ts_tag);
255 pkt
256 }
257
258 /// Concatenate several 188-byte packet arrays into a flat `Vec<u8>`.
259 fn concat_ts(packets: &[[u8; TS_PACKET_SIZE]]) -> Vec<u8> {
260 let mut v = Vec::with_capacity(packets.len() * TS_PACKET_SIZE);
261 for p in packets {
262 v.extend_from_slice(p);
263 }
264 v
265 }
266
267 // ------------------------------------------------------------------
268 // Test 1 — aligned 188-byte passthrough
269 // ------------------------------------------------------------------
270 #[test]
271 fn aligned_188_passthrough() {
272 let packets: Vec<_> = (0u8..5).map(|i| ts_packet(i + 1)).collect();
273 let data = concat_ts(&packets);
274
275 let mut r = TsResync::new();
276 let emitted = r.feed(&data);
277
278 assert_eq!(emitted.len(), 5);
279 for (i, (e, p)) in emitted.iter().zip(packets.iter()).enumerate() {
280 assert_eq!(e, p, "packet {i} mismatch");
281 }
282 assert_eq!(r.stride(), Some(PacketStride::Ts188));
283 let s = r.stats();
284 assert_eq!(s.packets, 5);
285 assert_eq!(s.resyncs, 0);
286 assert_eq!(s.dropped_bytes, 0);
287 }
288
289 // ------------------------------------------------------------------
290 // Test 2 — lock only after LOCK_CONFIRMATIONS sync bytes
291 // ------------------------------------------------------------------
292 #[test]
293 fn requires_n_confirmations_before_lock() {
294 // Pin the default lock window. The hardcoded 4-vs-5 boundary below only
295 // bites the threshold when this is 5 — assert it explicitly so a change
296 // to the default (or to this test's literals) cannot silently pass.
297 assert_eq!(
298 LOCK_CONFIRMATIONS, 5,
299 "this test pins the default lock window of 5"
300 );
301
302 // FOUR confirming, stride-aligned packets are one short of the window:
303 // only four sync bytes sit at the 188-stride boundaries, so the
304 // resynchroniser must NOT lock and must emit nothing (it buffers).
305 let four = concat_ts(&(1u8..=4).map(ts_packet).collect::<Vec<_>>());
306 let mut r = TsResync::new();
307 assert_eq!(
308 r.feed(&four).len(),
309 0,
310 "4 confirmations (< 5) must not lock or emit"
311 );
312 assert_eq!(r.stride(), None, "stride must remain None below the window");
313
314 // The FIFTH stride-aligned sync byte completes the window → lock.
315 let mut out = r.feed(&ts_packet(5));
316 out.extend(r.feed(&[]));
317 assert!(
318 r.stride().is_some(),
319 "the 5th confirmation must trigger lock"
320 );
321 // Once locked, the buffered packets are emitted (5 fed, all aligned).
322 assert_eq!(out.len(), 5, "all five aligned packets emit once locked");
323 }
324
325 // ------------------------------------------------------------------
326 // Test 3 — junk prefix: leading garbage dropped, correct count returned
327 // ------------------------------------------------------------------
328 #[test]
329 fn junk_prefix_correct_dropped_count() {
330 let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
331 let stream = concat_ts(&pkts);
332
333 let junk_len = 7usize;
334 let junk: Vec<u8> = vec![0x00; junk_len];
335 let mut data = junk;
336 data.extend_from_slice(&stream);
337
338 let mut r = TsResync::new();
339 let emitted = r.feed(&data);
340
341 assert_eq!(emitted.len(), 6);
342 for (i, (e, p)) in emitted.iter().zip(pkts.iter()).enumerate() {
343 assert_eq!(*e, *p, "packet {i} mismatch after junk prefix");
344 }
345 let s = r.stats();
346 assert_eq!(
347 s.dropped_bytes, junk_len as u64,
348 "dropped bytes must equal junk prefix"
349 );
350 assert_eq!(s.resyncs, 0);
351 assert_eq!(s.packets, 6);
352 }
353
354 // ------------------------------------------------------------------
355 // Test 4 — mid-stream sync loss and reacquisition
356 // ------------------------------------------------------------------
357 #[test]
358 fn midstream_loss_resync() {
359 // Need > LOCK_CONFIRMATIONS clean packets before and after the stray
360 // byte so the stream locks, loses sync, and re-locks.
361 let pkts: Vec<_> = (0u8..14).map(|i| ts_packet(i + 1)).collect();
362 let clean = concat_ts(&pkts);
363
364 // Insert a single stray byte 12 bytes into packet 7.
365 let insert_at = 7 * TS_PACKET_SIZE + 12;
366 let mut data = clean[..insert_at].to_vec();
367 data.push(0x00);
368 data.extend_from_slice(&clean[insert_at..]);
369
370 let mut r = TsResync::new();
371 let emitted = r.feed(&data);
372
373 let s = r.stats();
374 assert!(
375 s.resyncs >= 1,
376 "mid-stream corruption must trigger a resync, got {}",
377 s.resyncs
378 );
379 assert_eq!(emitted[0], pkts[0], "first emitted packet is P0");
380 assert!(
381 emitted.len() >= 10,
382 "should recover and emit most packets, got {}",
383 emitted.len()
384 );
385 }
386
387 // ------------------------------------------------------------------
388 // Test 5 — 204-byte RS-coded packets detected + RS stripped
389 // ------------------------------------------------------------------
390 #[test]
391 fn rs204_detected_and_stripped() {
392 let mut stream = Vec::new();
393 let mut expected = Vec::new();
394 for i in 0u8..6 {
395 let tag = i + 1;
396 let rs = rs_packet(tag, 0xFF);
397 stream.extend_from_slice(&rs);
398 expected.push(ts_packet(tag));
399 }
400
401 let mut r = TsResync::new();
402 let emitted = r.feed(&stream);
403
404 assert_eq!(emitted.len(), 6, "RS-coded stream must emit 6 packets");
405 assert_eq!(
406 r.stride(),
407 Some(PacketStride::Rs204),
408 "stride must be Rs204"
409 );
410 for (i, (e, p)) in emitted.iter().zip(expected.iter()).enumerate() {
411 assert_eq!(e, p, "RS-stripped packet {i} mismatch");
412 }
413 // Confirm the emitted 188-byte packets are parseable by TsPacket.
414 for (i, pkt) in emitted.iter().enumerate() {
415 crate::ts::TsPacket::parse(pkt)
416 .unwrap_or_else(|e| panic!("RS-stripped packet {i} TsPacket::parse failed: {e}"));
417 }
418 let s = r.stats();
419 assert_eq!(s.packets, 6);
420 assert_eq!(s.resyncs, 0);
421 assert_eq!(s.dropped_bytes, 0);
422 }
423
424 // ------------------------------------------------------------------
425 // Test 6 — aligned 188 stream yields same packets as plain chunks(188)
426 // (equivalence: fixture-less variant using synthetic data)
427 // ------------------------------------------------------------------
428 #[test]
429 fn aligned_188_matches_plain_chunks() {
430 let pkts: Vec<_> = (0u8..10).map(|i| ts_packet(i + 1)).collect();
431 let data = concat_ts(&pkts);
432
433 // Oracle: plain chunks(188) filtered by sync byte.
434 let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
435 .chunks_exact(TS_PACKET_SIZE)
436 .filter(|c| c[0] == TS_SYNC_BYTE)
437 .map(|c| c.try_into().unwrap())
438 .collect();
439
440 let mut r = TsResync::new();
441 let emitted = r.feed(&data);
442
443 assert_eq!(emitted.len(), oracle.len(), "count mismatch");
444 for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
445 assert_eq!(e, o, "packet {i} differs from chunks-oracle");
446 }
447 }
448
449 // ------------------------------------------------------------------
450 // Test 7 — chunked feed equivalence (same result in small increments)
451 // ------------------------------------------------------------------
452 #[test]
453 fn chunked_feed_equivalence() {
454 let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
455 let stream = concat_ts(&pkts);
456
457 let whole = {
458 let mut r = TsResync::new();
459 r.feed(&stream)
460 };
461
462 let chunked = {
463 let mut r = TsResync::new();
464 let mut out = Vec::new();
465 for chunk in stream.chunks(100) {
466 out.extend(r.feed(chunk));
467 }
468 out
469 };
470
471 assert_eq!(whole.len(), chunked.len());
472 for (i, (w, c)) in whole.iter().zip(chunked.iter()).enumerate() {
473 assert_eq!(w, c, "packet {i} mismatch");
474 }
475 }
476
477 // ------------------------------------------------------------------
478 // Test 8 — fixture-based: m6-single.ts differential
479 // feeding the real fixture through TsResync must yield the same
480 // 188-byte packets as plain chunks_exact(188) + sync-byte filter.
481 // ------------------------------------------------------------------
482 #[test]
483 fn fixture_m6_matches_plain_chunks() {
484 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/ts/m6-single.ts");
485 let data = std::fs::read(path).expect("m6-single.ts fixture not found");
486
487 // Oracle: plain aligned 188-byte reads.
488 let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
489 .chunks_exact(TS_PACKET_SIZE)
490 .filter(|c| c[0] == TS_SYNC_BYTE)
491 .map(|c| c.try_into().unwrap())
492 .collect();
493 assert!(!oracle.is_empty(), "oracle empty — fixture empty?");
494
495 let mut r = TsResync::new();
496 let emitted = r.feed(&data);
497
498 assert_eq!(
499 emitted.len(),
500 oracle.len(),
501 "packet count: TsResync={} oracle={}",
502 emitted.len(),
503 oracle.len()
504 );
505 for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
506 assert_eq!(e, o, "packet {i} mismatch vs oracle");
507 }
508 }
509}