1use bytes::{BufMut, Bytes, BytesMut};
11use seyd_wire::v2::{self, ChunkHeader, FecType, FrameMeta, HEADER_LEN};
12
13pub const BLOCK_DATA_CHUNKS: usize = 8;
14pub const MAX_PARITY_PER_BLOCK: usize = 16;
15
16pub struct VideoPack {
17 pub chunks: Vec<Bytes>,
18 pub blocks: usize,
19 pub data_chunks: usize,
20 pub parity_chunks: usize,
21}
22
23#[derive(Debug, Clone, Copy)]
24pub struct FrameParams {
25 pub channel_id: u8,
26 pub frame_id: u16,
27 pub keyframe: bool,
28 pub fec_pct: u32,
30 pub chunk_len: u16,
31 pub send_ts: u32,
32 pub meta: Option<FrameMeta>,
33}
34
35pub fn pack_video(p: FrameParams, payload: &[u8]) -> VideoPack {
37 let FrameParams {
38 channel_id,
39 frame_id,
40 keyframe,
41 fec_pct,
42 chunk_len,
43 send_ts,
44 meta,
45 } = p;
46 let chunk_len_us = chunk_len as usize;
47 let mut body: Vec<u8>;
50 let bytes: &[u8] = if let Some(m) = meta {
51 body = Vec::with_capacity(payload.len() + FrameMeta::LEN);
52 body.extend_from_slice(&m.encode());
53 body.extend_from_slice(payload);
54 &body
55 } else {
56 payload
57 };
58
59 let total_data = bytes.len().div_ceil(chunk_len_us).max(1);
60 let blocks = total_data.div_ceil(BLOCK_DATA_CHUNKS);
61 let mut out = Vec::with_capacity(total_data + blocks * 4);
62 let (mut data_count, mut parity_count_total) = (0, 0);
63
64 for block_idx in 0..blocks {
65 let first = block_idx * BLOCK_DATA_CHUNKS;
66 let n = (total_data - first).min(BLOCK_DATA_CHUNKS);
67 let last = block_idx + 1 == blocks;
68 let k = seyd_fec::parity_count(n, fec_pct, MAX_PARITY_PER_BLOCK);
69
70 let slices: Vec<&[u8]> = (0..n)
71 .map(|i| {
72 let start = (first + i) * chunk_len_us;
73 let end = (start + chunk_len_us).min(bytes.len());
74 if start >= bytes.len() {
75 &[][..]
76 } else {
77 &bytes[start..end]
78 }
79 })
80 .collect();
81 let last_len = slices[n - 1].len() as u16;
82
83 let mut flags2 = 0u8;
84 if block_idx == 0 && meta.is_some() {
85 flags2 |= v2::FLAG2_FRAME_META;
86 }
87 if last {
88 flags2 |= v2::FLAG2_END_OF_FRAME;
89 }
90 let hdr = ChunkHeader {
91 keyframe,
92 fec_type: if k > 0 {
93 FecType::ReedSolomon
94 } else {
95 FecType::None
96 },
97 channel_id,
98 frame_id,
99 chunk_idx: 0,
100 n: n as u16,
101 k: k as u8,
102 flags2,
103 last_len,
104 chunk_len,
105 send_ts,
106 block_idx: block_idx as u16,
107 };
108
109 for (i, s) in slices.iter().enumerate() {
110 let mut b = BytesMut::with_capacity(HEADER_LEN + s.len());
111 b.put_slice(
112 &ChunkHeader {
113 chunk_idx: i as u16,
114 ..hdr
115 }
116 .encode(),
117 );
118 b.put_slice(s);
119 out.push(b.freeze());
120 }
121 data_count += n;
122
123 if k > 0 {
124 let padded: Vec<Vec<u8>> = slices
126 .iter()
127 .map(|s| {
128 let mut v = s.to_vec();
129 v.resize(chunk_len_us, 0);
130 v
131 })
132 .collect();
133 let refs: Vec<&[u8]> = padded.iter().map(|v| v.as_slice()).collect();
134 for (j, p) in seyd_fec::encode_parity(&refs, k).into_iter().enumerate() {
135 let mut b = BytesMut::with_capacity(HEADER_LEN + p.len());
136 b.put_slice(
137 &ChunkHeader {
138 chunk_idx: (n + j) as u16,
139 ..hdr
140 }
141 .encode(),
142 );
143 b.put_slice(&p);
144 out.push(b.freeze());
145 }
146 parity_count_total += k;
147 }
148 }
149 VideoPack {
150 chunks: out,
151 blocks,
152 data_chunks: data_count,
153 parity_chunks: parity_count_total,
154 }
155}
156
157pub fn pack_message(channel_id: u8, seq: u16, send_ts: u32, payload: &[u8]) -> Bytes {
159 let len = payload.len().min(u16::MAX as usize) as u16;
160 let hdr = ChunkHeader {
161 keyframe: false,
162 fec_type: FecType::None,
163 channel_id,
164 frame_id: seq,
165 chunk_idx: 0,
166 n: 1,
167 k: 0,
168 flags2: v2::FLAG2_END_OF_FRAME,
169 last_len: len,
170 chunk_len: len,
171 send_ts,
172 block_idx: 0,
173 };
174 let mut b = BytesMut::with_capacity(HEADER_LEN + payload.len());
175 b.put_slice(&hdr.encode());
176 b.put_slice(&payload[..len as usize]);
177 b.freeze()
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn reassemble(chunks: &[Bytes], erase: &[usize]) -> Option<(Vec<u8>, bool)> {
187 use std::collections::BTreeMap;
188 type Block = (ChunkHeader, Vec<Option<Vec<u8>>>, Vec<Option<Vec<u8>>>);
189 let mut blocks: BTreeMap<u16, Block> = BTreeMap::new();
190 let mut end_seen = None;
191 for (i, c) in chunks.iter().enumerate() {
192 if erase.contains(&i) {
193 continue;
194 }
195 let (h, p) = ChunkHeader::parse(c).unwrap();
196 let e = blocks
197 .entry(h.block_idx)
198 .or_insert_with(|| (h, vec![None; h.n as usize], vec![None; h.k as usize]));
199 if h.is_end_of_frame() {
200 end_seen = Some(h.block_idx);
201 }
202 if h.is_parity() {
203 e.2[(h.chunk_idx - h.n) as usize] = Some(p.to_vec());
204 } else {
205 let mut v = p.to_vec();
206 v.resize(h.chunk_len as usize, 0);
207 e.1[h.chunk_idx as usize] = Some(v);
208 }
209 }
210 let last = end_seen?;
211 let mut out = Vec::new();
212 let mut key = false;
213 for b in 0..=last {
214 let (h, data, parity) = blocks.get_mut(&b)?;
215 key = h.keyframe;
216 if !seyd_fec::decode(data, parity) {
217 return None;
218 }
219 for (i, d) in data.iter().enumerate() {
220 let take = if i + 1 == h.n as usize {
221 h.last_len as usize
222 } else {
223 h.chunk_len as usize
224 };
225 out.extend_from_slice(&d.as_ref().unwrap()[..take]);
226 }
227 }
228 Some((out, key))
229 }
230
231 #[test]
232 fn multi_block_keyframe_survives_k_losses_per_block() {
233 let payload: Vec<u8> = (0..23_456u32).map(|i| (i * 7 % 251) as u8).collect();
234 let pack = pack_video(
235 FrameParams {
236 channel_id: 1,
237 frame_id: 42,
238 keyframe: true,
239 fec_pct: 50,
240 chunk_len: 1000,
241 send_ts: 123,
242 meta: None,
243 },
244 &payload,
245 );
246 assert_eq!(pack.blocks, 3);
248 assert_eq!(pack.data_chunks, 24);
249 assert_eq!(pack.parity_chunks, 12);
250 assert_eq!(pack.chunks.len(), 36);
251 let erase: Vec<usize> = [0usize, 5, 8, 11, 12, 13, 22, 23, 24, 30, 33, 35].to_vec();
253 let (out, key) = reassemble(&pack.chunks, &erase).unwrap();
254 assert!(key);
255 assert_eq!(out, payload);
256 let mut erase5 = erase.clone();
258 erase5.push(1);
259 assert!(reassemble(&pack.chunks, &erase5).is_none());
260 }
261
262 #[test]
263 fn tiny_frame_is_one_block_with_meta() {
264 let meta = FrameMeta {
265 capture_ts_us: 99,
266 seq_in_gop: 3,
267 };
268 let pack = pack_video(
269 FrameParams {
270 channel_id: 2,
271 frame_id: 7,
272 keyframe: false,
273 fec_pct: 15,
274 chunk_len: 1000,
275 send_ts: 5,
276 meta: Some(meta),
277 },
278 b"hello",
279 );
280 assert_eq!(pack.chunks.len(), 2); let (h, p) = ChunkHeader::parse(&pack.chunks[0]).unwrap();
282 assert!(h.has_frame_meta() && h.is_end_of_frame() && !h.keyframe);
283 assert_eq!(h.last_len as usize, FrameMeta::LEN + 5);
284 let (m, rest) = FrameMeta::parse(p).unwrap();
285 assert_eq!(m, meta);
286 assert_eq!(rest, b"hello");
287 let (out, _) = reassemble(&pack.chunks, &[0]).unwrap();
288 assert_eq!(&out[FrameMeta::LEN..], b"hello");
289 }
290
291 #[test]
292 fn exact_multiple_has_full_last_len() {
293 let payload = vec![1u8; 8000];
294 let pack = pack_video(
295 FrameParams {
296 channel_id: 1,
297 frame_id: 1,
298 keyframe: false,
299 fec_pct: 0,
300 chunk_len: 1000,
301 send_ts: 0,
302 meta: None,
303 },
304 &payload,
305 );
306 assert_eq!(pack.chunks.len(), 8);
307 let (h, _) = ChunkHeader::parse(&pack.chunks[7]).unwrap();
308 assert_eq!(h.last_len, 1000);
309 assert_eq!(h.fec_type, FecType::None);
310 }
311
312 #[test]
313 fn message_pack() {
314 let m = pack_message(3, 65535, 1, b"{\"pan\":1}");
315 let (h, p) = ChunkHeader::parse(&m).unwrap();
316 assert_eq!((h.n, h.k, h.frame_id, h.channel_id), (1, 0, 65535, 3));
317 assert!(h.is_end_of_frame());
318 assert_eq!(p, b"{\"pan\":1}");
319 }
320}