1use alloc::vec::Vec;
26
27use crate::ts::{TS_PACKET_SIZE, TS_SYNC_BYTE};
28
29pub const RS_PACKET_SIZE: usize = 204;
32
33pub const RS_PARITY_LEN: usize = RS_PACKET_SIZE - TS_PACKET_SIZE;
35
36pub const LOCK_CONFIRMATIONS: usize = 5;
38
39#[non_exhaustive]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize))]
43pub enum PacketStride {
44 Ts188,
46 Rs204,
48}
49
50impl PacketStride {
51 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#[non_exhaustive]
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub struct ResyncStats {
67 pub packets: u64,
69 pub resyncs: u64,
71 pub dropped_bytes: u64,
73}
74
75#[non_exhaustive]
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85pub struct ResyncLoss {
86 pub offset: u64,
88 pub dropped_bytes: u64,
90 pub packet_index: u64,
93 pub truncated_tail: bool,
96}
97
98#[derive(Debug, Default)]
117pub struct TsResync {
118 buf: Vec<u8>,
119 head: usize,
121 stride: Option<PacketStride>,
122 stats: ResyncStats,
123 consumed: u64,
125 open_loss: Option<u64>,
127 losses: Vec<ResyncLoss>,
129}
130
131impl TsResync {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn feed(&mut self, data: &[u8]) -> Vec<[u8; TS_PACKET_SIZE]> {
143 let mut emitted = Vec::new();
144 self.feed_into(data, &mut emitted);
145 emitted
146 }
147
148 pub fn feed_into(&mut self, data: &[u8], emitted: &mut Vec<[u8; TS_PACKET_SIZE]>) -> usize {
155 self.buf.extend_from_slice(data);
156 let before = emitted.len();
157 emitted.reserve((self.buf.len() - self.head) / TS_PACKET_SIZE);
158
159 loop {
160 match self.stride {
161 None => {
162 if let Some((offset, s)) = find_sync(&self.buf[self.head..]) {
163 self.stats.dropped_bytes += offset as u64;
164 self.head += offset;
165 self.stride = Some(s);
166 if let Some(start) = self.open_loss.take() {
167 self.losses.push(ResyncLoss {
168 offset: start,
169 dropped_bytes: self.consumed + self.head as u64 - start,
170 packet_index: self.stats.packets,
171 truncated_tail: false,
172 });
173 }
174 } else {
175 let keep = LOCK_CONFIRMATIONS * RS_PACKET_SIZE;
177 let tail_len = self.buf.len() - self.head;
178 if tail_len > keep {
179 let excess = tail_len - keep;
180 self.stats.dropped_bytes += excess as u64;
181 self.head += excess;
182 }
183 self.compact();
184 return emitted.len() - before;
185 }
186 }
187 Some(stride) => {
188 let s = match stride {
189 PacketStride::Ts188 => TS_PACKET_SIZE,
190 PacketStride::Rs204 => RS_PACKET_SIZE,
191 };
192 let tail_len = self.buf.len() - self.head;
193 if tail_len < s {
194 self.compact();
195 return emitted.len() - before;
196 }
197 if self.buf[self.head] == TS_SYNC_BYTE {
198 let mut packet = [0u8; TS_PACKET_SIZE];
199 packet.copy_from_slice(&self.buf[self.head..self.head + TS_PACKET_SIZE]);
200 emitted.push(packet);
201 self.head += s;
202 self.stats.packets += 1;
203 } else {
204 self.stats.resyncs += 1;
206 self.stats.dropped_bytes += 1;
207 self.open_loss
208 .get_or_insert(self.consumed + self.head as u64);
209 self.head += 1;
210 self.stride = None;
211 }
212 }
213 }
214 }
215 }
216
217 pub fn stride(&self) -> Option<PacketStride> {
219 self.stride
220 }
221
222 pub fn take_losses(&mut self) -> Vec<ResyncLoss> {
224 core::mem::take(&mut self.losses)
225 }
226
227 pub fn finish(&mut self) {
231 let end = self.consumed + self.buf.len() as u64;
232 if let Some(start) = self.open_loss.take() {
233 self.losses.push(ResyncLoss {
234 offset: start,
235 dropped_bytes: end - start,
236 packet_index: self.stats.packets,
237 truncated_tail: false,
238 });
239 } else if self.stride.is_some() && self.head < self.buf.len() {
240 self.losses.push(ResyncLoss {
241 offset: self.consumed + self.head as u64,
242 dropped_bytes: (self.buf.len() - self.head) as u64,
243 packet_index: self.stats.packets,
244 truncated_tail: true,
245 });
246 }
247 self.stats.dropped_bytes += (self.buf.len() - self.head) as u64;
248 self.head = self.buf.len();
249 self.compact();
250 }
251
252 pub fn stats(&self) -> ResyncStats {
254 self.stats
255 }
256
257 fn compact(&mut self) {
259 if self.head > 0 {
260 self.consumed += self.head as u64;
261 self.buf.drain(..self.head);
262 self.head = 0;
263 }
264 }
265}
266
267fn find_sync(buf: &[u8]) -> Option<(usize, PacketStride)> {
273 for o in 0..buf.len() {
274 if buf[o] != TS_SYNC_BYTE {
275 continue;
276 }
277 if try_stride(buf, o, TS_PACKET_SIZE) {
278 return Some((o, PacketStride::Ts188));
279 }
280 if try_stride(buf, o, RS_PACKET_SIZE) {
281 return Some((o, PacketStride::Rs204));
282 }
283 }
284 None
285}
286
287fn try_stride(buf: &[u8], offset: usize, s: usize) -> bool {
291 for k in 1..LOCK_CONFIRMATIONS {
292 let pos = offset + k * s;
293 if pos >= buf.len() || buf[pos] != TS_SYNC_BYTE {
294 return false;
295 }
296 }
297 true
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use alloc::vec;
304 use alloc::vec::Vec;
305 extern crate std;
306
307 fn ts_packet(tag: u8) -> [u8; TS_PACKET_SIZE] {
311 assert_ne!(tag, TS_SYNC_BYTE, "tag must not equal sync byte");
312 let mut pkt = [tag; TS_PACKET_SIZE];
313 pkt[0] = TS_SYNC_BYTE;
314 pkt
315 }
316
317 fn rs_packet(ts_tag: u8, parity: u8) -> [u8; RS_PACKET_SIZE] {
320 assert_ne!(ts_tag, TS_SYNC_BYTE);
321 assert_ne!(parity, TS_SYNC_BYTE);
322 let mut pkt = [parity; RS_PACKET_SIZE];
323 pkt[0] = TS_SYNC_BYTE;
324 pkt[1..TS_PACKET_SIZE].fill(ts_tag);
325 pkt
326 }
327
328 fn concat_ts(packets: &[[u8; TS_PACKET_SIZE]]) -> Vec<u8> {
330 let mut v = Vec::with_capacity(packets.len() * TS_PACKET_SIZE);
331 for p in packets {
332 v.extend_from_slice(p);
333 }
334 v
335 }
336
337 #[test]
341 fn aligned_188_passthrough() {
342 let packets: Vec<_> = (0u8..5).map(|i| ts_packet(i + 1)).collect();
343 let data = concat_ts(&packets);
344
345 let mut r = TsResync::new();
346 let emitted = r.feed(&data);
347
348 assert_eq!(emitted.len(), 5);
349 for (i, (e, p)) in emitted.iter().zip(packets.iter()).enumerate() {
350 assert_eq!(e, p, "packet {i} mismatch");
351 }
352 assert_eq!(r.stride(), Some(PacketStride::Ts188));
353 let s = r.stats();
354 assert_eq!(s.packets, 5);
355 assert_eq!(s.resyncs, 0);
356 assert_eq!(s.dropped_bytes, 0);
357 }
358
359 #[test]
363 fn requires_n_confirmations_before_lock() {
364 assert_eq!(
368 LOCK_CONFIRMATIONS, 5,
369 "this test pins the default lock window of 5"
370 );
371
372 let four = concat_ts(&(1u8..=4).map(ts_packet).collect::<Vec<_>>());
376 let mut r = TsResync::new();
377 assert_eq!(
378 r.feed(&four).len(),
379 0,
380 "4 confirmations (< 5) must not lock or emit"
381 );
382 assert_eq!(r.stride(), None, "stride must remain None below the window");
383
384 let mut out = r.feed(&ts_packet(5));
386 out.extend(r.feed(&[]));
387 assert!(
388 r.stride().is_some(),
389 "the 5th confirmation must trigger lock"
390 );
391 assert_eq!(out.len(), 5, "all five aligned packets emit once locked");
393 }
394
395 #[test]
399 fn junk_prefix_correct_dropped_count() {
400 let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
401 let stream = concat_ts(&pkts);
402
403 let junk_len = 7usize;
404 let junk: Vec<u8> = vec![0x00; junk_len];
405 let mut data = junk;
406 data.extend_from_slice(&stream);
407
408 let mut r = TsResync::new();
409 let emitted = r.feed(&data);
410
411 assert_eq!(emitted.len(), 6);
412 for (i, (e, p)) in emitted.iter().zip(pkts.iter()).enumerate() {
413 assert_eq!(*e, *p, "packet {i} mismatch after junk prefix");
414 }
415 let s = r.stats();
416 assert_eq!(
417 s.dropped_bytes, junk_len as u64,
418 "dropped bytes must equal junk prefix"
419 );
420 assert_eq!(s.resyncs, 0);
421 assert_eq!(s.packets, 6);
422 }
423
424 #[test]
428 fn midstream_loss_resync() {
429 let pkts: Vec<_> = (0u8..14).map(|i| ts_packet(i + 1)).collect();
432 let clean = concat_ts(&pkts);
433
434 let insert_at = 7 * TS_PACKET_SIZE + 12;
436 let mut data = clean[..insert_at].to_vec();
437 data.push(0x00);
438 data.extend_from_slice(&clean[insert_at..]);
439
440 let mut r = TsResync::new();
441 let emitted = r.feed(&data);
442
443 let s = r.stats();
444 assert!(
445 s.resyncs >= 1,
446 "mid-stream corruption must trigger a resync, got {}",
447 s.resyncs
448 );
449 assert_eq!(emitted[0], pkts[0], "first emitted packet is P0");
450 assert!(
451 emitted.len() >= 10,
452 "should recover and emit most packets, got {}",
453 emitted.len()
454 );
455 }
456
457 #[test]
461 fn rs204_detected_and_stripped() {
462 let mut stream = Vec::new();
463 let mut expected = Vec::new();
464 for i in 0u8..6 {
465 let tag = i + 1;
466 let rs = rs_packet(tag, 0xFF);
467 stream.extend_from_slice(&rs);
468 expected.push(ts_packet(tag));
469 }
470
471 let mut r = TsResync::new();
472 let emitted = r.feed(&stream);
473
474 assert_eq!(emitted.len(), 6, "RS-coded stream must emit 6 packets");
475 assert_eq!(
476 r.stride(),
477 Some(PacketStride::Rs204),
478 "stride must be Rs204"
479 );
480 for (i, (e, p)) in emitted.iter().zip(expected.iter()).enumerate() {
481 assert_eq!(e, p, "RS-stripped packet {i} mismatch");
482 }
483 for (i, pkt) in emitted.iter().enumerate() {
485 crate::ts::TsPacket::parse(pkt)
486 .unwrap_or_else(|e| panic!("RS-stripped packet {i} TsPacket::parse failed: {e}"));
487 }
488 let s = r.stats();
489 assert_eq!(s.packets, 6);
490 assert_eq!(s.resyncs, 0);
491 assert_eq!(s.dropped_bytes, 0);
492 }
493
494 #[test]
499 fn broken_sync_byte_after_lock_is_one_loss_with_offset() {
500 let mut bytes = Vec::new();
501 for t in 1..=12u8 {
502 bytes.extend_from_slice(&ts_packet(t));
503 }
504 bytes[5 * TS_PACKET_SIZE] = 0x00;
505 let mut r = TsResync::new();
506 let got = r.feed(&bytes);
507 assert_eq!(got.len(), 11);
508 r.finish();
509 let losses = r.take_losses();
510 assert_eq!(losses.len(), 1);
511 assert_eq!(losses[0].offset, 5 * TS_PACKET_SIZE as u64);
512 assert_eq!(losses[0].dropped_bytes, TS_PACKET_SIZE as u64);
513 assert_eq!(losses[0].packet_index, 5);
514 assert!(!losses[0].truncated_tail);
515 }
516
517 #[test]
518 fn leading_junk_is_not_a_loss_but_truncated_tail_is() {
519 let mut bytes = vec![0x11; 30];
520 for t in 1..=8u8 {
521 bytes.extend_from_slice(&ts_packet(t));
522 }
523 bytes.extend_from_slice(&[0x22; 50]);
524 let mut r = TsResync::new();
525 let _ = r.feed(&bytes);
526 r.finish();
527 let losses = r.take_losses();
528 assert_eq!(losses.len(), 1);
529 assert!(losses[0].truncated_tail);
530 assert_eq!(losses[0].offset, 30 + 8 * TS_PACKET_SIZE as u64);
531 assert_eq!(losses[0].dropped_bytes, 50);
532 }
533
534 #[test]
535 fn aligned_188_matches_plain_chunks() {
536 let pkts: Vec<_> = (0u8..10).map(|i| ts_packet(i + 1)).collect();
537 let data = concat_ts(&pkts);
538
539 let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
541 .chunks_exact(TS_PACKET_SIZE)
542 .filter(|c| c[0] == TS_SYNC_BYTE)
543 .map(|c| c.try_into().unwrap())
544 .collect();
545
546 let mut r = TsResync::new();
547 let emitted = r.feed(&data);
548
549 assert_eq!(emitted.len(), oracle.len(), "count mismatch");
550 for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
551 assert_eq!(e, o, "packet {i} differs from chunks-oracle");
552 }
553 }
554
555 #[test]
559 fn chunked_feed_equivalence() {
560 let pkts: Vec<_> = (0u8..6).map(|i| ts_packet(i + 1)).collect();
561 let stream = concat_ts(&pkts);
562
563 let whole = {
564 let mut r = TsResync::new();
565 r.feed(&stream)
566 };
567
568 let chunked = {
569 let mut r = TsResync::new();
570 let mut out = Vec::new();
571 for chunk in stream.chunks(100) {
572 out.extend(r.feed(chunk));
573 }
574 out
575 };
576
577 assert_eq!(whole.len(), chunked.len());
578 for (i, (w, c)) in whole.iter().zip(chunked.iter()).enumerate() {
579 assert_eq!(w, c, "packet {i} mismatch");
580 }
581 }
582
583 #[test]
589 fn fixture_m6_matches_plain_chunks() {
590 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/ts/m6-single.ts");
591 let data = std::fs::read(path).expect("m6-single.ts fixture not found");
592
593 let oracle: Vec<[u8; TS_PACKET_SIZE]> = data
595 .chunks_exact(TS_PACKET_SIZE)
596 .filter(|c| c[0] == TS_SYNC_BYTE)
597 .map(|c| c.try_into().unwrap())
598 .collect();
599 assert!(!oracle.is_empty(), "oracle empty — fixture empty?");
600
601 let mut r = TsResync::new();
602 let emitted = r.feed(&data);
603
604 assert_eq!(
605 emitted.len(),
606 oracle.len(),
607 "packet count: TsResync={} oracle={}",
608 emitted.len(),
609 oracle.len()
610 );
611 for (i, (e, o)) in emitted.iter().zip(oracle.iter()).enumerate() {
612 assert_eq!(e, o, "packet {i} mismatch vs oracle");
613 }
614 }
615}