1use std::collections::VecDeque;
10use std::net::Ipv4Addr;
11use std::ops::RangeInclusive;
12use std::time::{Duration, Instant};
13
14use crate::nic::Iface;
15use crate::stats::StreamStatsAccumulator;
16use bytes::{Buf, Bytes, BytesMut};
17use thiserror::Error;
18use tracing::{debug, warn};
19
20const PAYLOAD_TYPE_IMAGE: u8 = 0x01;
24
25const GVSP_HEADER_SIZE: usize = 8;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum StreamDest {
33 Unicast {
35 dst_ip: Ipv4Addr,
37 dst_port: u16,
39 },
40 Multicast {
42 group: Ipv4Addr,
44 port: u16,
46 loopback: bool,
48 ttl: u32,
50 },
51}
52
53impl StreamDest {
54 pub fn port(&self) -> u16 {
56 match self {
57 StreamDest::Unicast { dst_port, .. } => *dst_port,
58 StreamDest::Multicast { port, .. } => *port,
59 }
60 }
61
62 pub fn addr(&self) -> Ipv4Addr {
64 match self {
65 StreamDest::Unicast { dst_ip, .. } => *dst_ip,
66 StreamDest::Multicast { group, .. } => *group,
67 }
68 }
69
70 pub fn is_multicast(&self) -> bool {
72 matches!(self, StreamDest::Multicast { .. })
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct StreamConfig {
79 pub dest: StreamDest,
81 pub iface: Iface,
83 pub packet_size: Option<u32>,
85 pub packet_delay: Option<u32>,
87 pub source_filter: Option<Ipv4Addr>,
89 pub resend_enabled: bool,
91}
92
93#[derive(Debug, Error)]
95#[non_exhaustive]
96pub enum GvspError {
97 #[error("unsupported packet type: {0}")]
98 Unsupported(&'static str),
99 #[error("invalid packet: {0}")]
100 Invalid(&'static str),
101 #[error("resend timeout")]
102 ResendTimeout,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ChunkRaw {
108 pub id: u16,
109 pub data: Bytes,
110}
111
112pub fn parse_chunks(mut payload: &[u8]) -> Vec<ChunkRaw> {
114 let mut chunks = Vec::new();
115 while !payload.is_empty() {
116 if payload.len() < 8 {
117 warn!(remaining = payload.len(), "chunk header truncated");
118 break;
119 }
120 let mut cursor = payload;
121 let id = cursor.get_u16();
122 let _reserved = cursor.get_u16();
123 let length = cursor.get_u32() as usize;
124 let total = 8 + length;
125 if payload.len() < total {
126 warn!(
127 chunk_id = format_args!("{:#06x}", id),
128 len = payload.len(),
129 expected = total,
130 "chunk data truncated"
131 );
132 break;
133 }
134 let data = Bytes::copy_from_slice(&payload[8..total]);
135 debug!(
136 chunk_id = format_args!("{:#06x}", id),
137 len = length,
138 "parsed chunk"
139 );
140 chunks.push(ChunkRaw { id, data });
141 payload = &payload[total..];
142 }
143 chunks
144}
145
146#[derive(Debug, Clone)]
152pub enum GvspPacket {
153 Leader {
155 block_id: u64,
156 packet_id: u32,
157 payload_type: u8,
158 timestamp: u64,
159 width: u32,
160 height: u32,
161 pixel_format: u32,
162 },
163 Payload {
165 block_id: u64,
166 packet_id: u32,
167 data: Bytes,
168 },
169 Trailer {
171 block_id: u64,
172 packet_id: u32,
173 status: u16,
176 payload_type: u16,
179 size_y: u32,
181 chunk_data: Bytes,
182 },
183}
184
185const GVSP_EXTENDED_HEADER_SIZE: usize = 20;
198
199const EXTENDED_ID_FLAG: u8 = 0x80;
201
202pub fn parse_packet(payload: &[u8]) -> Result<GvspPacket, GvspError> {
203 if payload.len() < GVSP_HEADER_SIZE {
204 return Err(GvspError::Invalid("GVSP header truncated"));
205 }
206
207 let packet_format_byte = payload[4];
208 let extended = (packet_format_byte & EXTENDED_ID_FLAG) != 0;
209 let packet_format = packet_format_byte & 0x0F;
210
211 let (block_id, packet_id, data_offset) = if extended {
212 if payload.len() < GVSP_EXTENDED_HEADER_SIZE {
220 return Err(GvspError::Invalid("extended GVSP header truncated"));
221 }
222 let block_id = u64::from_be_bytes([
223 payload[8],
224 payload[9],
225 payload[10],
226 payload[11],
227 payload[12],
228 payload[13],
229 payload[14],
230 payload[15],
231 ]);
232 let packet_id = u32::from_be_bytes([payload[16], payload[17], payload[18], payload[19]]);
233 (block_id, packet_id, GVSP_EXTENDED_HEADER_SIZE)
234 } else {
235 let block_id = u16::from_be_bytes([payload[2], payload[3]]) as u64;
237 let packet_id = u32::from_be_bytes([0, payload[5], payload[6], payload[7]]);
238 (block_id, packet_id, GVSP_HEADER_SIZE)
239 };
240
241 let status = u16::from_be_bytes([payload[0], payload[1]]);
246
247 match packet_format {
248 0x01 => parse_leader(packet_id, block_id, &payload[data_offset..]),
249 0x03 => parse_payload(packet_id, block_id, &payload[data_offset..]),
250 0x02 => parse_trailer(packet_id, block_id, status, &payload[data_offset..]),
251 _ => Err(GvspError::Unsupported("packet format")),
252 }
253}
254
255fn parse_leader(packet_id: u32, block_id: u64, payload: &[u8]) -> Result<GvspPacket, GvspError> {
268 if payload.len() < 24 {
269 return Err(GvspError::Invalid("leader payload truncated"));
270 }
271 let mut cursor = payload;
272 let _reserved = cursor.get_u16();
273 let payload_type = cursor.get_u16() as u8;
274 if payload_type != PAYLOAD_TYPE_IMAGE {
275 return Err(GvspError::Unsupported("payload type"));
276 }
277 let timestamp = cursor.get_u64();
278 let pixel_format = cursor.get_u32();
279 let width = cursor.get_u32();
280 let height = cursor.get_u32();
281 Ok(GvspPacket::Leader {
282 block_id,
283 packet_id,
284 payload_type,
285 timestamp,
286 width,
287 height,
288 pixel_format,
289 })
290}
291
292fn parse_payload(packet_id: u32, block_id: u64, payload: &[u8]) -> Result<GvspPacket, GvspError> {
293 Ok(GvspPacket::Payload {
294 block_id,
295 packet_id,
296 data: Bytes::copy_from_slice(payload),
297 })
298}
299
300fn parse_trailer(
321 packet_id: u32,
322 block_id: u64,
323 status: u16,
324 payload: &[u8],
325) -> Result<GvspPacket, GvspError> {
326 if payload.len() < 2 {
327 return Err(GvspError::Invalid("trailer truncated"));
328 }
329 let mut cursor = payload;
330 let _reserved = cursor.get_u16();
331
332 let (payload_type, size_y, chunk_data) = if payload.len() >= 8 {
336 let payload_type = cursor.get_u16();
337 let size_y = cursor.get_u32();
338 (payload_type, size_y, Bytes::copy_from_slice(&payload[8..]))
339 } else {
340 debug!(
341 block_id,
342 len = payload.len(),
343 "trailer payload shorter than the specified 8 bytes"
344 );
345 (0, 0, Bytes::new())
346 };
347
348 Ok(GvspPacket::Trailer {
349 block_id,
350 packet_id,
351 status,
352 payload_type,
353 size_y,
354 chunk_data,
355 })
356}
357
358#[derive(Debug, Clone)]
360pub struct PacketBitmap {
361 words: Vec<u64>,
362 received: usize,
363 total: usize,
364}
365
366impl PacketBitmap {
367 pub fn new(total: usize) -> Self {
369 let words = total.div_ceil(64);
370 Self {
371 words: vec![0; words],
372 received: 0,
373 total,
374 }
375 }
376
377 fn mask_for(&self, packet_id: usize) -> (usize, u64) {
378 let word = packet_id / 64;
379 let bit = packet_id % 64;
380 (word, 1u64 << bit)
381 }
382
383 pub fn set(&mut self, packet_id: usize) -> bool {
385 if packet_id >= self.total {
386 return false;
387 }
388 let (word, mask) = self.mask_for(packet_id);
389 let entry = &mut self.words[word];
390 if *entry & mask == 0 {
391 *entry |= mask;
392 self.received += 1;
393 true
394 } else {
395 false
396 }
397 }
398
399 pub fn is_complete(&self) -> bool {
401 self.received == self.total
402 }
403
404 pub fn missing_ranges(&self) -> Vec<RangeInclusive<u32>> {
406 let mut ranges = Vec::new();
407 let mut current: Option<(u32, u32)> = None;
408 for idx in 0..self.total {
409 let (word, mask) = self.mask_for(idx);
410 let present = (self.words[word] & mask) != 0;
411 match (present, current) {
412 (false, None) => current = Some((idx as u32, idx as u32)),
413 (false, Some((start, _))) => current = Some((start, idx as u32)),
414 (true, Some((start, end))) => {
415 ranges.push(start..=end);
416 current = None;
417 }
418 _ => {}
419 }
420 }
421 if let Some((start, end)) = current {
422 ranges.push(start..=end);
423 }
424 ranges
425 }
426}
427
428#[derive(Debug)]
430pub struct FrameAssembly {
431 block_id: u64,
432 expected_packets: usize,
433 packet_payload: usize,
434 bitmap: PacketBitmap,
435 buffer: BytesMut,
436 lengths: Vec<usize>,
437 deadline: Instant,
438}
439
440impl FrameAssembly {
441 pub fn new(
443 block_id: u64,
444 expected_packets: usize,
445 packet_payload: usize,
446 buffer: BytesMut,
447 deadline: Instant,
448 ) -> Self {
449 Self {
450 block_id,
451 expected_packets,
452 packet_payload,
453 bitmap: PacketBitmap::new(expected_packets),
454 buffer,
455 lengths: vec![0; expected_packets],
456 deadline,
457 }
458 }
459
460 pub fn block_id(&self) -> u64 {
462 self.block_id
463 }
464
465 pub fn is_expired(&self, now: Instant) -> bool {
467 now >= self.deadline
468 }
469
470 pub fn ingest(&mut self, packet_id: usize, payload: &[u8]) -> bool {
472 if packet_id >= self.expected_packets || payload.len() > self.packet_payload {
473 return false;
474 }
475 if !self.bitmap.set(packet_id) {
476 return true;
477 }
478 self.lengths[packet_id] = payload.len();
480 let offset = packet_id * self.packet_payload;
481 if self.buffer.len() < offset + payload.len() {
482 self.buffer.resize(offset + payload.len(), 0);
483 }
484 self.buffer[offset..offset + payload.len()].copy_from_slice(payload);
485 true
486 }
487
488 pub fn finish(self) -> Option<Bytes> {
490 if !self.bitmap.is_complete() {
491 return None;
492 }
493
494 let full_sized_prefix = if self.expected_packets > 0 {
497 self.lengths
498 .iter()
499 .take(self.expected_packets.saturating_sub(1))
500 .all(|&len| len == self.packet_payload)
501 } else {
502 true
503 };
504
505 if full_sized_prefix {
506 let last_len = *self.lengths.last().unwrap_or(&0);
507 let used = self
508 .packet_payload
509 .saturating_mul(self.expected_packets.saturating_sub(1))
510 + last_len;
511 let mut buf = self.buffer;
512 if buf.len() > used {
513 buf.truncate(used);
514 }
515 return Some(buf.freeze());
516 }
517
518 let total: usize = self.lengths.iter().sum();
521 let mut out = BytesMut::with_capacity(total);
522 for (i, &len) in self.lengths.iter().enumerate() {
523 if len == 0 {
524 continue;
525 }
526 let start = i * self.packet_payload;
527 let end = start + len;
528 out.extend_from_slice(&self.buffer[start..end]);
529 }
530 Some(out.freeze())
531 }
532}
533
534#[derive(Debug, Clone)]
536pub struct ResendPlanner {
537 retries: u32,
538 max_retries: u32,
539 base_delay: Duration,
540 next_deadline: Instant,
541}
542
543impl ResendPlanner {
544 pub fn new(max_retries: u32, base_delay: Duration) -> Self {
545 Self {
546 retries: 0,
547 max_retries,
548 base_delay,
549 next_deadline: Instant::now(),
550 }
551 }
552
553 pub fn should_resend(&self, now: Instant) -> bool {
555 self.retries < self.max_retries && now >= self.next_deadline
556 }
557
558 pub fn record_attempt(&mut self, now: Instant, jitter: Duration) {
560 self.retries += 1;
561 let base = self
562 .base_delay
563 .checked_mul(self.retries)
564 .unwrap_or(self.base_delay);
565 self.next_deadline = now + base + jitter;
566 }
567
568 pub fn is_exhausted(&self) -> bool {
570 self.retries >= self.max_retries
571 }
572}
573
574#[derive(Debug, Clone)]
576pub struct CompletedFrame {
577 pub block_id: u64,
578 pub timestamp: Instant,
579 pub payload: Bytes,
580}
581
582#[derive(Debug)]
585pub struct FrameQueue {
586 inner: VecDeque<CompletedFrame>,
587 capacity: usize,
588}
589
590impl FrameQueue {
591 pub fn new(capacity: usize) -> Self {
592 Self {
593 inner: VecDeque::with_capacity(capacity),
594 capacity,
595 }
596 }
597
598 pub fn push(&mut self, frame: CompletedFrame, stats: &StreamStatsAccumulator) {
599 if self.inner.len() == self.capacity {
600 self.inner.pop_front();
601 stats.record_backpressure_drop();
602 }
603 self.inner.push_back(frame);
604 }
605
606 pub fn pop(&mut self) -> Option<CompletedFrame> {
607 self.inner.pop_front()
608 }
609}
610
611pub fn coalesce_missing(bitmap: &PacketBitmap, max_range: usize) -> Vec<RangeInclusive<u32>> {
613 bitmap
614 .missing_ranges()
615 .into_iter()
616 .flat_map(|range| split_range(range, max_range))
617 .collect()
618}
619
620fn split_range(range: RangeInclusive<u32>, max_len: usize) -> Vec<RangeInclusive<u32>> {
621 let start = *range.start() as usize;
622 let end = *range.end() as usize;
623 if max_len == 0 {
624 return vec![range];
625 }
626 let mut result = Vec::new();
627 let mut current = start;
628 while current <= end {
629 let upper = (current + max_len - 1).min(end);
630 result.push(current as u32..=upper as u32);
631 current = upper + 1;
632 }
633 result
634}
635
636#[derive(Debug)]
638pub struct Reassembler {
639 active: Option<FrameAssembly>,
640 packet_payload: usize,
641 stats: StreamStatsAccumulator,
642}
643
644impl Reassembler {
645 pub fn new(packet_payload: usize, stats: StreamStatsAccumulator) -> Self {
646 Self {
647 active: None,
648 packet_payload,
649 stats,
650 }
651 }
652
653 pub fn start_block(&mut self, block_id: u64, expected_packets: usize, buffer: BytesMut) {
655 let deadline = Instant::now() + Duration::from_millis(50);
656 self.active = Some(FrameAssembly::new(
657 block_id,
658 expected_packets,
659 self.packet_payload,
660 buffer,
661 deadline,
662 ));
663 }
664
665 pub fn push_packet(&mut self, packet_id: usize, payload: &[u8]) {
667 if let Some(assembly) = self.active.as_mut()
668 && assembly.ingest(packet_id, payload)
669 {
670 self.stats.record_packet();
671 }
672 }
673
674 pub fn finish_block(&mut self) -> Option<Bytes> {
676 self.active.take().and_then(FrameAssembly::finish)
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn parse_multiple_chunks() {
686 let mut payload = Vec::new();
687 payload.extend_from_slice(&0x0001u16.to_be_bytes());
688 payload.extend_from_slice(&0u16.to_be_bytes());
689 payload.extend_from_slice(&4u32.to_be_bytes());
690 payload.extend_from_slice(&[1, 2, 3, 4]);
691 payload.extend_from_slice(&0x0002u16.to_be_bytes());
692 payload.extend_from_slice(&0u16.to_be_bytes());
693 payload.extend_from_slice(&2u32.to_be_bytes());
694 payload.extend_from_slice(&[5, 6]);
695 let chunks = parse_chunks(&payload);
696 assert_eq!(chunks.len(), 2);
697 assert_eq!(chunks[0].id, 0x0001);
698 assert_eq!(chunks[0].data.as_ref(), &[1, 2, 3, 4]);
699 assert_eq!(chunks[1].id, 0x0002);
700 assert_eq!(chunks[1].data.as_ref(), &[5, 6]);
701 }
702
703 #[test]
704 fn truncated_chunk_is_ignored() {
705 let payload = vec![0u8; 6];
706 let chunks = parse_chunks(&payload);
707 assert!(chunks.is_empty());
708 }
709
710 fn golden_trailer(status: u16, payload_type: u16, size_y: u32, chunks: &[u8]) -> Vec<u8> {
717 let mut pkt = Vec::new();
718 pkt.extend_from_slice(&status.to_be_bytes());
719 pkt.extend_from_slice(&0x0007u16.to_be_bytes()); pkt.push(0x02); pkt.extend_from_slice(&[0x00, 0x00, 0x42]); pkt.extend_from_slice(&0u16.to_be_bytes()); pkt.extend_from_slice(&payload_type.to_be_bytes());
724 pkt.extend_from_slice(&size_y.to_be_bytes());
725 pkt.extend_from_slice(chunks);
726 pkt
727 }
728
729 #[test]
734 fn trailer_without_chunks_yields_no_chunk_region() {
735 let pkt = golden_trailer(0, PAYLOAD_TYPE_IMAGE as u16, 1536, &[]);
736 assert_eq!(pkt.len(), 16, "8-byte header + 8-byte trailer payload");
737
738 let GvspPacket::Trailer {
739 block_id,
740 packet_id,
741 status,
742 payload_type,
743 size_y,
744 chunk_data,
745 } = parse_packet(&pkt).expect("parse trailer")
746 else {
747 panic!("expected a trailer");
748 };
749
750 assert_eq!(block_id, 0x0007);
751 assert_eq!(packet_id, 0x42);
752 assert_eq!(status, 0);
753 assert_eq!(payload_type, 0x0001);
754 assert_eq!(size_y, 1536);
755 assert!(
756 chunk_data.is_empty(),
757 "chunk region begins at offset 8, so a plain image trailer has none"
758 );
759 assert!(parse_chunks(&chunk_data).is_empty());
760 }
761
762 #[test]
765 fn trailer_chunk_region_starts_after_the_payload_header() {
766 let mut chunks = Vec::new();
767 chunks.extend_from_slice(&0x0001u16.to_be_bytes()); chunks.extend_from_slice(&0u16.to_be_bytes()); chunks.extend_from_slice(&8u32.to_be_bytes()); chunks.extend_from_slice(&0x0123_4567_89AB_CDEFu64.to_be_bytes());
771
772 let pkt = golden_trailer(0, 0x4001, 1536, &chunks);
775
776 let GvspPacket::Trailer {
777 payload_type,
778 chunk_data,
779 ..
780 } = parse_packet(&pkt).expect("parse trailer")
781 else {
782 panic!("expected a trailer");
783 };
784
785 assert_eq!(payload_type, 0x4001);
786 let parsed = parse_chunks(&chunk_data);
787 assert_eq!(
788 parsed.len(),
789 1,
790 "exactly one chunk, not a desynchronised run"
791 );
792 assert_eq!(parsed[0].id, 0x0001);
793 assert_eq!(
794 parsed[0].data.as_ref(),
795 &0x0123_4567_89AB_CDEFu64.to_be_bytes()
796 );
797 }
798
799 #[test]
803 fn trailer_status_comes_from_the_packet_header() {
804 let pkt = golden_trailer(0x8004, PAYLOAD_TYPE_IMAGE as u16, 0, &[]);
807 let GvspPacket::Trailer { status, .. } = parse_packet(&pkt).expect("parse trailer") else {
808 panic!("expected a trailer");
809 };
810 assert_eq!(status, 0x8004);
811 }
812
813 #[test]
814 fn parse_chunks_tolerates_padding() {
815 for _ in 0..128 {
816 let count = fastrand::usize(..6);
817 let mut payload = Vec::new();
818 let mut entries = Vec::new();
819 for _ in 0..count {
820 let id = fastrand::u16(..);
821 let len = fastrand::usize(..16);
822 let mut data = vec![0u8; len];
823 for byte in &mut data {
824 *byte = fastrand::u8(..);
825 }
826 payload.extend_from_slice(&id.to_be_bytes());
827 payload.extend_from_slice(&0u16.to_be_bytes());
828 payload.extend_from_slice(&(data.len() as u32).to_be_bytes());
829 payload.extend_from_slice(&data);
830 entries.push((id, data));
831 }
832 let padding_len = fastrand::usize(..8);
833 for _ in 0..padding_len {
834 payload.push(fastrand::u8(..));
835 }
836 let parsed = parse_chunks(&payload);
837 assert!(parsed.len() <= entries.len());
838 for (idx, chunk) in parsed.iter().enumerate() {
839 assert_eq!(chunk.id, entries[idx].0);
840 assert_eq!(chunk.data.as_ref(), entries[idx].1.as_slice());
841 }
842 }
843 }
844
845 #[test]
846 fn bitmap_missing_ranges_coalesce() {
847 let mut bitmap = PacketBitmap::new(10);
848 for &idx in &[0usize, 1, 5, 6, 9] {
849 bitmap.set(idx);
850 }
851 let ranges = bitmap.missing_ranges();
852 assert_eq!(ranges.len(), 2);
853 assert_eq!(ranges[0], 2..=4);
854 assert_eq!(ranges[1], 7..=8);
855 }
856
857 #[test]
858 fn coalesce_splits_large_ranges() {
859 let mut bitmap = PacketBitmap::new(20);
860 for idx in [0usize, 1, 2, 18, 19] {
861 bitmap.set(idx);
862 }
863 let ranges = coalesce_missing(&bitmap, 4);
864 assert_eq!(ranges, vec![3..=6, 7..=10, 11..=14, 15..=17]);
865 }
866
867 #[test]
868 fn reassembler_finishes_frame() {
869 let stats = StreamStatsAccumulator::new();
870 let mut reassembler = Reassembler::new(4, stats);
871 reassembler.start_block(1, 3, BytesMut::with_capacity(12));
872 reassembler.push_packet(0, &[1, 2, 3]);
873 reassembler.push_packet(1, &[4, 5, 6]);
874 reassembler.push_packet(2, &[7, 8, 9]);
875 let frame = reassembler.finish_block().expect("frame");
876 assert_eq!(frame.as_ref(), &[1, 2, 3, 4, 5, 6, 7, 8, 9]);
877 }
878}