Skip to main content

mpeg_ts/
pusi.rs

1//! PUSI-delimited payload reassembler — generic non-PSI PID payload accumulation.
2//!
3//! In MPEG-2 TS, a PID carrying packetised data that is **not** PSI section
4//! data uses `payload_unit_start_indicator` (PUSI) to delimit units: the
5//! packet whose PUSI == `1` starts a new unit. This is the mechanism used by
6//! ISO/IEC 23009-1 §5.10.3.3.5 to carry a DASH `emsg` box on reserved PID
7//! `0x0004` — the `emsg` box begins in the PUSI packet's payload, continues
8//! across zero or more non-PUSI packets on the same PID, and its last packet
9//! is padded with **adaptation-field stuffing** (not payload stuffing), so
10//! the concatenation of the PID's payload bytes across a PUSI-delimited run
11//! equals the box bytes exactly.
12//!
13//! [`PusiReassembler`] implements this generic reassembly: feed TS payload
14//! bytes together with the PID and PUSI flag; it returns
15//! `Option<Vec<u8>>` when a unit completes. This type is not `emsg`-specific
16//! — it works for any PUSI-delimited non-PSI PID payload.
17
18use alloc::vec::Vec;
19use broadcast_common::pusi::PusiAccumulator;
20
21/// Default maximum accumulated unit size (16 MiB), shared with `mpeg-pes`'s
22/// `PesAssembler` through [`broadcast_common::pusi`] (audit r01-W13, #1074).
23pub use broadcast_common::pusi::DEFAULT_MAX_UNIT_SIZE;
24
25/// Generic PUSI-delimited payload reassembler: a PID filter over the shared
26/// [`PusiAccumulator`].
27///
28/// Accumulates payload bytes across consecutive TS packets sharing the same
29/// PID, using `payload_unit_start_indicator` to delimit unit boundaries.
30///
31/// # Example
32///
33/// ```ignore
34/// let mut reassembler = PusiReassembler::new(0x0004);
35///
36/// // Feed pusi=true first packet
37/// if let Some(unit) = reassembler.push(0x0004, true, &first_payload) {
38///     // unit is complete (would only happen if a second PUSI follows)
39/// }
40///
41/// // Feed continuation
42/// reassembler.push(0x0004, false, &cont_payload);
43///
44/// // Drain the final unit
45/// if let Some(unit) = reassembler.flush() {
46///     // process complete unit
47/// }
48/// ```
49#[derive(Debug, Clone)]
50pub struct PusiReassembler {
51    /// The PID we are listening to. Packets with a different PID are ignored.
52    pid: u16,
53    inner: PusiAccumulator,
54}
55
56impl PusiReassembler {
57    /// Create a new reassembler for the given `pid` using the default maximum unit size.
58    #[inline]
59    pub fn new(pid: u16) -> Self {
60        Self {
61            pid,
62            inner: PusiAccumulator::new(),
63        }
64    }
65
66    /// Create a new reassembler with a custom maximum unit size.
67    #[inline]
68    pub fn with_max_unit_size(pid: u16, max: usize) -> Self {
69        Self {
70            pid,
71            inner: PusiAccumulator::with_max_unit_size(max),
72        }
73    }
74
75    /// Feed one TS packet's (pid, payload_unit_start_indicator, payload bytes).
76    ///
77    /// Packets whose `pid != self.pid` are silently ignored (returns `None`).
78    ///
79    /// **PUSI semantics**: When `pusi == true`, the packet marks the start of a
80    /// *new* unit. If a unit was already in progress, it is **complete** — it
81    /// is returned as `Some(unit_bytes)` (an empty unit is not returned), and
82    /// a fresh unit begins with this packet's payload. When `pusi == false`,
83    /// the payload is appended to the in-progress unit; payload before the
84    /// first PUSI is ignored (ISO/IEC 13818-1 §2.4.3.2), and a unit past the
85    /// size cap is discarded until the next PUSI.
86    pub fn push(&mut self, pid: u16, pusi: bool, payload: &[u8]) -> Option<Vec<u8>> {
87        if pid != self.pid {
88            return None;
89        }
90        self.inner.feed(pusi, payload)
91    }
92
93    /// Return any in-progress (final) unit; the caller drains this at end of
94    /// the PID's data (the last real box has no following PUSI to close it).
95    ///
96    /// Returns `None` when no data has been accumulated.
97    pub fn flush(&mut self) -> Option<Vec<u8>> {
98        self.inner.flush()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use alloc::vec;
106
107    /// A synthetic "box" larger than a single TS payload (184 bytes).
108    /// We use a valid big-endian size prefix so the test is realistic.
109    const BIG_BOX_SIZE: u32 = 400;
110    /// Build a synthetic box with a valid 4-byte big-endian size and 'test' type.
111    fn synthetic_box_bytes(size: u32) -> Vec<u8> {
112        let n = size as usize;
113        let mut bytes = Vec::with_capacity(n);
114        bytes.extend_from_slice(&size.to_be_bytes()); // size
115        bytes.extend_from_slice(b"test"); // type
116        // Fill the rest (starting at offset 8) with a counter pattern.
117        for i in 8..n {
118            bytes.push((i & 0xFF) as u8);
119        }
120        debug_assert_eq!(bytes.len(), n);
121        bytes
122    }
123
124    #[test]
125    fn spanning_across_two_packets() {
126        let box_bytes = synthetic_box_bytes(BIG_BOX_SIZE);
127        let pid = 0x0004u16;
128
129        // Split at a boundary that fits in a single TS payload (≤ 184 bytes).
130        let chunk1 = &box_bytes[..184];
131        let chunk2 = &box_bytes[184..];
132
133        let mut reasm = PusiReassembler::new(pid);
134
135        // Push first chunk with PUSI = true.
136        assert!(reasm.push(pid, true, chunk1).is_none());
137
138        // Push second chunk (continuation).
139        assert!(reasm.push(pid, false, chunk2).is_none());
140
141        // Flush and verify.
142        let reassembled = reasm.flush().expect("flush should return the unit");
143        assert_eq!(reassembled, box_bytes);
144    }
145
146    #[test]
147    fn boundary_two_units() {
148        let unit_a = b"AAAA-first-unit-data";
149        let unit_b = b"BBBB-second-unit-data";
150        let pid = 0x0004u16;
151
152        let mut reasm = PusiReassembler::new(pid);
153
154        // Push unit A (PUSI start).
155        assert!(reasm.push(pid, true, unit_a).is_none());
156
157        // Push unit B (PUSI start) — this should close unit A and return it.
158        let closed = reasm.push(pid, true, unit_b);
159        assert_eq!(closed.as_deref(), Some(unit_a.as_slice()));
160
161        // Flush should return unit B.
162        let flushed = reasm.flush();
163        assert_eq!(flushed.as_deref(), Some(unit_b.as_slice()));
164    }
165
166    #[test]
167    fn different_pid_ignored() {
168        let pid = 0x0004u16;
169        let mut reasm = PusiReassembler::new(pid);
170
171        // Packet with different PID should be ignored.
172        assert!(reasm.push(0x0100, true, b"ignored").is_none());
173        assert!(reasm.push(0x0100, false, b"more-ignored").is_none());
174        assert!(reasm.flush().is_none());
175
176        // Now feed a packet on our PID.
177        assert!(reasm.push(pid, true, b"real-data").is_none());
178        let flushed = reasm.flush();
179        assert_eq!(flushed.as_deref(), Some(b"real-data".as_slice()));
180    }
181
182    #[test]
183    fn flush_empty_returns_none() {
184        let mut reasm = PusiReassembler::new(0x0004);
185        assert!(reasm.flush().is_none());
186    }
187
188    #[test]
189    fn single_packet_unit() {
190        let pid = 0x0004u16;
191        let mut reasm = PusiReassembler::new(pid);
192
193        assert!(reasm.push(pid, true, b"single-packet-emsg").is_none());
194        let flushed = reasm.flush();
195        assert_eq!(flushed.as_deref(), Some(b"single-packet-emsg".as_slice()));
196    }
197
198    #[test]
199    fn ignores_payload_before_first_pusi() {
200        let pid = 0x0004u16;
201        let mut reasm = PusiReassembler::new(pid);
202
203        // Feed non-PUSI payloads before any PUSI — these should be ignored.
204        assert!(reasm.push(pid, false, b"junk1").is_none());
205        assert!(reasm.push(pid, false, b"junk2").is_none());
206
207        // First PUSI starts a new unit.
208        assert!(reasm.push(pid, true, b"real-data").is_none());
209
210        let flushed = reasm.flush();
211        // The flushed unit must be exactly "real-data", not "junk1junk2real-data".
212        assert_eq!(flushed.as_deref(), Some(b"real-data".as_slice()));
213    }
214
215    #[test]
216    fn caps_buffer_at_default_max_unit_size() {
217        let pid = 0x0004u16;
218        let mut reasm = PusiReassembler::new(pid);
219
220        // Start with PUSI.
221        assert!(reasm.push(pid, true, &[0u8; 10]).is_none());
222
223        // Verify the default cap is DEFAULT_MAX_UNIT_SIZE.
224        assert_eq!(reasm.inner.max_unit_size(), DEFAULT_MAX_UNIT_SIZE);
225
226        // Flush and verify.
227        let flushed = reasm.flush();
228        assert_eq!(flushed.as_deref(), Some(&[0u8; 10][..]));
229    }
230
231    #[test]
232    fn custom_cap_discards_oversized_unit() {
233        let pid = 0x0004u16;
234        let custom_cap = 1000usize;
235        let mut reasm = PusiReassembler::with_max_unit_size(pid, custom_cap);
236
237        // Start with PUSI.
238        assert!(reasm.push(pid, true, &[0u8; 10]).is_none());
239
240        // Try to feed enough non-PUSI continuation to exceed the custom cap.
241        let chunk = vec![0x42u8; 600];
242        reasm.push(pid, false, &chunk);
243
244        // The second chunk should trigger the cap check and reset.
245        reasm.push(pid, false, &chunk);
246
247        // A new PUSI returns None (prior oversized unit was discarded).
248        let completed = reasm.push(pid, true, b"new-unit");
249        assert!(completed.is_none());
250
251        // Flush the final unit.
252        let flushed = reasm.flush();
253        assert_eq!(flushed.as_deref(), Some(b"new-unit".as_slice()));
254    }
255
256    #[test]
257    fn one_mib_unit_reassembles_with_default_cap() {
258        let pid = 0x0004u16;
259        let mut reasm = PusiReassembler::new(pid);
260        let unit_size = 1024 * 1024usize;
261
262        // Start with PUSI.
263        assert!(reasm.push(pid, true, &[0u8; 10000]).is_none());
264
265        // Feed 1 MiB in chunks, well within the default 16 MiB cap.
266        let chunk = vec![0xAB; 256 * 1024];
267        for _ in 0..3 {
268            reasm.push(pid, false, &chunk);
269        }
270
271        // Flush and verify the unit is complete.
272        let flushed = reasm.flush();
273        assert!(flushed.is_some());
274        assert!(flushed.as_ref().unwrap().len() > unit_size / 2);
275    }
276}