Skip to content
File

Blob: firmware/vendor/str0m/src/rtp/rtcp/psfbapp.rs

rust168 lines
1use std::sync::Arc;
2 
3use crate::rtp::Ssrc;
4 
5use super::RtcpType;
6use super::{FeedbackMessageType, PayloadType, RtcpHeader, RtcpPacket};
7 
8/// Application-specific Payload-Specific Feedback message (PSFB FMT=15, PT=206).
9///
10/// Generic container for FMT=15 RTCP PSFB messages that are not REMB.
11/// Carries an opaque application-dependent payload after the standard
12/// sender and media SSRC fields (RFC 4585 Section 6.4).
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct AppSpecificFeedback {
15 /// SSRC of the sender of this feedback message.
16 pub sender_ssrc: Ssrc,
17 /// SSRC of the media source this feedback relates to.
18 pub media_ssrc: Ssrc,
19 /// Application-dependent payload (after sender_ssrc and media_ssrc).
20 pub payload: Arc<[u8]>,
21}
22 
23impl RtcpPacket for AppSpecificFeedback {
24 fn header(&self) -> RtcpHeader {
25 RtcpHeader {
26 rtcp_type: RtcpType::PayloadSpecificFeedback,
27 feedback_message_type: FeedbackMessageType::PayloadFeedback(
28 PayloadType::ApplicationLayer,
29 ),
30 words_less_one: (self.length_words() - 1) as u16,
31 }
32 }
33 
34 fn length_words(&self) -> usize {
35 // 1 (RTCP header) + 2 (sender_ssrc + media_ssrc) + payload words
36 let payload_words = self.payload.len().div_ceil(4);
37 1 + 2 + payload_words
38 }
39 
40 fn write_to(&self, buf: &mut [u8]) -> usize {
41 let total_len = self.length_words() * 4;
42 assert!(
43 buf.len() >= total_len,
44 "AppSpecificFeedback::write_to: buffer too small ({} < {})",
45 buf.len(),
46 total_len,
47 );
48 
49 self.header().write_to(&mut buf[..4]);
50 buf[4..8].copy_from_slice(&self.sender_ssrc.to_be_bytes());
51 buf[8..12].copy_from_slice(&self.media_ssrc.to_be_bytes());
52 
53 let payload_len = self.payload.len();
54 buf[12..][..payload_len].copy_from_slice(&self.payload);
55 
56 // Pad to 4-byte boundary
57 let pad_len = payload_len.next_multiple_of(4) - payload_len;
58 buf[12 + payload_len..][..pad_len].fill(0);
59 
60 12 + payload_len + pad_len
61 }
62}
63 
64impl<'a> TryFrom<&'a [u8]> for AppSpecificFeedback {
65 type Error = &'static str;
66 
67 fn try_from(buf: &'a [u8]) -> Result<Self, Self::Error> {
68 if buf.len() < 8 {
69 return Err("AppSpecificFeedback less than 8 bytes");
70 }
71 
72 let (ssrc_buf, payload) = buf.split_at(8);
73 let sender_ssrc =
74 u32::from_be_bytes([ssrc_buf[0], ssrc_buf[1], ssrc_buf[2], ssrc_buf[3]]).into();
75 let media_ssrc =
76 u32::from_be_bytes([ssrc_buf[4], ssrc_buf[5], ssrc_buf[6], ssrc_buf[7]]).into();
77 
78 Ok(AppSpecificFeedback {
79 sender_ssrc,
80 media_ssrc,
81 payload: payload.into(),
82 })
83 }
84}
85 
86#[cfg(test)]
87mod tests {
88 use super::*;
89 
90 #[test]
91 fn test_round_trip() {
92 let input = AppSpecificFeedback {
93 sender_ssrc: 1.into(),
94 media_ssrc: 2.into(),
95 payload: vec![0x01, 0x00, 0x00, 0x44, 0xAA, 0xBB, 0xCC, 0xDD].into(),
96 };
97 
98 let mut buf = [0u8; 256];
99 let written = input.write_to(&mut buf);
100 
101 // Parse back (skip 4-byte RTCP header)
102 let parsed = AppSpecificFeedback::try_from(&buf[4..written]).unwrap();
103 assert_eq!(input.sender_ssrc, parsed.sender_ssrc);
104 assert_eq!(input.media_ssrc, parsed.media_ssrc);
105 assert_eq!(input.payload, parsed.payload);
106 }
107 
108 #[test]
109 fn test_header() {
110 let fb = AppSpecificFeedback {
111 sender_ssrc: 0.into(),
112 media_ssrc: 0.into(),
113 payload: vec![1, 2, 3, 4].into(),
114 };
115 
116 let header = fb.header();
117 assert_eq!(header.rtcp_type, RtcpType::PayloadSpecificFeedback);
118 }
119 
120 #[test]
121 fn test_non_aligned_payload() {
122 // Payload not aligned to 4 bytes — should still round-trip
123 let input = AppSpecificFeedback {
124 sender_ssrc: 100.into(),
125 media_ssrc: 200.into(),
126 payload: vec![0xAA, 0xBB, 0xCC].into(), // 3 bytes, needs 1 byte padding
127 };
128 
129 let mut buf = [0u8; 256];
130 let written = input.write_to(&mut buf);
131 
132 // Total should be 4 (header) + 4 (sender) + 4 (media) + 4 (padded payload) = 16
133 assert_eq!(written, 16);
134 
135 let parsed = AppSpecificFeedback::try_from(&buf[4..written]).unwrap();
136 assert_eq!(parsed.sender_ssrc, input.sender_ssrc);
137 assert_eq!(parsed.media_ssrc, input.media_ssrc);
138 // Parsed payload includes padding byte
139 assert_eq!(parsed.payload.len(), 4);
140 assert_eq!(&parsed.payload[..3], &[0xAA, 0xBB, 0xCC]);
141 assert_eq!(parsed.payload[3], 0); // padding
142 }
143 
144 #[test]
145 fn test_empty_payload() {
146 let input = AppSpecificFeedback {
147 sender_ssrc: 1.into(),
148 media_ssrc: 2.into(),
149 payload: vec![].into(),
150 };
151 
152 let mut buf = [0u8; 256];
153 let written = input.write_to(&mut buf);
154 assert_eq!(written, 12); // header + sender + media, no payload
155 
156 let parsed = AppSpecificFeedback::try_from(&buf[4..written]).unwrap();
157 assert_eq!(parsed.sender_ssrc, input.sender_ssrc);
158 assert_eq!(parsed.media_ssrc, input.media_ssrc);
159 assert!(parsed.payload.is_empty());
160 }
161 
162 #[test]
163 fn test_too_short_rejected() {
164 let buf = [0u8; 7]; // Less than 8 bytes
165 assert!(AppSpecificFeedback::try_from(&buf[..]).is_err());
166 }
167}