ts_fix/ops/stuffing.rs
1//! Null packet stuffing / drop operation.
2//!
3//! Inserts null (PID 0x1FFF) packets to reach a target packet rate, or drops
4//! existing null packets.
5//!
6//! # Null packet format
7//!
8//! A null TS packet is a standard 188-byte TS packet with:
9//! - PID = 0x1FFF (ISO/IEC 13818-1 §2.4.1)
10//! - Adaptation field control = 01 (payload only, no adaptation field)
11//! - No payload; rest is 0xFF padding
12//!
13//! Null packets are built via [`mpeg_ts::OwnedTsPacket::null_packet`]; this
14//! module contains no raw wire-byte construction.
15//!
16//! # Spec
17//!
18//! ISO/IEC 13818-1 (= ITU-T H.222.0) §2.4.1 (null packets).
19
20use mpeg_ts::owned::OwnedTsPacket;
21use mpeg_ts::ts::TS_PACKET_SIZE;
22
23use crate::ops::{Op, StreamModel};
24
25/// Null packet PID (ISO/IEC 13818-1 §2.4.1).
26const NULL_PID: u16 = 0x1FFF;
27
28/// Configuration for [`TsFixBuilder::stuffing`](crate::TsFixBuilder::stuffing).
29///
30/// `#[non_exhaustive]` — future modes may be added without a breaking change.
31#[non_exhaustive]
32#[derive(Debug, Clone)]
33pub enum Stuffing {
34 /// Drop all null packets (PID 0x1FFF) from the output.
35 ///
36 /// Constructed via [`Stuffing::drop_nulls`].
37 DropNulls,
38
39 /// Insert null packets to reach a target packet rate.
40 ///
41 /// Constructed via [`Stuffing::pad_to`].
42 PadTo {
43 /// Target number of packets per input packet.
44 ///
45 /// If the input has 100 packets and `packets_per_input` is 2,
46 /// the output will have approximately 200 packets (100 input × 2).
47 /// Fractional scaling is achieved by accumulating the rate across
48 /// the stream.
49 packets_per_input: f64,
50 },
51}
52
53impl Stuffing {
54 /// Drop all null packets from the output.
55 ///
56 /// # Example
57 /// ```
58 /// use ts_fix::Stuffing;
59 /// let cfg = Stuffing::drop_nulls();
60 /// ```
61 pub fn drop_nulls() -> Self {
62 Self::DropNulls
63 }
64
65 /// Pad the stream to a target packet rate.
66 ///
67 /// Inserts null packets (PID 0x1FFF) after each input packet such that the
68 /// total output is approximately `packets_per_input × number_of_input_packets`.
69 ///
70 /// The rate is accumulated across the stream; fractional rates are smoothed.
71 ///
72 /// # Example — double the packet count
73 /// ```
74 /// use ts_fix::Stuffing;
75 /// let cfg = Stuffing::pad_to(2.0);
76 /// ```
77 pub fn pad_to(packets_per_input: f64) -> Self {
78 Self::PadTo { packets_per_input }
79 }
80}
81
82// ── Null packet construction ──────────────────────────────────────────────
83
84/// Construct a standard null TS packet via the mpeg-ts writer.
85fn make_null_packet(continuity_counter: u8) -> [u8; TS_PACKET_SIZE] {
86 OwnedTsPacket::null_packet(continuity_counter)
87}
88
89// ── The operation ────────────────────────────────────────────────────────────
90
91/// Null packet stuffing / drop operation.
92pub(crate) struct StuffingOp {
93 mode: StuffingMode,
94}
95
96enum StuffingMode {
97 /// Drop all null packets.
98 Drop,
99 /// Pad to a target rate.
100 Pad {
101 /// How many null packets to insert per real packet (derived from rate).
102 nulls_per_real: f64,
103 /// Accumulated fractional packets; when ≥ 1.0, emit a null packet.
104 accumulated: f64,
105 /// Null packet continuity counter (per ISO 13818-1, CCs wrap 0-15).
106 null_cc: u8,
107 },
108}
109
110impl StuffingOp {
111 pub(crate) fn new(cfg: Stuffing) -> Self {
112 let mode = match cfg {
113 Stuffing::DropNulls => StuffingMode::Drop,
114 Stuffing::PadTo { packets_per_input } => {
115 // If the target is 2.0, we want 2 total packets per input packet.
116 // That means 1 null packet per input packet.
117 let nulls_per_real = packets_per_input - 1.0;
118 StuffingMode::Pad {
119 nulls_per_real,
120 accumulated: 0.0,
121 null_cc: 0,
122 }
123 }
124 };
125 Self { mode }
126 }
127
128 /// Check if a packet is a null packet (PID 0x1FFF).
129 fn is_null_packet(packet: &[u8]) -> bool {
130 if packet.len() < 3 {
131 return false;
132 }
133 // Extract PID from bytes 1-2.
134 let pid = (((packet[1] & 0x1F) as u16) << 8) | packet[2] as u16;
135 pid == NULL_PID
136 }
137}
138
139impl Op for StuffingOp {
140 fn process(&mut self, packet: &[u8], _model: &mut StreamModel, out: &mut dyn FnMut(&[u8])) {
141 match &mut self.mode {
142 StuffingMode::Drop => {
143 // Drop null packets; pass everything else through.
144 if !Self::is_null_packet(packet) {
145 out(packet);
146 }
147 }
148 StuffingMode::Pad {
149 nulls_per_real,
150 accumulated,
151 null_cc,
152 ..
153 } => {
154 // Always emit the input packet (even null packets).
155 out(packet);
156
157 // Accumulate the number of null packets to insert.
158 *accumulated += *nulls_per_real;
159
160 // Emit null packets for every 1.0 units of accumulated nulls.
161 while *accumulated >= 1.0 {
162 let null = make_null_packet(*null_cc);
163 out(&null);
164 *accumulated -= 1.0;
165 *null_cc = (*null_cc + 1) & 0x0F;
166 }
167 }
168 }
169 }
170
171 fn flush(&mut self, _model: &mut StreamModel, _out: &mut dyn FnMut(&[u8])) {
172 // Stuffing has no buffered state to flush at end-of-stream.
173 // In pad mode, the accumulated fractional packet is discarded
174 // (typical for bitrate padding — the last partial packet is normal).
175 }
176}