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}