Skip to main content

dvb_bbframe/
packet.rs

1//! User packet extraction from BBFrame data fields.
2//!
3//! Supports Normal Mode (NM, 188-byte stride) and High Efficiency Mode
4//! (HEM, 187-byte stride) per EN 302 755 §5.1.8.
5//!
6//! In NM the first byte of each user packet in the data field is a CRC-8
7//! that replaces the original sync byte (0x47). In HEM the sync byte is
8//! simply absent and must be prepended.
9//!
10//! ## SYNCD handling
11//!
12//! `SYNCD` gives the bit offset from the start of the DATA FIELD to the
13//! first bit of the CRC-8 byte of the first user packet (NM) or the first
14//! byte of the first user packet (HEM).  Callers typically prepend any
15//! carry-over from the previous BBFrame and pass the result plus SYNCD.
16//!
17//! ## NPD/DNP reinsertion (HEM only)
18//!
19//! When NPD is active (`matype.npd == true`), each transmitted user
20//! packet is followed by a 1-byte DNP counter. The [`HemTsIter`] skips
21//! these DNP bytes automatically.
22
23use crate::header::{Bbheader, Mode, BBHEADER_LEN};
24
25/// User packet size in Normal Mode (188 bytes = full MPEG-2 TS packet).
26pub const NM_UP_SIZE: usize = 188;
27
28/// User packet size in High Efficiency Mode (187 bytes = TS minus sync byte).
29pub const HEM_UP_SIZE: usize = 187;
30
31/// MPEG-2 sync byte that CRC-8 replaces in NM.
32pub const TS_SYNC_BYTE: u8 = 0x47;
33
34/// Iterator over NM TS user packets.
35///
36/// Each item is a `[u8; 188]` with the sync byte restored to 0x47,
37/// replacing the CRC-8 byte that occupies position 0 in the data field.
38#[derive(Clone, Copy)]
39#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
40pub struct NmTsIter<'a> {
41    data: &'a [u8],
42    pos: usize,
43}
44
45impl<'a> NmTsIter<'a> {
46    /// Create a new NM TS user packet iterator.
47    ///
48    /// `data` is the full data field (after skipping SYNCD bytes if
49    /// already aligned). The iterator starts at byte 0 of `data`.
50    pub fn new(data: &'a [u8]) -> Self {
51        Self { data, pos: 0 }
52    }
53
54    /// Return the unconsumed tail of the data field.
55    pub fn remaining(self) -> &'a [u8] {
56        &self.data[self.pos..]
57    }
58}
59
60impl Iterator for NmTsIter<'_> {
61    type Item = [u8; NM_UP_SIZE];
62
63    fn next(&mut self) -> Option<Self::Item> {
64        if self.pos + NM_UP_SIZE > self.data.len() {
65            return None;
66        }
67        let mut pkt = [0u8; NM_UP_SIZE];
68        pkt[0] = TS_SYNC_BYTE; // Replace CRC-8 byte with sync byte
69        pkt[1..].copy_from_slice(&self.data[self.pos + 1..self.pos + NM_UP_SIZE]);
70        self.pos += NM_UP_SIZE;
71        Some(pkt)
72    }
73
74    fn size_hint(&self) -> (usize, Option<usize>) {
75        let remaining = self.data.len().saturating_sub(self.pos);
76        let count = remaining / NM_UP_SIZE;
77        (count, Some(count))
78    }
79}
80
81/// Iterator over HEM TS user packets.
82///
83/// Each item is a `[u8; 188]` with the sync byte prepended (0x47).
84/// The 187-byte user packets in the data field have no sync byte.
85/// If NPD is active, DNP bytes are skipped automatically.
86#[derive(Clone, Copy)]
87#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
88pub struct HemTsIter<'a> {
89    data: &'a [u8],
90    pos: usize,
91    npd: bool,
92}
93
94impl<'a> HemTsIter<'a> {
95    /// Create a new HEM TS user packet iterator.
96    ///
97    /// `data` is the full data field (after skipping SYNCD bytes if
98    /// already aligned). The iterator starts at byte 0 of `data`.
99    pub fn new(data: &'a [u8], npd: bool) -> Self {
100        Self { data, pos: 0, npd }
101    }
102
103    /// Return the unconsumed tail of the data field.
104    pub fn remaining(self) -> &'a [u8] {
105        &self.data[self.pos..]
106    }
107}
108
109impl Iterator for HemTsIter<'_> {
110    type Item = [u8; NM_UP_SIZE];
111
112    fn next(&mut self) -> Option<Self::Item> {
113        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
114        if self.pos + stride > self.data.len() {
115            return None;
116        }
117        let mut pkt = [0u8; NM_UP_SIZE];
118        pkt[0] = TS_SYNC_BYTE;
119        pkt[1..].copy_from_slice(&self.data[self.pos..self.pos + HEM_UP_SIZE]);
120        self.pos += stride;
121        Some(pkt)
122    }
123
124    fn size_hint(&self) -> (usize, Option<usize>) {
125        let stride = HEM_UP_SIZE + if self.npd { 1 } else { 0 };
126        let remaining = self.data.len().saturating_sub(self.pos);
127        let count = remaining / stride;
128        (count, Some(count))
129    }
130}
131
132/// Build an appropriate user-packet iterator for the given BBHEADER.
133///
134/// Returns either an NM or HEM iterator depending on the detected mode.
135/// The caller must handle SYNCD alignment before calling this — typically
136/// by skipping `syncd / 8` bytes at the start of the data field.
137pub fn up_iter<'a>(
138    data: &'a [u8],
139    bbheader: &Bbheader,
140) -> Box<dyn Iterator<Item = [u8; NM_UP_SIZE]> + 'a> {
141    match bbheader.mode {
142        Mode::Normal => Box::new(NmTsIter::new(data)),
143        Mode::HighEfficiency => Box::new(HemTsIter::new(data, bbheader.matype.npd)),
144    }
145}
146
147/// Stateful UP extractor that carries partial user packets across BBFrame
148/// boundaries — a single UP can span multiple frames, especially in HEM
149/// where stride=187 bytes.
150///
151/// Use `feed_nm` / `feed_hem` per received frame; the returned Vec holds
152/// whichever 188-byte TS packets completed during that frame.
153pub struct CarryOverExtractor {
154    /// Partial TS packet being assembled (sync byte position 0).
155    buf: [u8; NM_UP_SIZE],
156    /// Bytes already written into `buf`.
157    pos: usize,
158}
159
160impl Default for CarryOverExtractor {
161    fn default() -> Self {
162        Self {
163            buf: [0u8; NM_UP_SIZE],
164            pos: 0,
165        }
166    }
167}
168
169impl CarryOverExtractor {
170    /// Create a fresh extractor with no carried-over state.
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Feed a HEM BBFrame's header + data field. Returns any TS packets that
176    /// completed during this frame.
177    ///
178    /// `npd` is the MATYPE-1 NPD flag for the frame — when true the stream
179    /// would additionally carry DNP bytes between UPs. NPD reinsertion is
180    /// NOT YET implemented here; callers must not pass `npd=true` until
181    /// the DNP path lands.
182    pub fn feed_hem(
183        &mut self,
184        bbheader_bytes: &[u8; BBHEADER_LEN],
185        data_field: &[u8],
186        npd: bool,
187    ) -> Vec<[u8; NM_UP_SIZE]> {
188        let mut out = Vec::new();
189        self.feed_hem_into(bbheader_bytes, data_field, npd, &mut out);
190        out
191    }
192
193    /// Buffer-reusing variant of [`feed_hem`](Self::feed_hem). Clears `out`,
194    /// then appends the TS packets that completed during this frame. Reuse the
195    /// same `Vec` across frames to avoid a per-frame heap allocation.
196    pub fn feed_hem_into(
197        &mut self,
198        bbheader_bytes: &[u8; BBHEADER_LEN],
199        data_field: &[u8],
200        npd: bool,
201        out: &mut Vec<[u8; NM_UP_SIZE]>,
202    ) {
203        out.clear();
204        // NPD/DNP reinsertion is not yet implemented; rather than panic on
205        // wire-derived input, produce no output (out is already cleared).
206        if npd {
207            return;
208        }
209        let hdr = match Bbheader::parse(bbheader_bytes) {
210            Ok(h) => h,
211            Err(_) => return,
212        };
213        // Mismatched mode (caller fed a non-HEM header): no output, no panic.
214        if hdr.mode != Mode::HighEfficiency {
215            return;
216        }
217
218        let stride = HEM_UP_SIZE;
219        let syncd_bytes = (hdr.syncd / 8) as usize;
220        let dfl_bytes = (hdr.dfl / 8) as usize;
221        let data = &data_field[..dfl_bytes.min(data_field.len())];
222
223        // Complete the partial UP from the previous frame.
224        if self.pos > 0 {
225            let need = stride + 1 - self.pos; // +1 for the sync byte we'll prepend
226            if syncd_bytes == need && data.len() >= need {
227                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
228                self.buf[0] = TS_SYNC_BYTE;
229                out.push(self.buf);
230                self.pos = 0;
231            } else {
232                // Stride mismatch — discard partial and resync to syncd.
233                self.pos = 0;
234            }
235        }
236
237        // Extract complete UPs at stride.
238        let mut i = syncd_bytes;
239        while i + stride <= data.len() {
240            // HEM: 187 bytes of UP, prepend sync byte.
241            self.buf[0] = TS_SYNC_BYTE;
242            self.buf[1..1 + stride].copy_from_slice(&data[i..i + stride]);
243            out.push(self.buf);
244            i += stride;
245        }
246
247        // Buffer trailing partial packet (may be filled by next frame).
248        if i < data.len() {
249            let tail = (data.len() - i).min(stride);
250            // Store at offset 1 (reserving byte 0 for the sync we prepend later).
251            self.buf[1..1 + tail].copy_from_slice(&data[i..i + tail]);
252            self.pos = 1 + tail;
253        } else {
254            self.pos = 0;
255        }
256    }
257
258    /// Feed an NM BBFrame. Stride=188, `byte[0]` of each UP is the CRC-8 of
259    /// the previous UP and gets replaced with 0x47.
260    pub fn feed_nm(
261        &mut self,
262        bbheader_bytes: &[u8; BBHEADER_LEN],
263        data_field: &[u8],
264    ) -> Vec<[u8; NM_UP_SIZE]> {
265        let mut out = Vec::new();
266        self.feed_nm_into(bbheader_bytes, data_field, &mut out);
267        out
268    }
269
270    /// Buffer-reusing variant of [`feed_nm`](Self::feed_nm). Clears `out`, then
271    /// appends the TS packets that completed during this frame. Reuse the same
272    /// `Vec` across frames to avoid a per-frame heap allocation.
273    pub fn feed_nm_into(
274        &mut self,
275        bbheader_bytes: &[u8; BBHEADER_LEN],
276        data_field: &[u8],
277        out: &mut Vec<[u8; NM_UP_SIZE]>,
278    ) {
279        out.clear();
280        let hdr = match Bbheader::parse(bbheader_bytes) {
281            Ok(h) => h,
282            Err(_) => return,
283        };
284        // Mismatched mode (caller fed a non-NM header): no output, no panic.
285        if hdr.mode != Mode::Normal {
286            return;
287        }
288
289        let stride = NM_UP_SIZE;
290        let syncd_bytes = (hdr.syncd / 8) as usize;
291        let dfl_bytes = (hdr.dfl / 8) as usize;
292        let data = &data_field[..dfl_bytes.min(data_field.len())];
293
294        // Complete partial UP from previous frame.
295        if self.pos > 0 {
296            let need = stride - self.pos;
297            if syncd_bytes == need && data.len() >= need {
298                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
299                self.buf[0] = TS_SYNC_BYTE; // replace CRC-8 with sync byte
300                out.push(self.buf);
301                self.pos = 0;
302            } else {
303                self.pos = 0;
304            }
305        }
306
307        // Extract complete UPs at stride.
308        let mut i = syncd_bytes;
309        while i + stride <= data.len() {
310            self.buf.copy_from_slice(&data[i..i + stride]);
311            self.buf[0] = TS_SYNC_BYTE; // replace CRC-8 with sync byte
312            out.push(self.buf);
313            i += stride;
314        }
315
316        // Buffer trailing partial.
317        if i < data.len() {
318            let tail = (data.len() - i).min(stride);
319            self.buf[..tail].copy_from_slice(&data[i..i + tail]);
320            self.pos = tail;
321        } else {
322            self.pos = 0;
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::header::{Bbheader, Matype, Mode, TsGs};
331
332    fn make_nm_header(syncd: u16) -> Bbheader {
333        Bbheader {
334            matype: Matype {
335                ts_gs: TsGs::Ts,
336                sis: true,
337                ccm: true,
338                issyi: false,
339                npd: false,
340                ext: 0,
341                isi: 0,
342            },
343            upl: 0,
344            sync: 0x47,
345            dfl: 0,
346            syncd,
347            mode: Mode::Normal,
348            issy_in_header: None,
349        }
350    }
351
352    fn make_hem_header(npd: bool) -> Bbheader {
353        Bbheader {
354            matype: Matype {
355                ts_gs: TsGs::Ts,
356                sis: true,
357                ccm: true,
358                issyi: false,
359                npd,
360                ext: 0,
361                isi: 0,
362            },
363            upl: 0,
364            sync: 0,
365            dfl: 0,
366            syncd: 0,
367            mode: Mode::HighEfficiency,
368            issy_in_header: None,
369        }
370    }
371
372    #[test]
373    fn nm_extracts_single_complete_up() {
374        let mut data = vec![0xAA; NM_UP_SIZE];
375        data[0] = 0xFF; // CRC-8 byte (will be replaced with sync)
376        for (i, byte) in data.iter_mut().enumerate().skip(1) {
377            *byte = i as u8;
378        }
379
380        let _hdr = make_nm_header(0);
381        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
382
383        assert_eq!(pkts.len(), 1);
384        assert_eq!(pkts[0][0], TS_SYNC_BYTE);
385        assert_eq!(&pkts[0][1..], &data[1..]);
386    }
387
388    #[test]
389    fn nm_multiple_back_to_back_ups() {
390        let num_ups = 3;
391        let mut data = Vec::with_capacity(num_ups * NM_UP_SIZE);
392
393        for i in 0..num_ups {
394            data.push(0x00); // CRC-8 byte
395            for j in 1..NM_UP_SIZE {
396                data.push((i * 10 + j) as u8);
397            }
398        }
399
400        let _hdr = make_nm_header(0);
401        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
402
403        assert_eq!(pkts.len(), num_ups);
404        for pkt in &pkts {
405            assert_eq!(pkt[0], TS_SYNC_BYTE);
406        }
407    }
408
409    #[test]
410    fn nm_partial_tail_does_not_yield() {
411        let mut data = vec![0xAA; NM_UP_SIZE + 50];
412        data[0] = 0xFF; // CRC-8
413        for (i, byte) in data.iter_mut().enumerate().skip(1) {
414            *byte = i as u8;
415        }
416
417        let _hdr = make_nm_header(0);
418        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
419
420        assert_eq!(pkts.len(), 1); // Only one complete UP
421    }
422
423    #[test]
424    fn nm_crc_byte_replaced_with_sync_only() {
425        let mut data = vec![0u8; NM_UP_SIZE];
426        data[0] = 0x42; // Some CRC value
427        data[1] = 0x47; // Actual sync byte in payload position 1
428        for (i, byte) in data.iter_mut().enumerate().skip(2) {
429            *byte = i as u8;
430        }
431
432        let _hdr = make_nm_header(0);
433        let pkt = up_iter(&data, &_hdr).next().unwrap();
434
435        assert_eq!(pkt[0], TS_SYNC_BYTE); // CRC replaced
436        assert_eq!(pkt[1], 0x47); // Original byte preserved
437        assert_eq!(&pkt[2..], &data[2..]);
438    }
439
440    #[test]
441    fn hem_extracts_up_with_sync_prepend() {
442        let data: Vec<u8> = (0..HEM_UP_SIZE as u8).cycle().take(HEM_UP_SIZE).collect();
443        let mut expected = [0u8; NM_UP_SIZE];
444        expected[0] = TS_SYNC_BYTE;
445        expected[1..].copy_from_slice(&data[..HEM_UP_SIZE]);
446
447        let _hdr = make_hem_header(false);
448        let pkt = up_iter(&data, &_hdr).next().unwrap();
449
450        assert_eq!(pkt, expected);
451    }
452
453    #[test]
454    fn hem_multiple_ups_without_npd() {
455        let num_ups = 3;
456        let data = vec![0xAB; num_ups * HEM_UP_SIZE];
457        let expected: [u8; NM_UP_SIZE] = {
458            let mut e = [0xAB; NM_UP_SIZE];
459            e[0] = TS_SYNC_BYTE;
460            e
461        };
462
463        let _hdr = make_hem_header(false);
464        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
465
466        assert_eq!(pkts.len(), num_ups);
467        for pkt in &pkts {
468            assert_eq!(*pkt, expected);
469        }
470    }
471
472    #[test]
473    fn hem_with_npd_skips_dnp_bytes() {
474        let up_size = HEM_UP_SIZE;
475        let num_ups = 2;
476        // Each UP followed by a DNP byte
477        let stride = up_size + 1;
478        let mut data = Vec::with_capacity(num_ups * stride);
479
480        for i in 0..num_ups {
481            for j in 0..up_size {
482                data.push((i * up_size + j) as u8);
483            }
484            data.push(i as u8); // DNP counter
485        }
486
487        let _hdr = make_hem_header(true);
488        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
489
490        assert_eq!(pkts.len(), num_ups);
491        for (i, pkt) in pkts.iter().enumerate() {
492            assert_eq!(pkt[0], TS_SYNC_BYTE);
493            // Verify the 187 bytes match the expected slice
494            let offset = i * stride;
495            assert_eq!(&pkt[1..], &data[offset..offset + up_size]);
496        }
497    }
498
499    #[test]
500    fn nm_remaining_returns_unconsumed_tail() {
501        let data = vec![0xAA; NM_UP_SIZE * 2 + 50];
502        let _hdr = make_nm_header(0);
503        let mut iter = NmTsIter::new(&data);
504
505        let _p1 = iter.next().unwrap();
506        let _p2 = iter.next().unwrap();
507        let remaining = iter.remaining();
508
509        assert_eq!(remaining.len(), 50);
510    }
511
512    #[test]
513    fn hem_remaining_returns_unconsumed_tail() {
514        let data = vec![0xAA; HEM_UP_SIZE * 2 + 30];
515        let _hdr = make_hem_header(false);
516        let mut iter = HemTsIter::new(&data, false);
517
518        let _p1 = iter.next().unwrap();
519        let _p2 = iter.next().unwrap();
520        let remaining = iter.remaining();
521
522        assert_eq!(remaining.len(), 30);
523    }
524
525    #[test]
526    fn empty_data_yields_nothing() {
527        let _hdr = make_nm_header(0);
528        let pkts: Vec<_> = up_iter(&[], &_hdr).collect();
529        assert!(pkts.is_empty());
530    }
531
532    #[test]
533    fn data_shorter_than_one_up_yields_nothing() {
534        let data = vec![0xAA; 100]; // Less than NM_UP_SIZE or HEM_UP_SIZE
535        let _hdr = make_nm_header(0);
536        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
537        assert!(pkts.is_empty());
538    }
539
540    #[test]
541    fn carry_over_extractor_emits_ts_across_two_bbframes_hem() {
542        // Two HEM BBFrames where the first ends with a partial UP (70 bytes) and
543        // the second completes it (117 bytes) then starts a new UP.
544        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
545            let mut h = [0u8; 10];
546            // MATYPE-1: TS=0b11 → 0xC0, SIS=1 → 0x20, CCM=1 → 0x10, ISSYI=0, NPD=0, EXT=0
547            h[0] = 0xF0;
548            h[1] = 0x00; // ISI=0
549                         // UPL=0 (ignored in HEM)
550            h[2] = 0x00;
551            h[3] = 0x00;
552            h[4] = (dfl_bits >> 8) as u8;
553            h[5] = (dfl_bits & 0xFF) as u8;
554            h[6] = 0x00; // sync (ignored in HEM)
555            h[7] = (syncd_bits >> 8) as u8;
556            h[8] = (syncd_bits & 0xFF) as u8;
557            // byte 9 = CRC-8 XOR MODE with MODE=1 for HEM
558            let crc = crate::crc::crc8(&h[..9]);
559            h[9] = crc ^ 1;
560            h
561        };
562
563        // Two BBFrames, each 70 data bytes. Pattern: each data byte is (frame << 4) | offset_lo.
564        // We expect CarryOverExtractor to produce 0 packets on frame1 (partial tail),
565        // then 1 on frame2 (187-byte completion across the boundary).
566        let frame1_data = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
567        let frame2_data = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
568        let hdr1 = make_hem_header(0, (frame1_data.len() * 8) as u16);
569        let hdr2 = make_hem_header(0, (frame2_data.len() * 8) as u16);
570
571        let mut extractor = CarryOverExtractor::new();
572        let packets1 = extractor.feed_hem(&hdr1, &frame1_data, false);
573        assert_eq!(packets1.len(), 0, "70 bytes (< 187) must not yet emit a UP");
574
575        let packets2 = extractor.feed_hem(&hdr2, &frame2_data, false);
576        assert!(!packets2.is_empty(), "boundary UP should complete");
577        assert_eq!(
578            packets2[0][0], 0x47,
579            "first emitted packet has sync byte prepended"
580        );
581    }
582
583    #[test]
584    fn feed_into_matches_allocating_api() {
585        // Run the same two-frame HEM sequence (partial UP carried across the
586        // boundary) through the allocating `feed_hem` and the buffer-reusing
587        // `feed_hem_into` (one Vec reused across both frames). Identical output
588        // proves `_into` clears + appends equivalently — if it failed to clear,
589        // frame 2's buffer would still hold frame 1's packets and diverge.
590        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
591            let mut h = [0u8; 10];
592            h[0] = 0xF0;
593            h[4] = (dfl_bits >> 8) as u8;
594            h[5] = (dfl_bits & 0xFF) as u8;
595            h[7] = (syncd_bits >> 8) as u8;
596            h[8] = (syncd_bits & 0xFF) as u8;
597            let crc = crate::crc::crc8(&h[..9]);
598            h[9] = crc ^ 1;
599            h
600        };
601        let f1 = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
602        let f2 = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
603        let h1 = make_hem_header(0, (f1.len() * 8) as u16);
604        let h2 = make_hem_header(0, (f2.len() * 8) as u16);
605
606        let mut alloc = CarryOverExtractor::new();
607        let a1 = alloc.feed_hem(&h1, &f1, false);
608        let a2 = alloc.feed_hem(&h2, &f2, false);
609
610        let mut reuse = CarryOverExtractor::new();
611        let mut buf = Vec::new();
612        reuse.feed_hem_into(&h1, &f1, false, &mut buf);
613        let b1 = buf.clone();
614        reuse.feed_hem_into(&h2, &f2, false, &mut buf);
615        let b2 = buf.clone();
616
617        assert_eq!(a1, b1, "frame 1 output matches across APIs");
618        assert_eq!(
619            a2, b2,
620            "frame 2 (carry-over) output matches; buffer was cleared"
621        );
622    }
623}