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        assert!(
189            !npd,
190            "CarryOverExtractor does not yet implement NPD/DNP reinsertion"
191        );
192        let hdr = match Bbheader::parse(bbheader_bytes) {
193            Ok(h) => h,
194            Err(_) => return Vec::new(),
195        };
196        assert_eq!(
197            hdr.mode,
198            Mode::HighEfficiency,
199            "feed_hem called on non-HEM header"
200        );
201
202        let stride = HEM_UP_SIZE;
203        let syncd_bytes = (hdr.syncd / 8) as usize;
204        let dfl_bytes = (hdr.dfl / 8) as usize;
205        let data = &data_field[..dfl_bytes.min(data_field.len())];
206
207        let mut out = Vec::new();
208
209        // Complete the partial UP from the previous frame.
210        if self.pos > 0 {
211            let need = stride + 1 - self.pos; // +1 for the sync byte we'll prepend
212            if syncd_bytes == need && data.len() >= need {
213                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
214                self.buf[0] = 0x47;
215                out.push(self.buf);
216                self.pos = 0;
217            } else {
218                // Stride mismatch — discard partial and resync to syncd.
219                self.pos = 0;
220            }
221        }
222
223        // Extract complete UPs at stride.
224        let mut i = syncd_bytes;
225        while i + stride <= data.len() {
226            // HEM: 187 bytes of UP, prepend 0x47.
227            self.buf[0] = 0x47;
228            self.buf[1..1 + stride].copy_from_slice(&data[i..i + stride]);
229            out.push(self.buf);
230            i += stride;
231        }
232
233        // Buffer trailing partial packet (may be filled by next frame).
234        if i < data.len() {
235            let tail = (data.len() - i).min(stride);
236            // Store at offset 1 (reserving byte 0 for the sync we prepend later).
237            self.buf[1..1 + tail].copy_from_slice(&data[i..i + tail]);
238            self.pos = 1 + tail;
239        } else {
240            self.pos = 0;
241        }
242
243        out
244    }
245
246    /// Feed an NM BBFrame. Stride=188, `byte[0]` of each UP is the CRC-8 of
247    /// the previous UP and gets replaced with 0x47.
248    pub fn feed_nm(
249        &mut self,
250        bbheader_bytes: &[u8; BBHEADER_LEN],
251        data_field: &[u8],
252    ) -> Vec<[u8; NM_UP_SIZE]> {
253        let hdr = match Bbheader::parse(bbheader_bytes) {
254            Ok(h) => h,
255            Err(_) => return Vec::new(),
256        };
257        assert_eq!(hdr.mode, Mode::Normal, "feed_nm called on non-NM header");
258
259        let stride = NM_UP_SIZE;
260        let syncd_bytes = (hdr.syncd / 8) as usize;
261        let dfl_bytes = (hdr.dfl / 8) as usize;
262        let data = &data_field[..dfl_bytes.min(data_field.len())];
263
264        let mut out = Vec::new();
265
266        // Complete partial UP from previous frame.
267        if self.pos > 0 {
268            let need = stride - self.pos;
269            if syncd_bytes == need && data.len() >= need {
270                self.buf[self.pos..self.pos + need].copy_from_slice(&data[..need]);
271                self.buf[0] = 0x47; // replace CRC-8 with sync byte
272                out.push(self.buf);
273                self.pos = 0;
274            } else {
275                self.pos = 0;
276            }
277        }
278
279        // Extract complete UPs at stride.
280        let mut i = syncd_bytes;
281        while i + stride <= data.len() {
282            self.buf.copy_from_slice(&data[i..i + stride]);
283            self.buf[0] = 0x47; // replace CRC-8 with sync byte
284            out.push(self.buf);
285            i += stride;
286        }
287
288        // Buffer trailing partial.
289        if i < data.len() {
290            let tail = (data.len() - i).min(stride);
291            self.buf[..tail].copy_from_slice(&data[i..i + tail]);
292            self.pos = tail;
293        } else {
294            self.pos = 0;
295        }
296
297        out
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::header::{Bbheader, Matype, Mode, TsGs};
305
306    fn make_nm_header(syncd: u16) -> Bbheader {
307        Bbheader {
308            matype: Matype {
309                ts_gs: TsGs::Ts,
310                sis: true,
311                ccm: true,
312                issyi: false,
313                npd: false,
314                ext: 0,
315                isi: 0,
316            },
317            upl: 0,
318            sync: 0x47,
319            dfl: 0,
320            syncd,
321            mode: Mode::Normal,
322            issy_in_header: None,
323        }
324    }
325
326    fn make_hem_header(npd: bool) -> Bbheader {
327        Bbheader {
328            matype: Matype {
329                ts_gs: TsGs::Ts,
330                sis: true,
331                ccm: true,
332                issyi: false,
333                npd,
334                ext: 0,
335                isi: 0,
336            },
337            upl: 0,
338            sync: 0,
339            dfl: 0,
340            syncd: 0,
341            mode: Mode::HighEfficiency,
342            issy_in_header: None,
343        }
344    }
345
346    #[test]
347    fn nm_extracts_single_complete_up() {
348        let mut data = vec![0xAA; NM_UP_SIZE];
349        data[0] = 0xFF; // CRC-8 byte (will be replaced with sync)
350        for (i, byte) in data.iter_mut().enumerate().skip(1) {
351            *byte = i as u8;
352        }
353
354        let _hdr = make_nm_header(0);
355        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
356
357        assert_eq!(pkts.len(), 1);
358        assert_eq!(pkts[0][0], TS_SYNC_BYTE);
359        assert_eq!(&pkts[0][1..], &data[1..]);
360    }
361
362    #[test]
363    fn nm_multiple_back_to_back_ups() {
364        let num_ups = 3;
365        let mut data = Vec::with_capacity(num_ups * NM_UP_SIZE);
366
367        for i in 0..num_ups {
368            data.push(0x00); // CRC-8 byte
369            for j in 1..NM_UP_SIZE {
370                data.push((i * 10 + j) as u8);
371            }
372        }
373
374        let _hdr = make_nm_header(0);
375        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
376
377        assert_eq!(pkts.len(), num_ups);
378        for pkt in &pkts {
379            assert_eq!(pkt[0], TS_SYNC_BYTE);
380        }
381    }
382
383    #[test]
384    fn nm_partial_tail_does_not_yield() {
385        let mut data = vec![0xAA; NM_UP_SIZE + 50];
386        data[0] = 0xFF; // CRC-8
387        for (i, byte) in data.iter_mut().enumerate().skip(1) {
388            *byte = i as u8;
389        }
390
391        let _hdr = make_nm_header(0);
392        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
393
394        assert_eq!(pkts.len(), 1); // Only one complete UP
395    }
396
397    #[test]
398    fn nm_crc_byte_replaced_with_sync_only() {
399        let mut data = vec![0u8; NM_UP_SIZE];
400        data[0] = 0x42; // Some CRC value
401        data[1] = 0x47; // Actual sync byte in payload position 1
402        for (i, byte) in data.iter_mut().enumerate().skip(2) {
403            *byte = i as u8;
404        }
405
406        let _hdr = make_nm_header(0);
407        let pkt = up_iter(&data, &_hdr).next().unwrap();
408
409        assert_eq!(pkt[0], TS_SYNC_BYTE); // CRC replaced
410        assert_eq!(pkt[1], 0x47); // Original byte preserved
411        assert_eq!(&pkt[2..], &data[2..]);
412    }
413
414    #[test]
415    fn hem_extracts_up_with_sync_prepend() {
416        let data: Vec<u8> = (0..HEM_UP_SIZE as u8).cycle().take(HEM_UP_SIZE).collect();
417        let mut expected = [0u8; NM_UP_SIZE];
418        expected[0] = TS_SYNC_BYTE;
419        expected[1..].copy_from_slice(&data[..HEM_UP_SIZE]);
420
421        let _hdr = make_hem_header(false);
422        let pkt = up_iter(&data, &_hdr).next().unwrap();
423
424        assert_eq!(pkt, expected);
425    }
426
427    #[test]
428    fn hem_multiple_ups_without_npd() {
429        let num_ups = 3;
430        let data = vec![0xAB; num_ups * HEM_UP_SIZE];
431        let expected: [u8; NM_UP_SIZE] = {
432            let mut e = [0xAB; NM_UP_SIZE];
433            e[0] = TS_SYNC_BYTE;
434            e
435        };
436
437        let _hdr = make_hem_header(false);
438        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
439
440        assert_eq!(pkts.len(), num_ups);
441        for pkt in &pkts {
442            assert_eq!(*pkt, expected);
443        }
444    }
445
446    #[test]
447    fn hem_with_npd_skips_dnp_bytes() {
448        let up_size = HEM_UP_SIZE;
449        let num_ups = 2;
450        // Each UP followed by a DNP byte
451        let stride = up_size + 1;
452        let mut data = Vec::with_capacity(num_ups * stride);
453
454        for i in 0..num_ups {
455            for j in 0..up_size {
456                data.push((i * up_size + j) as u8);
457            }
458            data.push(i as u8); // DNP counter
459        }
460
461        let _hdr = make_hem_header(true);
462        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
463
464        assert_eq!(pkts.len(), num_ups);
465        for (i, pkt) in pkts.iter().enumerate() {
466            assert_eq!(pkt[0], TS_SYNC_BYTE);
467            // Verify the 187 bytes match the expected slice
468            let offset = i * stride;
469            assert_eq!(&pkt[1..], &data[offset..offset + up_size]);
470        }
471    }
472
473    #[test]
474    fn nm_remaining_returns_unconsumed_tail() {
475        let data = vec![0xAA; NM_UP_SIZE * 2 + 50];
476        let _hdr = make_nm_header(0);
477        let mut iter = NmTsIter::new(&data);
478
479        let _p1 = iter.next().unwrap();
480        let _p2 = iter.next().unwrap();
481        let remaining = iter.remaining();
482
483        assert_eq!(remaining.len(), 50);
484    }
485
486    #[test]
487    fn hem_remaining_returns_unconsumed_tail() {
488        let data = vec![0xAA; HEM_UP_SIZE * 2 + 30];
489        let _hdr = make_hem_header(false);
490        let mut iter = HemTsIter::new(&data, false);
491
492        let _p1 = iter.next().unwrap();
493        let _p2 = iter.next().unwrap();
494        let remaining = iter.remaining();
495
496        assert_eq!(remaining.len(), 30);
497    }
498
499    #[test]
500    fn empty_data_yields_nothing() {
501        let _hdr = make_nm_header(0);
502        let pkts: Vec<_> = up_iter(&[], &_hdr).collect();
503        assert!(pkts.is_empty());
504    }
505
506    #[test]
507    fn data_shorter_than_one_up_yields_nothing() {
508        let data = vec![0xAA; 100]; // Less than NM_UP_SIZE or HEM_UP_SIZE
509        let _hdr = make_nm_header(0);
510        let pkts: Vec<_> = up_iter(&data, &_hdr).collect();
511        assert!(pkts.is_empty());
512    }
513
514    #[test]
515    fn carry_over_extractor_emits_ts_across_two_bbframes_hem() {
516        // Two HEM BBFrames where the first ends with a partial UP (70 bytes) and
517        // the second completes it (117 bytes) then starts a new UP.
518        let make_hem_header = |syncd_bits: u16, dfl_bits: u16| -> [u8; 10] {
519            let mut h = [0u8; 10];
520            // MATYPE-1: TS=0b11 → 0xC0, SIS=1 → 0x20, CCM=1 → 0x10, ISSYI=0, NPD=0, EXT=0
521            h[0] = 0xF0;
522            h[1] = 0x00; // ISI=0
523                         // UPL=0 (ignored in HEM)
524            h[2] = 0x00;
525            h[3] = 0x00;
526            h[4] = (dfl_bits >> 8) as u8;
527            h[5] = (dfl_bits & 0xFF) as u8;
528            h[6] = 0x00; // sync (ignored in HEM)
529            h[7] = (syncd_bits >> 8) as u8;
530            h[8] = (syncd_bits & 0xFF) as u8;
531            // byte 9 = CRC-8 XOR MODE with MODE=1 for HEM
532            let crc = crate::crc::crc8(&h[..9]);
533            h[9] = crc ^ 1;
534            h
535        };
536
537        // Two BBFrames, each 70 data bytes. Pattern: each data byte is (frame << 4) | offset_lo.
538        // We expect CarryOverExtractor to produce 0 packets on frame1 (partial tail),
539        // then 1 on frame2 (187-byte completion across the boundary).
540        let frame1_data = (0..70u8).map(|i| 0xA0 | (i & 0x0F)).collect::<Vec<u8>>();
541        let frame2_data = (0..200u8).map(|i| 0xB0 | (i & 0x0F)).collect::<Vec<u8>>();
542        let hdr1 = make_hem_header(0, (frame1_data.len() * 8) as u16);
543        let hdr2 = make_hem_header(0, (frame2_data.len() * 8) as u16);
544
545        let mut extractor = CarryOverExtractor::new();
546        let packets1 = extractor.feed_hem(&hdr1, &frame1_data, false);
547        assert_eq!(packets1.len(), 0, "70 bytes (< 187) must not yet emit a UP");
548
549        let packets2 = extractor.feed_hem(&hdr2, &frame2_data, false);
550        assert!(!packets2.is_empty(), "boundary UP should complete");
551        assert_eq!(
552            packets2[0][0], 0x47,
553            "first emitted packet has sync byte prepended"
554        );
555    }
556}