Skip to main content

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}