use crate::rtp::{RtpError, RtpHeader};
const MOCK_RTP_PAYLOAD_TYPE: u8 = 120;
const MOCK_RTP_SSRC: u32 = 0x4f49_5043;
const MOCK_PAYLOAD_MAGIC: &[u8; 4] = b"ORMF";
const MOCK_PAYLOAD_HEADER_LEN: usize = 24;
const MOCK_RTP_PAYLOAD_BYTES: usize = 1_100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MockRtpFrame {
pub width: u16,
pub height: u16,
pub frame_index: u64,
pub timestamp: u32,
pub rgba: Vec<u8>,
pub rtp_packets: usize,
pub rtp_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct MockRtpPipeline {
width: u16,
height: u16,
fps: u16,
frame_index: u64,
sequence: u16,
timestamp: u32,
}
impl Default for MockRtpPipeline {
fn default() -> Self {
Self::new(320, 180, 30)
}
}
impl MockRtpPipeline {
pub fn new(width: u16, height: u16, fps: u16) -> Self {
Self {
width: width.clamp(16, 1_920),
height: height.clamp(16, 1_080),
fps: fps.clamp(1, 120),
frame_index: 0,
sequence: 1,
timestamp: 0,
}
}
pub fn next_frame(&mut self) -> Result<MockRtpFrame, RtpError> {
let rgba = render_mock_rgba(self.width, self.height, self.frame_index);
let packets = self.packetize(&rgba);
let rtp_packets = packets.len();
let rtp_bytes = packets.iter().map(Vec::len).sum();
let mut assembler = MockRtpAssembler::default();
let mut frame = None;
for packet in packets {
if let Some(recovered) = assembler.push(&packet)? {
frame = Some(recovered);
}
}
self.frame_index = self.frame_index.wrapping_add(1);
self.timestamp = self.timestamp.wrapping_add(90_000u32 / u32::from(self.fps));
let mut frame = frame.ok_or(RtpError::EmptyPayload)?;
frame.rtp_packets = rtp_packets;
frame.rtp_bytes = rtp_bytes;
Ok(frame)
}
fn packetize(&mut self, rgba: &[u8]) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
let total_len = rgba.len() as u32;
let mut offset = 0usize;
while offset < rgba.len() {
let remaining = rgba.len() - offset;
let chunk_len = remaining.min(MOCK_RTP_PAYLOAD_BYTES);
let marker = offset + chunk_len == rgba.len();
let mut packet = Vec::with_capacity(12 + MOCK_PAYLOAD_HEADER_LEN + chunk_len);
packet.push(0x80);
packet.push((if marker { 0x80 } else { 0x00 }) | MOCK_RTP_PAYLOAD_TYPE);
packet.extend_from_slice(&self.sequence.to_be_bytes());
packet.extend_from_slice(&self.timestamp.to_be_bytes());
packet.extend_from_slice(&MOCK_RTP_SSRC.to_be_bytes());
packet.extend_from_slice(MOCK_PAYLOAD_MAGIC);
packet.extend_from_slice(&self.frame_index.to_be_bytes());
packet.extend_from_slice(&self.width.to_be_bytes());
packet.extend_from_slice(&self.height.to_be_bytes());
packet.extend_from_slice(&total_len.to_be_bytes());
packet.extend_from_slice(&(offset as u32).to_be_bytes());
packet.extend_from_slice(&rgba[offset..offset + chunk_len]);
packets.push(packet);
self.sequence = self.sequence.wrapping_add(1);
offset += chunk_len;
}
packets
}
}
#[derive(Debug, Default)]
struct MockRtpAssembler {
frame_index: Option<u64>,
timestamp: u32,
width: u16,
height: u16,
total_len: usize,
rgba: Vec<u8>,
received: Vec<bool>,
}
impl MockRtpAssembler {
fn push(&mut self, packet: &[u8]) -> Result<Option<MockRtpFrame>, RtpError> {
let header = RtpHeader::parse(packet)?;
if header.payload_type != MOCK_RTP_PAYLOAD_TYPE {
return Err(RtpError::UnsupportedPayload);
}
let payload = header.payload(packet);
if payload.len() < MOCK_PAYLOAD_HEADER_LEN || &payload[..4] != MOCK_PAYLOAD_MAGIC {
return Err(RtpError::UnsupportedPayload);
}
let frame_index = u64::from_be_bytes(payload[4..12].try_into().unwrap());
let width = u16::from_be_bytes(payload[12..14].try_into().unwrap());
let height = u16::from_be_bytes(payload[14..16].try_into().unwrap());
let total_len = u32::from_be_bytes(payload[16..20].try_into().unwrap()) as usize;
let offset = u32::from_be_bytes(payload[20..24].try_into().unwrap()) as usize;
let bytes = &payload[24..];
if total_len == 0 || offset + bytes.len() > total_len {
return Err(RtpError::InvalidPadding);
}
if self.frame_index != Some(frame_index) {
self.frame_index = Some(frame_index);
self.timestamp = header.timestamp;
self.width = width;
self.height = height;
self.total_len = total_len;
self.rgba = vec![0; total_len];
self.received = vec![false; total_len];
}
self.rgba[offset..offset + bytes.len()].copy_from_slice(bytes);
self.received[offset..offset + bytes.len()].fill(true);
if header.marker && self.received.iter().all(|received| *received) {
Ok(Some(MockRtpFrame {
width: self.width,
height: self.height,
frame_index,
timestamp: self.timestamp,
rgba: self.rgba.clone(),
rtp_packets: 0,
rtp_bytes: 0,
}))
} else {
Ok(None)
}
}
}
fn render_mock_rgba(width: u16, height: u16, frame_index: u64) -> Vec<u8> {
let width = usize::from(width);
let height = usize::from(height);
let mut rgba = vec![0; width * height * 4];
let phase = (frame_index as usize * 3) % width.max(1);
for y in 0..height {
for x in 0..width {
let i = (y * width + x) * 4;
let bar = ((x + phase) * 6 / width.max(1)) as u8;
let grid = x % 32 == 0 || y % 32 == 0;
let pulse = ((frame_index * 5 + y as u64) & 0xff) as u8;
let (r, g, b): (u8, u8, u8) = match bar {
0 => (236, 72, 85),
1 => (245, 158, 11),
2 => (34, 197, 94),
3 => (20, 184, 166),
4 => (59, 130, 246),
_ => (168, 85, 247),
};
rgba[i] = if grid { 245 } else { r };
rgba[i + 1] = if grid {
245
} else {
g.saturating_add(pulse / 12)
};
rgba[i + 2] = if grid { 245 } else { b };
rgba[i + 3] = 255;
}
}
rgba
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_pipeline_roundtrips_rgba_through_rtp_packets() {
let mut mock = MockRtpPipeline::new(64, 36, 30);
let frame = mock.next_frame().unwrap();
assert_eq!(frame.width, 64);
assert_eq!(frame.height, 36);
assert_eq!(frame.frame_index, 0);
assert_eq!(frame.rgba.len(), 64 * 36 * 4);
assert!(frame.rtp_packets > 1);
assert!(frame.rtp_bytes > frame.rgba.len());
}
}