Skip to main content

openipc_core/
mock.rs

1use crate::rtp::{RtpError, RtpHeader};
2
3const MOCK_RTP_PAYLOAD_TYPE: u8 = 120;
4const MOCK_RTP_SSRC: u32 = 0x4f49_5043;
5const MOCK_PAYLOAD_MAGIC: &[u8; 4] = b"ORMF";
6const MOCK_PAYLOAD_HEADER_LEN: usize = 24;
7const MOCK_RTP_PAYLOAD_BYTES: usize = 1_100;
8
9/// One RGBA frame recovered from the Rust mock RTP pipeline.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MockRtpFrame {
12    /// Frame width in pixels.
13    pub width: u16,
14    /// Frame height in pixels.
15    pub height: u16,
16    /// Monotonic mock frame index.
17    pub frame_index: u64,
18    /// RTP timestamp used for this frame.
19    pub timestamp: u32,
20    /// RGBA pixels in row-major order.
21    pub rgba: Vec<u8>,
22    /// Number of RTP packets generated and consumed for this frame.
23    pub rtp_packets: usize,
24    /// Total RTP bytes generated for this frame.
25    pub rtp_bytes: usize,
26}
27
28/// Synthetic RTP source for no-hardware development.
29///
30/// This is intentionally not an H.264 encoder. It is a deterministic RTP
31/// packet generator plus RTP reassembler that produces RGBA test frames for
32/// frontend development, layout work, and metrics plumbing without a USB radio.
33#[derive(Debug, Clone)]
34pub struct MockRtpPipeline {
35    width: u16,
36    height: u16,
37    fps: u16,
38    frame_index: u64,
39    sequence: u16,
40    timestamp: u32,
41}
42
43impl Default for MockRtpPipeline {
44    fn default() -> Self {
45        Self::new(320, 180, 30)
46    }
47}
48
49impl MockRtpPipeline {
50    /// Create a mock RTP pipeline.
51    pub fn new(width: u16, height: u16, fps: u16) -> Self {
52        Self {
53            width: width.clamp(16, 1_920),
54            height: height.clamp(16, 1_080),
55            fps: fps.clamp(1, 120),
56            frame_index: 0,
57            sequence: 1,
58            timestamp: 0,
59        }
60    }
61
62    /// Generate RTP packets, consume them with the mock RTP assembler, and
63    /// return the recovered RGBA frame.
64    pub fn next_frame(&mut self) -> Result<MockRtpFrame, RtpError> {
65        let rgba = render_mock_rgba(self.width, self.height, self.frame_index);
66        let packets = self.packetize(&rgba);
67        let rtp_packets = packets.len();
68        let rtp_bytes = packets.iter().map(Vec::len).sum();
69        let mut assembler = MockRtpAssembler::default();
70        let mut frame = None;
71        for packet in packets {
72            if let Some(recovered) = assembler.push(&packet)? {
73                frame = Some(recovered);
74            }
75        }
76
77        self.frame_index = self.frame_index.wrapping_add(1);
78        self.timestamp = self.timestamp.wrapping_add(90_000u32 / u32::from(self.fps));
79
80        let mut frame = frame.ok_or(RtpError::EmptyPayload)?;
81        frame.rtp_packets = rtp_packets;
82        frame.rtp_bytes = rtp_bytes;
83        Ok(frame)
84    }
85
86    fn packetize(&mut self, rgba: &[u8]) -> Vec<Vec<u8>> {
87        let mut packets = Vec::new();
88        let total_len = rgba.len() as u32;
89        let mut offset = 0usize;
90        while offset < rgba.len() {
91            let remaining = rgba.len() - offset;
92            let chunk_len = remaining.min(MOCK_RTP_PAYLOAD_BYTES);
93            let marker = offset + chunk_len == rgba.len();
94            let mut packet = Vec::with_capacity(12 + MOCK_PAYLOAD_HEADER_LEN + chunk_len);
95            packet.push(0x80);
96            packet.push((if marker { 0x80 } else { 0x00 }) | MOCK_RTP_PAYLOAD_TYPE);
97            packet.extend_from_slice(&self.sequence.to_be_bytes());
98            packet.extend_from_slice(&self.timestamp.to_be_bytes());
99            packet.extend_from_slice(&MOCK_RTP_SSRC.to_be_bytes());
100            packet.extend_from_slice(MOCK_PAYLOAD_MAGIC);
101            packet.extend_from_slice(&self.frame_index.to_be_bytes());
102            packet.extend_from_slice(&self.width.to_be_bytes());
103            packet.extend_from_slice(&self.height.to_be_bytes());
104            packet.extend_from_slice(&total_len.to_be_bytes());
105            packet.extend_from_slice(&(offset as u32).to_be_bytes());
106            packet.extend_from_slice(&rgba[offset..offset + chunk_len]);
107            packets.push(packet);
108            self.sequence = self.sequence.wrapping_add(1);
109            offset += chunk_len;
110        }
111        packets
112    }
113}
114
115#[derive(Debug, Default)]
116struct MockRtpAssembler {
117    frame_index: Option<u64>,
118    timestamp: u32,
119    width: u16,
120    height: u16,
121    total_len: usize,
122    rgba: Vec<u8>,
123    received: Vec<bool>,
124}
125
126impl MockRtpAssembler {
127    fn push(&mut self, packet: &[u8]) -> Result<Option<MockRtpFrame>, RtpError> {
128        let header = RtpHeader::parse(packet)?;
129        if header.payload_type != MOCK_RTP_PAYLOAD_TYPE {
130            return Err(RtpError::UnsupportedPayload);
131        }
132        let payload = header.payload(packet);
133        if payload.len() < MOCK_PAYLOAD_HEADER_LEN || &payload[..4] != MOCK_PAYLOAD_MAGIC {
134            return Err(RtpError::UnsupportedPayload);
135        }
136        let frame_index = u64::from_be_bytes(payload[4..12].try_into().unwrap());
137        let width = u16::from_be_bytes(payload[12..14].try_into().unwrap());
138        let height = u16::from_be_bytes(payload[14..16].try_into().unwrap());
139        let total_len = u32::from_be_bytes(payload[16..20].try_into().unwrap()) as usize;
140        let offset = u32::from_be_bytes(payload[20..24].try_into().unwrap()) as usize;
141        let bytes = &payload[24..];
142        if total_len == 0 || offset + bytes.len() > total_len {
143            return Err(RtpError::InvalidPadding);
144        }
145        if self.frame_index != Some(frame_index) {
146            self.frame_index = Some(frame_index);
147            self.timestamp = header.timestamp;
148            self.width = width;
149            self.height = height;
150            self.total_len = total_len;
151            self.rgba = vec![0; total_len];
152            self.received = vec![false; total_len];
153        }
154        self.rgba[offset..offset + bytes.len()].copy_from_slice(bytes);
155        self.received[offset..offset + bytes.len()].fill(true);
156
157        if header.marker && self.received.iter().all(|received| *received) {
158            Ok(Some(MockRtpFrame {
159                width: self.width,
160                height: self.height,
161                frame_index,
162                timestamp: self.timestamp,
163                rgba: self.rgba.clone(),
164                rtp_packets: 0,
165                rtp_bytes: 0,
166            }))
167        } else {
168            Ok(None)
169        }
170    }
171}
172
173fn render_mock_rgba(width: u16, height: u16, frame_index: u64) -> Vec<u8> {
174    let width = usize::from(width);
175    let height = usize::from(height);
176    let mut rgba = vec![0; width * height * 4];
177    let phase = (frame_index as usize * 3) % width.max(1);
178    for y in 0..height {
179        for x in 0..width {
180            let i = (y * width + x) * 4;
181            let bar = ((x + phase) * 6 / width.max(1)) as u8;
182            let grid = x % 32 == 0 || y % 32 == 0;
183            let pulse = ((frame_index * 5 + y as u64) & 0xff) as u8;
184            let (r, g, b): (u8, u8, u8) = match bar {
185                0 => (236, 72, 85),
186                1 => (245, 158, 11),
187                2 => (34, 197, 94),
188                3 => (20, 184, 166),
189                4 => (59, 130, 246),
190                _ => (168, 85, 247),
191            };
192            rgba[i] = if grid { 245 } else { r };
193            rgba[i + 1] = if grid {
194                245
195            } else {
196                g.saturating_add(pulse / 12)
197            };
198            rgba[i + 2] = if grid { 245 } else { b };
199            rgba[i + 3] = 255;
200        }
201    }
202    rgba
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn mock_pipeline_roundtrips_rgba_through_rtp_packets() {
211        let mut mock = MockRtpPipeline::new(64, 36, 30);
212        let frame = mock.next_frame().unwrap();
213        assert_eq!(frame.width, 64);
214        assert_eq!(frame.height, 36);
215        assert_eq!(frame.frame_index, 0);
216        assert_eq!(frame.rgba.len(), 64 * 36 * 4);
217        assert!(frame.rtp_packets > 1);
218        assert!(frame.rtp_bytes > frame.rgba.len());
219    }
220}