Skip to main content

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/// One span of bytes the resynchroniser discarded **after** it had locked
76/// (the stream lost sync mid-way, or ended on a truncated packet).
77///
78/// Drained with [`TsResync::take_losses`]; one entry per lost-sync event, so a
79/// caller can report each skipped unit exactly once. Junk *before* the first
80/// lock (a capture that starts mid-packet) is not a loss and is only counted in
81/// [`ResyncStats::dropped_bytes`].
82#[non_exhaustive]
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85pub struct ResyncLoss {
86    /// Byte offset, from the first byte ever fed, where the discarded span starts.
87    pub offset: u64,
88    /// Length of the discarded span in bytes.
89    pub dropped_bytes: u64,
90    /// Number of whole packets emitted before this loss, i.e. the index the
91    /// next intact packet will carry.
92    pub packet_index: u64,
93    /// `true` when the span is a truncated trailing packet found by
94    /// [`TsResync::finish`] rather than a sync loss in mid-stream.
95    pub truncated_tail: bool,
96}
97
98/// Stateful TS byte-stream resynchroniser (ISO/IEC 13818-1 §2.4.3.2).
99///
100/// Recovers 188-byte MPEG-TS packet alignment from an arbitrary byte stream
101/// that may start mid-packet or contain junk, and detects 204-byte
102/// Reed-Solomon-coded packets (stripping the 16 parity bytes).
103///
104/// Feed raw bytes with [`feed`](Self::feed); each call returns a `Vec` of
105/// aligned 188-byte TS packets.  Bytes are buffered across calls so that
106/// packet boundaries may span call boundaries freely.
107///
108/// Lock is declared after [`LOCK_CONFIRMATIONS`] consecutive sync bytes are
109/// found at the candidate stride (188 or 204).  On sync loss the resynchroniser
110/// re-scans from the byte after the lost position.
111///
112/// # Stats
113///
114/// [`stats`](Self::stats) returns cumulative counters (packets emitted,
115/// resyncs, dropped bytes).
116#[derive(Debug, Default)]
117pub struct TsResync {
118    buf: Vec<u8>,
119    /// Logical read head into `buf`; compacted periodically.
120    head: usize,
121    stride: Option<PacketStride>,
122    stats: ResyncStats,
123    /// Bytes already compacted away (stream offset of `buf[0]`).
124    consumed: u64,
125    /// Stream offset where the currently open sync loss began.
126    open_loss: Option<u64>,
127    /// Closed losses not yet drained by [`take_losses`](Self::take_losses).
128    losses: Vec<ResyncLoss>,
129    /// Aligned packets recovered by [`finish`](Self::finish), not yet drained
130    /// by [`take_tail_packets`](Self::take_tail_packets).
131    tail: Vec<[u8; TS_PACKET_SIZE]>,
132}
133
134impl TsResync {
135    /// Create a new resynchroniser with an empty internal buffer.
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Feed `data` and emit every newly-aligned 188-byte TS packet.
141    ///
142    /// For a 204-byte stream the 16 Reed-Solomon parity bytes are stripped;
143    /// only the 188-byte TS payload is returned.  Bytes that cannot yet form
144    /// a complete packet (or fall before lock) are buffered for the next call.
145    ///
146    /// **Drain [`take_losses`](Self::take_losses).** Every span discarded after
147    /// lock is appended to an internal list (about 32-40 bytes each) that only
148    /// `take_losses` empties; a caller that feeds forever and never drains it
149    /// grows memory linearly with the number of re-locks on a lossy input.
150    pub fn feed(&mut self, data: &[u8]) -> Vec<[u8; TS_PACKET_SIZE]> {
151        let mut emitted = Vec::new();
152        self.feed_into(data, &mut emitted);
153        emitted
154    }
155
156    /// [`feed`](Self::feed) appending to a caller-owned `out` (not cleared),
157    /// so a steady-state caller that reuses one `Vec` allocates nothing per
158    /// call (audit r01-O4). The output capacity is reserved once from the
159    /// buffered byte count instead of growing by doubling.
160    ///
161    /// Returns the number of packets appended.
162    pub fn feed_into(&mut self, data: &[u8], emitted: &mut Vec<[u8; TS_PACKET_SIZE]>) -> usize {
163        self.buf.extend_from_slice(data);
164        let before = emitted.len();
165        emitted.reserve((self.buf.len() - self.head) / TS_PACKET_SIZE);
166
167        loop {
168            match self.stride {
169                None => {
170                    if let Some((offset, s)) = find_sync(&self.buf[self.head..]) {
171                        self.lock(offset, s);
172                    } else {
173                        // Keep at most enough bytes to detect a future lock.
174                        let keep = LOCK_CONFIRMATIONS * RS_PACKET_SIZE;
175                        let tail_len = self.buf.len() - self.head;
176                        if tail_len > keep {
177                            let excess = tail_len - keep;
178                            self.stats.dropped_bytes += excess as u64;
179                            self.head += excess;
180                        }
181                        self.compact();
182                        return emitted.len() - before;
183                    }
184                }
185                Some(stride) => {
186                    let s = match stride {
187                        PacketStride::Ts188 => TS_PACKET_SIZE,
188                        PacketStride::Rs204 => RS_PACKET_SIZE,
189                    };
190                    let tail_len = self.buf.len() - self.head;
191                    if tail_len < s {
192                        self.compact();
193                        return emitted.len() - before;
194                    }
195                    if self.buf[self.head] == TS_SYNC_BYTE {
196                        let mut packet = [0u8; TS_PACKET_SIZE];
197                        packet.copy_from_slice(&self.buf[self.head..self.head + TS_PACKET_SIZE]);
198                        emitted.push(packet);
199                        self.head += s;
200                        self.stats.packets += 1;
201                    } else {
202                        // Sync byte missing — record the loss and re-scan.
203                        self.stats.resyncs += 1;
204                        self.stats.dropped_bytes += 1;
205                        self.open_loss
206                            .get_or_insert(self.consumed + self.head as u64);
207                        self.head += 1;
208                        self.stride = None;
209                    }
210                }
211            }
212        }
213    }
214
215    /// Detected packet stride, or [`None`] before the stream has locked.
216    pub fn stride(&self) -> Option<PacketStride> {
217        self.stride
218    }
219
220    /// Drain the sync losses recorded since the last call (see [`ResyncLoss`]).
221    ///
222    /// This is the only thing that empties the internal loss list, which is
223    /// unbounded by design (dropping a loss would hide data loss): call it
224    /// regularly — e.g. after every [`feed`](Self::feed) — or the list grows
225    /// with every re-lock on a lossy input. If you do not need the losses,
226    /// still call it and discard the result.
227    pub fn take_losses(&mut self) -> Vec<ResyncLoss> {
228        core::mem::take(&mut self.losses)
229    }
230
231    fn lock(&mut self, offset: usize, stride: PacketStride) {
232        self.stats.dropped_bytes += offset as u64;
233        self.head += offset;
234        self.stride = Some(stride);
235        if let Some(start) = self.open_loss.take() {
236            self.losses.push(ResyncLoss {
237                offset: start,
238                dropped_bytes: self.consumed + self.head as u64 - start,
239                packet_index: self.stats.packets,
240                truncated_tail: false,
241            });
242        }
243    }
244
245    /// Declare end of input. Packets that were still waiting for a full
246    /// [`LOCK_CONFIRMATIONS`] window cannot be confirmed by more bytes, so the
247    /// aligned packets that fill the rest of the input (a stream shorter than
248    /// the window included) are kept; drain them with
249    /// [`take_tail_packets`](Self::take_tail_packets), like
250    /// [`take_losses`](Self::take_losses) for the losses.
251    ///
252    /// Also closes a sync loss that never re-locked and records a truncated
253    /// trailing packet, both retrievable with
254    /// [`take_losses`](Self::take_losses). The buffer is discarded.
255    pub fn finish(&mut self) {
256        if self.stride.is_none()
257            && let Some((offset, s)) = find_tail_sync(&self.buf[self.head..])
258        {
259            self.lock(offset, s);
260            let mut emitted = core::mem::take(&mut self.tail);
261            self.feed_into(&[], &mut emitted);
262            self.tail = emitted;
263        }
264        let end = self.consumed + self.buf.len() as u64;
265        if let Some(start) = self.open_loss.take() {
266            self.losses.push(ResyncLoss {
267                offset: start,
268                dropped_bytes: end - start,
269                packet_index: self.stats.packets,
270                truncated_tail: false,
271            });
272        } else if self.stride.is_some() && self.head < self.buf.len() {
273            self.losses.push(ResyncLoss {
274                offset: self.consumed + self.head as u64,
275                dropped_bytes: (self.buf.len() - self.head) as u64,
276                packet_index: self.stats.packets,
277                truncated_tail: true,
278            });
279        }
280        self.stats.dropped_bytes += (self.buf.len() - self.head) as u64;
281        self.head = self.buf.len();
282        self.compact();
283    }
284
285    /// Drain the aligned packets [`finish`](Self::finish) recovered at end of
286    /// input. Call it after `finish` and process the result like the output
287    /// of [`feed`](Self::feed).
288    pub fn take_tail_packets(&mut self) -> Vec<[u8; TS_PACKET_SIZE]> {
289        core::mem::take(&mut self.tail)
290    }
291
292    /// Accumulated statistics.
293    pub fn stats(&self) -> ResyncStats {
294        self.stats
295    }
296
297    /// Compact the internal buffer by discarding consumed bytes.
298    fn compact(&mut self) {
299        if self.head > 0 {
300            self.consumed += self.head as u64;
301            self.buf.drain(..self.head);
302            self.head = 0;
303        }
304    }
305}
306
307/// Scan `buf` for the smallest offset `o` such that stride `S` (tried 188
308/// first, then 204) yields [`LOCK_CONFIRMATIONS`] consecutive sync bytes at
309/// positions `o + k*S` for `k = 0 .. LOCK_CONFIRMATIONS`.
310///
311/// Returns `(offset, stride)` on success, or `None` if no lock is found.
312fn find_sync(buf: &[u8]) -> Option<(usize, PacketStride)> {
313    for o in 0..buf.len() {
314        if buf[o] != TS_SYNC_BYTE {
315            continue;
316        }
317        if try_stride(buf, o, TS_PACKET_SIZE) {
318            return Some((o, PacketStride::Ts188));
319        }
320        if try_stride(buf, o, RS_PACKET_SIZE) {
321            return Some((o, PacketStride::Rs204));
322        }
323    }
324    None
325}
326
327/// End-of-input counterpart of [`find_sync`]: the smallest offset whose
328/// stride (188 first, then 204) holds a sync byte at every packet boundary
329/// the buffer still covers, with at least one whole packet.
330fn find_tail_sync(buf: &[u8]) -> Option<(usize, PacketStride)> {
331    let chain = |o: usize, s: usize| {
332        o + s <= buf.len()
333            && (o..buf.len())
334                .step_by(s)
335                .take_while(|pos| pos + s <= buf.len())
336                .all(|pos| buf[pos] == TS_SYNC_BYTE)
337    };
338    (0..buf.len())
339        .filter(|&o| buf[o] == TS_SYNC_BYTE)
340        .find_map(|o| {
341            if chain(o, TS_PACKET_SIZE) {
342                Some((o, PacketStride::Ts188))
343            } else if chain(o, RS_PACKET_SIZE) {
344                Some((o, PacketStride::Rs204))
345            } else {
346                None
347            }
348        })
349}
350
351/// Return `true` if `LOCK_CONFIRMATIONS` consecutive sync bytes exist at
352/// stride `s` starting from `offset` (the first is already known to be
353/// [`TS_SYNC_BYTE`]).
354fn try_stride(buf: &[u8], offset: usize, s: usize) -> bool {
355    for k in 1..LOCK_CONFIRMATIONS {
356        let pos = offset + k * s;
357        if pos >= buf.len() || buf[pos] != TS_SYNC_BYTE {
358            return false;
359        }
360    }
361    true
362}
363
364// ── Stateless datagram resync (dvb-stream origin) ───────────────────────────
365
366/// Find the byte offset of the first confirmed `0x47` sync byte in `buf`,
367/// accepting a lone sync byte when the buffer is too small to hold a
368/// confirming packet.
369///
370/// "Confirmed" means either `buf[offset + TS_PACKET_SIZE] == TS_SYNC_BYTE`
371/// (two-packet confirmation), or the buffer is too small for two packets
372/// (best-effort single-sync-byte alignment).
373///
374/// Returns `None` when `buf` contains no `0x47` byte at all.
375#[must_use]
376pub fn resync(buf: &[u8]) -> Option<usize> {
377    let mut i = 0;
378    while i < buf.len() {
379        if buf[i] == TS_SYNC_BYTE {
380            let next = i + TS_PACKET_SIZE;
381            if next < buf.len() {
382                if buf[next] == TS_SYNC_BYTE {
383                    return Some(i);
384                }
385                i += 1;
386                continue;
387            }
388            return Some(i);
389        }
390        i += 1;
391    }
392    None
393}
394
395/// Find the byte offset of the first sync byte in `buf` confirmed by a
396/// second sync byte exactly one packet later. Never accepts a lone sync byte.
397#[must_use]
398pub fn resync_confirmed(buf: &[u8]) -> Option<usize> {
399    (0..buf.len().saturating_sub(TS_PACKET_SIZE))
400        .find(|&i| buf[i] == TS_SYNC_BYTE && buf[i + TS_PACKET_SIZE] == TS_SYNC_BYTE)
401}
402
403/// Offset of the first `0x47` in `buf` that has no room for a confirming
404/// sync byte one packet later.
405#[must_use]
406pub fn unconfirmed_candidate(buf: &[u8]) -> Option<usize> {
407    let from = buf.len().saturating_sub(TS_PACKET_SIZE);
408    buf[from..]
409        .iter()
410        .position(|&b| b == TS_SYNC_BYTE)
411        .map(|i| from + i)
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use alloc::vec;
418    use alloc::vec::Vec;
419    extern crate std;
420
421    /// Build a 188-byte TS packet starting with `TS_SYNC_BYTE` followed by
422    /// `tag` (repeated).  All non-sync bytes are kept away from `0x47` so
423    /// that no false lock occurs in the fixture data.
424    fn ts_packet(tag: u8) -> [u8; TS_PACKET_SIZE] {
425        assert_ne!(tag, TS_SYNC_BYTE, "tag must not equal sync byte");
426        let mut pkt = [tag; TS_PACKET_SIZE];
427        pkt[0] = TS_SYNC_BYTE;
428        pkt
429    }
430
431    /// Build a 204-byte RS-coded packet: 188-byte TS payload (with `ts_tag`)
432    /// plus 16 parity bytes filled with `parity` (must not be `0x47`).
433    fn rs_packet(ts_tag: u8, parity: u8) -> [u8; RS_PACKET_SIZE] {
434        assert_ne!(ts_tag, TS_SYNC_BYTE);
435        assert_ne!(parity, TS_SYNC_BYTE);
436        let mut pkt = [parity; RS_PACKET_SIZE];
437        pkt[0] = TS_SYNC_BYTE;
438        pkt[1..TS_PACKET_SIZE].fill(ts_tag);
439        pkt
440    }
441
442    /// Concatenate several 188-byte packet arrays into a flat `Vec<u8>`.
443    fn concat_ts(packets: &[[u8; TS_PACKET_SIZE]]) -> Vec<u8> {
444        let mut v = Vec::with_capacity(packets.len() * TS_PACKET_SIZE);
445        for p in packets {
446            v.extend_from_slice(p);
447        }
448        v
449    }
450
451    // ------------------------------------------------------------------
452    // Test 1 — aligned 188-byte passthrough
453    // ------------------------------------------------------------------
454    #[test]
455    fn lone_sync_byte_accepted_by_resync_not_confirmed() {
456        let buf = [TS_SYNC_BYTE];
457        assert_eq!(resync(&buf), Some(0));
458        assert_eq!(resync_confirmed(&buf), None);
459    }
460
461    #[test]
462    fn aligned_188_passthrough() {
463        let packets: Vec<_> = (0u8..5).map(|i| ts_packet(i + 1)).collect();
464        let data = concat_ts(&packets);
465
466        let mut r = TsResync::new();
467        let emitted = r.feed(&data);
468
469        assert_eq!(emitted.len(), 5);
470        for (i, (e, p)) in emitted.iter().zip(packets.iter()).enumerate() {
471            assert_eq!(e, p, "packet {i} mismatch");
472        }
473        assert_eq!(r.stride(), Some(PacketStride::Ts188));
474        let s = r.stats();
475        assert_eq!(s.packets, 5);
476        assert_eq!(s.resyncs, 0);
477        assert_eq!(s.dropped_bytes, 0);
478    }
479
480    // ------------------------------------------------------------------
481    // Test 2 — lock only after LOCK_CONFIRMATIONS sync bytes
482    // ------------------------------------------------------------------
483    #[test]
484    fn requires_n_confirmations_before_lock() {
485        // Pin the default lock window. The hardcoded 4-vs-5 boundary below only
486        // bites the threshold when this is 5 — assert it explicitly so a change
487        // to the default (or to this test's literals) cannot silently pass.
488        assert_eq!(
489            LOCK_CONFIRMATIONS, 5,
490            "this test pins the default lock window of 5"
491        );
492
493        // FOUR confirming, stride-aligned packets are one short of the window:
494        // only four sync bytes sit at the 188-stride boundaries, so the
495        // resynchroniser must NOT lock and must emit nothing (it buffers).
496        let four = concat_ts(&(1u8..=4).map(ts_packet).collect::<Vec<_>>());
497        let mut r = TsResync::new();
498        assert_eq!(
499            r.feed(&four).len(),
500            0,
501            "4 confirmations (< 5) must not lock or emit"
502        );
503        assert_eq!(r.stride(), None, "stride must remain None below the window");
504
505        // The FIFTH stride-aligned sync byte completes the window → lock.
506        let mut out = r.feed(&ts_packet(5));
507        out.extend(r.feed(&[]));
508        assert!(
509            r.stride().is_some(),
510            "the 5th confirmation must trigger lock"
511        );
512        // Once locked, the buffered packets are emitted (5 fed, all aligned).
513        assert_eq!(out.len(), 5, "all five aligned packets emit once locked");
514    }
515
516    // ------------------------------------------------------------------
517    // Test 3 — junk prefix: leading garbage dropped, correct count returned
518    // ------------------------------------------------------------------
519    #[test]
520    fn junk_prefix_correct_dropped_count() {
521        let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
522        let stream = concat_ts(&pkts);
523
524        let junk_len = 7usize;
525        let junk: Vec<u8> = vec![0x00; junk_len];
526        let mut data = junk;
527        data.extend_from_slice(&stream);
528
529        let mut r = TsResync::new();
530        let emitted = r.feed(&data);
531
532        assert_eq!(emitted.len(), 6);
533        for (i, (e, p)) in emitted.iter().zip(pkts.iter()).enumerate() {
534            assert_eq!(*e, *p, "packet {i} mismatch after junk prefix");
535        }
536        let s = r.stats();
537        assert_eq!(
538            s.dropped_bytes, junk_len as u64,
539            "dropped bytes must equal junk prefix"
540        );
541        assert_eq!(s.resyncs, 0);
542        assert_eq!(s.packets, 6);
543    }
544
545    // ------------------------------------------------------------------
546    // Test 4 — mid-stream sync loss and reacquisition
547    // ------------------------------------------------------------------
548    #[test]
549    fn midstream_loss_resync() {
550        // Need > LOCK_CONFIRMATIONS clean packets before and after the stray
551        // byte so the stream locks, loses sync, and re-locks.
552        let pkts: Vec<_> = (0u8..14).map(|i| ts_packet(i + 1)).collect();
553        let clean = concat_ts(&pkts);
554
555        // Insert a single stray byte 12 bytes into packet 7.
556        let insert_at = 7 * TS_PACKET_SIZE + 12;
557        let mut data = clean[..insert_at].to_vec();
558        data.push(0x00);
559        data.extend_from_slice(&clean[insert_at..]);
560
561        let mut r = TsResync::new();
562        let emitted = r.feed(&data);
563
564        let s = r.stats();
565        assert!(
566            s.resyncs >= 1,
567            "mid-stream corruption must trigger a resync, got {}",
568            s.resyncs
569        );
570        assert_eq!(emitted[0], pkts[0], "first emitted packet is P0");
571        assert!(
572            emitted.len() >= 10,
573            "should recover and emit most packets, got {}",
574            emitted.len()
575        );
576    }
577
578    // ------------------------------------------------------------------
579    // Test 5 — 204-byte RS-coded packets detected + RS stripped
580    // ------------------------------------------------------------------
581    #[test]
582    fn rs204_detected_and_stripped() {
583        let mut stream = Vec::new();
584        let mut expected = Vec::new();
585        for i in 0u8..6 {
586            let tag = i + 1;
587            let rs = rs_packet(tag, 0xFF);
588            stream.extend_from_slice(&rs);
589            expected.push(ts_packet(tag));
590        }
591
592        let mut r = TsResync::new();
593        let emitted = r.feed(&stream);
594
595        assert_eq!(emitted.len(), 6, "RS-coded stream must emit 6 packets");
596        assert_eq!(
597            r.stride(),
598            Some(PacketStride::Rs204),
599            "stride must be Rs204"
600        );
601        for (i, (e, p)) in emitted.iter().zip(expected.iter()).enumerate() {
602            assert_eq!(e, p, "RS-stripped packet {i} mismatch");
603        }
604        // Confirm the emitted 188-byte packets are parseable by TsPacket.
605        for (i, pkt) in emitted.iter().enumerate() {
606            crate::ts::TsPacket::parse(pkt)
607                .unwrap_or_else(|e| panic!("RS-stripped packet {i} TsPacket::parse failed: {e}"));
608        }
609        let s = r.stats();
610        assert_eq!(s.packets, 6);
611        assert_eq!(s.resyncs, 0);
612        assert_eq!(s.dropped_bytes, 0);
613    }
614
615    // ------------------------------------------------------------------
616    // Test 6 — aligned 188 stream yields same packets as plain chunks(188)
617    //   (equivalence: fixture-less variant using synthetic data)
618    // ------------------------------------------------------------------
619    #[test]
620    fn broken_sync_byte_after_lock_is_one_loss_with_offset() {
621        let mut bytes = Vec::new();
622        for t in 1..=12u8 {
623            bytes.extend_from_slice(&ts_packet(t));
624        }
625        bytes[5 * TS_PACKET_SIZE] = 0x00;
626        let mut r = TsResync::new();
627        let got = r.feed(&bytes);
628        assert_eq!(got.len(), 11);
629        r.finish();
630        let losses = r.take_losses();
631        assert_eq!(losses.len(), 1);
632        assert_eq!(losses[0].offset, 5 * TS_PACKET_SIZE as u64);
633        assert_eq!(losses[0].dropped_bytes, TS_PACKET_SIZE as u64);
634        assert_eq!(losses[0].packet_index, 5);
635        assert!(!losses[0].truncated_tail);
636    }
637
638    #[test]
639    fn leading_junk_is_not_a_loss_but_truncated_tail_is() {
640        let mut bytes = vec![0x11; 30];
641        for t in 1..=8u8 {
642            bytes.extend_from_slice(&ts_packet(t));
643        }
644        bytes.extend_from_slice(&[0x22; 50]);
645        let mut r = TsResync::new();
646        let _ = r.feed(&bytes);
647        r.finish();
648        let losses = r.take_losses();
649        assert_eq!(losses.len(), 1);
650        assert!(losses[0].truncated_tail);
651        assert_eq!(losses[0].offset, 30 + 8 * TS_PACKET_SIZE as u64);
652        assert_eq!(losses[0].dropped_bytes, 50);
653    }
654
655    #[test]
656    fn aligned_188_matches_plain_chunks() {
657        let pkts: Vec<_> = (0u8..10).map(|i| ts_packet(i + 1)).collect();
658        let data = concat_ts(&pkts);
659
660        // Oracle: plain chunks(188) filtered by sync byte.
661        let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
662            .chunks_exact(TS_PACKET_SIZE)
663            .filter(|c| c[0] == TS_SYNC_BYTE)
664            .map(|c| c.try_into().unwrap())
665            .collect();
666
667        let mut r = TsResync::new();
668        let emitted = r.feed(&data);
669
670        assert_eq!(emitted.len(), oracle.len(), "count mismatch");
671        for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
672            assert_eq!(e, o, "packet {i} differs from chunks-oracle");
673        }
674    }
675
676    // ------------------------------------------------------------------
677    // Test 7 — chunked feed equivalence (same result in small increments)
678    // ------------------------------------------------------------------
679    #[test]
680    fn chunked_feed_equivalence() {
681        let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
682        let stream = concat_ts(&pkts);
683
684        let whole = {
685            let mut r = TsResync::new();
686            r.feed(&stream)
687        };
688
689        let chunked = {
690            let mut r = TsResync::new();
691            let mut out = Vec::new();
692            for chunk in stream.chunks(100) {
693                out.extend(r.feed(chunk));
694            }
695            out
696        };
697
698        assert_eq!(whole.len(), chunked.len());
699        for (i, (w, c)) in whole.iter().zip(chunked.iter()).enumerate() {
700            assert_eq!(w, c, "packet {i} mismatch");
701        }
702    }
703
704    // ------------------------------------------------------------------
705    // Test 8 — fixture-based: m6-single.ts differential
706    //   feeding the real fixture through TsResync must yield the same
707    //   188-byte packets as plain chunks_exact(188) + sync-byte filter.
708    // ------------------------------------------------------------------
709    #[test]
710    fn fixture_m6_matches_plain_chunks() {
711        let path = test_fixtures::path("ts/m6-single.ts");
712        let data = std::fs::read(path).expect("m6-single.ts fixture not found");
713
714        // Oracle: plain aligned 188-byte reads.
715        let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
716            .chunks_exact(TS_PACKET_SIZE)
717            .filter(|c| c[0] == TS_SYNC_BYTE)
718            .map(|c| c.try_into().unwrap())
719            .collect();
720        assert!(!oracle.is_empty(), "oracle empty — fixture empty?");
721
722        let mut r = TsResync::new();
723        let emitted = r.feed(&data);
724
725        assert_eq!(
726            emitted.len(),
727            oracle.len(),
728            "packet count: TsResync={} oracle={}",
729            emitted.len(),
730            oracle.len()
731        );
732        for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
733            assert_eq!(e, o, "packet {i} mismatch vs oracle");
734        }
735    }
736
737    #[test]
738    fn finish_emits_the_aligned_packets_after_a_corrupt_sync_byte() {
739        let mut input = Vec::new();
740        for tag in 1u8..=10 {
741            input.extend_from_slice(&ts_packet(tag));
742        }
743        let mut bad = ts_packet(11);
744        bad[0] = 0x00;
745        input.extend_from_slice(&bad);
746        for tag in 12u8..=14 {
747            input.extend_from_slice(&ts_packet(tag));
748        }
749
750        let mut r = TsResync::new();
751        let mut packets = r.feed(&input);
752        assert_eq!(
753            packets.len(),
754            10,
755            "three packets cannot lock before the end"
756        );
757        r.finish();
758        packets.extend(r.take_tail_packets());
759        let tags: Vec<u8> = packets.iter().map(|p| p[1]).collect();
760        assert_eq!(tags, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14]);
761        let stats = r.stats();
762        assert_eq!(stats.packets, 13);
763        assert_eq!(stats.dropped_bytes, TS_PACKET_SIZE as u64);
764        let losses = r.take_losses();
765        assert_eq!(losses.len(), 1);
766        assert_eq!(losses[0].dropped_bytes, TS_PACKET_SIZE as u64);
767        assert!(!losses[0].truncated_tail);
768    }
769
770    #[test]
771    fn finish_emits_a_stream_shorter_than_the_lock_window() {
772        let packets: Vec<_> = (1u8..=4).map(ts_packet).collect();
773        let mut r = TsResync::new();
774        assert!(r.feed(&concat_ts(&packets)).is_empty());
775        r.finish();
776        assert_eq!(r.take_tail_packets(), packets);
777        assert!(r.take_tail_packets().is_empty());
778        assert_eq!(r.stride(), Some(PacketStride::Ts188));
779        assert_eq!(r.stats().dropped_bytes, 0);
780    }
781
782    #[test]
783    fn finish_emits_a_short_rs204_stream_stripped() {
784        let mut stream = Vec::new();
785        for tag in 1u8..=3 {
786            stream.extend_from_slice(&rs_packet(tag, 0xFF));
787        }
788        let mut r = TsResync::new();
789        assert!(r.feed(&stream).is_empty());
790        r.finish();
791        let packets = r.take_tail_packets();
792        assert_eq!(packets, [ts_packet(1), ts_packet(2), ts_packet(3)]);
793        assert_eq!(r.stride(), Some(PacketStride::Rs204));
794    }
795
796    #[test]
797    fn finish_does_not_lock_onto_a_stray_sync_byte_without_a_whole_packet() {
798        let mut junk = vec![0x00u8; 100];
799        junk[40] = TS_SYNC_BYTE;
800        let mut r = TsResync::new();
801        assert!(r.feed(&junk).is_empty());
802        r.finish();
803        assert!(r.take_tail_packets().is_empty());
804        assert_eq!(r.stats().dropped_bytes, 100);
805    }
806
807    #[test]
808    fn finish_does_not_emit_a_trailing_partial_packet_starting_with_sync_byte() {
809        let mut input = Vec::new();
810        for tag in 1u8..=10 {
811            input.extend_from_slice(&ts_packet(tag));
812        }
813        input.push(TS_SYNC_BYTE);
814        input.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
815
816        let mut r = TsResync::new();
817        let mut packets = r.feed(&input);
818        r.finish();
819        packets.extend(r.take_tail_packets());
820        let tags: Vec<u8> = packets.iter().map(|p| p[1]).collect();
821        assert_eq!(tags, (1u8..=10).collect::<Vec<_>>());
822        assert!(r.take_tail_packets().is_empty());
823        let losses = r.take_losses();
824        assert_eq!(losses.len(), 1);
825        assert!(losses[0].truncated_tail);
826        assert_eq!(losses[0].dropped_bytes, 5);
827    }
828}