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