1pub const VERSION: u8 = 2;
21pub const HEADER_LEN: usize = 20;
22pub const CONTROL_CHANNEL: u8 = 0;
23
24pub const DEFAULT_CHUNK_LEN: u16 = 1000;
26pub const MAX_CHUNK_LEN: u16 = 1350;
29
30pub const FLAG2_FRAME_META: u8 = 1 << 0;
31pub const FLAG2_DISCARDABLE: u8 = 1 << 1;
32pub const FLAG2_END_OF_FRAME: u8 = 1 << 2;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(u8)]
36pub enum FecType {
37 None = 0,
38 ReedSolomon = 2,
39}
40
41impl FecType {
42 fn from_bits(b: u8) -> Option<Self> {
43 match b {
44 0 => Some(FecType::None),
45 2 => Some(FecType::ReedSolomon),
46 _ => None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct ChunkHeader {
53 pub keyframe: bool,
54 pub fec_type: FecType,
55 pub channel_id: u8,
56 pub frame_id: u16,
57 pub chunk_idx: u16,
58 pub n: u16,
59 pub k: u8,
60 pub flags2: u8,
61 pub last_len: u16,
62 pub chunk_len: u16,
63 pub send_ts: u32,
64 pub block_idx: u16,
65}
66
67impl ChunkHeader {
68 #[inline]
69 pub fn is_parity(&self) -> bool {
70 self.chunk_idx >= self.n
71 }
72 #[inline]
73 pub fn has_frame_meta(&self) -> bool {
74 self.flags2 & FLAG2_FRAME_META != 0
75 }
76 #[inline]
77 pub fn is_discardable(&self) -> bool {
78 self.flags2 & FLAG2_DISCARDABLE != 0
79 }
80 #[inline]
81 pub fn is_end_of_frame(&self) -> bool {
82 self.flags2 & FLAG2_END_OF_FRAME != 0
83 }
84
85 pub fn encode(&self) -> [u8; HEADER_LEN] {
86 let mut out = [0u8; HEADER_LEN];
87 out[0] = (if self.keyframe { 0x80 } else { 0 }) | ((self.fec_type as u8) << 4) | VERSION;
88 out[1] = self.channel_id;
89 out[2..4].copy_from_slice(&self.frame_id.to_be_bytes());
90 out[4..6].copy_from_slice(&self.chunk_idx.to_be_bytes());
91 out[6..8].copy_from_slice(&self.n.to_be_bytes());
92 out[8] = self.k;
93 out[9] = self.flags2;
94 out[10..12].copy_from_slice(&self.last_len.to_be_bytes());
95 out[12..14].copy_from_slice(&self.chunk_len.to_be_bytes());
96 out[14..18].copy_from_slice(&self.send_ts.to_be_bytes());
97 out[18..20].copy_from_slice(&self.block_idx.to_be_bytes());
98 out
99 }
100
101 pub fn parse(buf: &[u8]) -> Option<(ChunkHeader, &[u8])> {
103 if buf.len() < HEADER_LEN {
104 return None;
105 }
106 let flags = buf[0];
107 if flags & 0x0f != VERSION {
108 return None;
109 }
110 let fec_type = FecType::from_bits((flags >> 4) & 0x07)?;
111 let be16 = |i: usize| u16::from_be_bytes([buf[i], buf[i + 1]]);
112 let hdr = ChunkHeader {
113 keyframe: flags & 0x80 != 0,
114 fec_type,
115 channel_id: buf[1],
116 frame_id: be16(2),
117 chunk_idx: be16(4),
118 n: be16(6),
119 k: buf[8],
120 flags2: buf[9],
121 last_len: be16(10),
122 chunk_len: be16(12),
123 send_ts: u32::from_be_bytes([buf[14], buf[15], buf[16], buf[17]]),
124 block_idx: be16(18),
125 };
126 if hdr.n == 0 || hdr.chunk_idx as usize >= hdr.n as usize + hdr.k as usize {
127 return None;
128 }
129 Some((hdr, &buf[HEADER_LEN..]))
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct FrameMeta {
137 pub capture_ts_us: u64,
139 pub seq_in_gop: u16,
141}
142
143impl FrameMeta {
144 pub const LEN: usize = 10;
145
146 pub fn encode(&self) -> [u8; Self::LEN] {
147 let mut out = [0u8; Self::LEN];
148 out[..8].copy_from_slice(&self.capture_ts_us.to_be_bytes());
149 out[8..].copy_from_slice(&self.seq_in_gop.to_be_bytes());
150 out
151 }
152
153 pub fn parse(buf: &[u8]) -> Option<(FrameMeta, &[u8])> {
154 if buf.len() < Self::LEN {
155 return None;
156 }
157 let mut ts = [0u8; 8];
158 ts.copy_from_slice(&buf[..8]);
159 Some((
160 FrameMeta {
161 capture_ts_us: u64::from_be_bytes(ts),
162 seq_in_gop: u16::from_be_bytes([buf[8], buf[9]]),
163 },
164 &buf[Self::LEN..],
165 ))
166 }
167}
168
169#[inline]
171pub fn ts_delta_us(later: u32, earlier: u32) -> i32 {
172 later.wrapping_sub(earlier) as i32
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn sample() -> ChunkHeader {
180 ChunkHeader {
181 keyframe: true,
182 fec_type: FecType::ReedSolomon,
183 channel_id: 3,
184 frame_id: 0xbeef,
185 chunk_idx: 9,
186 n: 8,
187 k: 4,
188 flags2: FLAG2_FRAME_META | FLAG2_END_OF_FRAME,
189 last_len: 517,
190 chunk_len: 1200,
191 send_ts: 0xdeadbeef,
192 block_idx: 2,
193 }
194 }
195
196 #[test]
197 fn roundtrip() {
198 let h = sample();
199 let mut buf = h.encode().to_vec();
200 buf.extend_from_slice(b"payload");
201 let (parsed, payload) = ChunkHeader::parse(&buf).unwrap();
202 assert_eq!(parsed, h);
203 assert_eq!(payload, b"payload");
204 assert!(parsed.is_parity());
205 assert!(parsed.has_frame_meta());
206 assert!(parsed.is_end_of_frame());
207 assert!(!parsed.is_discardable());
208 }
209
210 #[test]
211 fn layout_is_exact() {
212 let bytes = sample().encode();
213 assert_eq!(bytes[0], 0x80 | (2 << 4) | 2);
214 assert_eq!(bytes[1], 3);
215 assert_eq!(&bytes[2..4], &[0xbe, 0xef]);
216 assert_eq!(&bytes[6..8], &[0, 8]);
217 assert_eq!(bytes[8], 4);
218 assert_eq!(&bytes[14..18], &[0xde, 0xad, 0xbe, 0xef]);
219 assert_eq!(&bytes[18..20], &[0, 2]);
220 }
221
222 #[test]
223 fn rejects_bad_input() {
224 let mut bytes = sample().encode();
225 assert!(ChunkHeader::parse(&bytes[..19]).is_none(), "short");
226 bytes[0] = (bytes[0] & 0xf0) | 1;
227 assert!(
228 ChunkHeader::parse(&bytes).is_none(),
229 "v1 not accepted by v2 parser"
230 );
231 let mut bytes = sample().encode();
232 bytes[0] = (bytes[0] & 0x8f) | (5 << 4);
233 assert!(ChunkHeader::parse(&bytes).is_none(), "unknown fec type");
234 let mut bytes = sample().encode();
235 bytes[4..6].copy_from_slice(&12u16.to_be_bytes());
236 assert!(ChunkHeader::parse(&bytes).is_none(), "chunk_idx >= n+k");
237 }
238
239 #[test]
240 fn frame_meta_roundtrip() {
241 let m = FrameMeta {
242 capture_ts_us: 1_700_000_000_123_456,
243 seq_in_gop: 7,
244 };
245 let mut buf = m.encode().to_vec();
246 buf.push(0x42);
247 let (p, rest) = FrameMeta::parse(&buf).unwrap();
248 assert_eq!(p, m);
249 assert_eq!(rest, &[0x42]);
250 }
251
252 #[test]
253 fn ts_delta_wraps() {
254 assert_eq!(ts_delta_us(10, u32::MAX - 5), 16);
255 assert_eq!(ts_delta_us(5, 10), -5);
256 }
257}