1 pub mod define;
2 use indexmap::IndexMap;
3
4 macro_rules! scanf {
5 ( $string:expr, $sep:expr, $( $x:ty ),+ ) => {{
6 let mut iter = $string.split($sep);
7 ($(iter.next().and_then(|word| word.parse::<$x>().ok()),)*)
8 }}
9 }
10
11 pub trait Unmarshal {
unmarshal(request_data: &str) -> Option<Self> where Self: Sized12 fn unmarshal(request_data: &str) -> Option<Self>
13 where
14 Self: Sized;
15 }
16
17 pub trait Marshal {
marshal(&self) -> String18 fn marshal(&self) -> String;
19 }
20
21 #[derive(Debug, Clone, Default)]
22 pub struct HttpRequest {
23 pub method: String,
24 pub address: String,
25 pub port: u16,
26 pub path: String,
27 pub path_parameters: Option<String>,
28 //parse path_parameters and save the results
29 pub path_parameters_map: IndexMap<String, String>,
30 pub version: String,
31 pub headers: IndexMap<String, String>,
32 pub body: Option<String>,
33 }
34
35 impl HttpRequest {
get_header(&self, header_name: &String) -> Option<&String>36 pub fn get_header(&self, header_name: &String) -> Option<&String> {
37 self.headers.get(header_name)
38 }
39 }
40
parse_content_length(request_data: &str) -> Option<u32>41 pub fn parse_content_length(request_data: &str) -> Option<u32> {
42 let start = "Content-Length:";
43 let end = "\r\n";
44
45 let start_index = request_data.find(start)? + start.len();
46 let end_index = request_data[start_index..].find(end)? + start_index;
47 let length_str = &request_data[start_index..end_index];
48
49 length_str.trim().parse().ok()
50 }
51
52 impl Unmarshal for HttpRequest {
unmarshal(request_data: &str) -> Option<Self>53 fn unmarshal(request_data: &str) -> Option<Self> {
54 log::trace!("len: {} content: {}", request_data.len(), request_data);
55
56 let mut http_request = HttpRequest::default();
57 let header_end_idx = if let Some(idx) = request_data.find("\r\n\r\n") {
58 let data_except_body = &request_data[..idx];
59 let mut lines = data_except_body.lines();
60 //parse the first line
61 //POST /whip?app=live&stream=test HTTP/1.1
62 if let Some(request_first_line) = lines.next() {
63 let mut fields = request_first_line.split_ascii_whitespace();
64 if let Some(method) = fields.next() {
65 http_request.method = method.to_string();
66 }
67 if let Some(path) = fields.next() {
68 let path_data: Vec<&str> = path.splitn(2, '?').collect();
69 http_request.path = path_data[0].to_string();
70
71 if path_data.len() > 1 {
72 let pars = path_data[1].to_string();
73 let pars_array: Vec<&str> = pars.split('&').collect();
74
75 for ele in pars_array {
76 let (k, v) = scanf!(ele, '=', String, String);
77 if k.is_none() || v.is_none() {
78 continue;
79 }
80 http_request
81 .path_parameters_map
82 .insert(k.unwrap(), v.unwrap());
83 }
84 http_request.path_parameters = Some(pars);
85 }
86 }
87 if let Some(version) = fields.next() {
88 http_request.version = version.to_string();
89 }
90 }
91 //parse headers
92 for line in lines {
93 if let Some(index) = line.find(": ") {
94 let name = line[..index].to_string();
95 let value = line[index + 2..].to_string();
96 if name == "Host" {
97 let (address_val, port_val) = scanf!(value, ':', String, u16);
98 if let Some(address) = address_val {
99 http_request.address = address;
100 }
101 if let Some(port) = port_val {
102 http_request.port = port;
103 }
104 }
105 http_request.headers.insert(name, value);
106 }
107 }
108 idx + 4
109 } else {
110 return None;
111 };
112 log::trace!(
113 "header_end_idx is: {} {}",
114 header_end_idx,
115 request_data.len()
116 );
117
118 if request_data.len() > header_end_idx {
119 //parse body
120 http_request.body = Some(request_data[header_end_idx..].to_string());
121 }
122
123 Some(http_request)
124 }
125 }
126
127 impl Marshal for HttpRequest {
marshal(&self) -> String128 fn marshal(&self) -> String {
129 let full_path = if let Some(parameters) = &self.path_parameters {
130 format!("{}?{}", self.path, parameters)
131 } else {
132 self.path.clone()
133 };
134 let mut request_str = format!("{} {} {}\r\n", self.method, full_path, self.version);
135 for (header_name, header_value) in &self.headers {
136 if header_name == &"Content-Length".to_string() {
137 if let Some(body) = &self.body {
138 request_str += &format!("Content-Length: {}\r\n", body.len());
139 }
140 } else {
141 request_str += &format!("{header_name}: {header_value}\r\n");
142 }
143 }
144
145 request_str += "\r\n";
146 if let Some(body) = &self.body {
147 request_str += body;
148 }
149 request_str
150 }
151 }
152
153 #[derive(Debug, Clone, Default)]
154 pub struct HttpResponse {
155 pub version: String,
156 pub status_code: u16,
157 pub reason_phrase: String,
158 pub headers: IndexMap<String, String>,
159 pub body: Option<String>,
160 }
161
162 impl Unmarshal for HttpResponse {
unmarshal(request_data: &str) -> Option<Self>163 fn unmarshal(request_data: &str) -> Option<Self> {
164 let mut http_response = HttpResponse::default();
165 let header_end_idx = if let Some(idx) = request_data.find("\r\n\r\n") {
166 let data_except_body = &request_data[..idx];
167 let mut lines = data_except_body.lines();
168 //parse the first line
169 if let Some(request_first_line) = lines.next() {
170 let mut fields = request_first_line.split_ascii_whitespace();
171
172 if let Some(version) = fields.next() {
173 http_response.version = version.to_string();
174 }
175 if let Some(status) = fields.next() {
176 if let Ok(status) = status.parse::<u16>() {
177 http_response.status_code = status;
178 }
179 }
180 if let Some(reason_phrase) = fields.next() {
181 http_response.reason_phrase = reason_phrase.to_string();
182 }
183 }
184 //parse headers
185 for line in lines {
186 if let Some(index) = line.find(": ") {
187 let name = line[..index].to_string();
188 let value = line[index + 2..].to_string();
189 http_response.headers.insert(name, value);
190 }
191 }
192 idx + 4
193 } else {
194 return None;
195 };
196
197 if request_data.len() > header_end_idx {
198 //parse body
199 http_response.body = Some(request_data[header_end_idx..].to_string());
200 }
201
202 Some(http_response)
203 }
204 }
205
206 impl Marshal for HttpResponse {
marshal(&self) -> String207 fn marshal(&self) -> String {
208 let mut response_str = format!(
209 "{} {} {}\r\n",
210 self.version, self.status_code, self.reason_phrase
211 );
212
213 for (header_name, header_value) in &self.headers {
214 if header_name != &"Content-Length".to_string() {
215 response_str += &format!("{header_name}: {header_value}\r\n");
216 }
217 }
218
219 if let Some(body) = &self.body {
220 response_str += &format!("Content-Length: {}\r\n", body.len());
221 }
222
223 response_str += "\r\n";
224 if let Some(body) = &self.body {
225 response_str += body;
226 }
227 response_str
228 }
229 }
230
231 #[cfg(test)]
232 mod tests {
233
234 use super::Marshal;
235 use super::Unmarshal;
236
237 use super::HttpRequest;
238 use super::HttpResponse;
239
240 use indexmap::IndexMap;
241 use std::io::BufRead;
242 #[allow(dead_code)]
read_headers(reader: &mut dyn BufRead) -> Option<IndexMap<String, String>>243 fn read_headers(reader: &mut dyn BufRead) -> Option<IndexMap<String, String>> {
244 let mut headers = IndexMap::new();
245 loop {
246 let mut line = String::new();
247 match reader.read_line(&mut line) {
248 Ok(0) => break,
249 Ok(_) => {
250 if let Some(index) = line.find(": ") {
251 let name = line[..index].to_string();
252 let value = line[index + 2..].trim().to_string();
253 headers.insert(name, value);
254 }
255 }
256 Err(_) => return None,
257 }
258 }
259 Some(headers)
260 }
261
262 #[test]
test_parse_http_request()263 fn test_parse_http_request() {
264 let request = "POST /whip/endpoint?app=live&stream=test HTTP/1.1\r\n\
265 Host: whip.example.com\r\n\
266 Content-Type: application/sdp\r\n\
267 Content-Length: 1326\r\n\
268 \r\n\
269 v=0\r\n\
270 o=- 5228595038118931041 2 IN IP4 127.0.0.1\r\n\
271 s=-\r\n\
272 t=0 0\r\n\
273 a=group:BUNDLE 0 1\r\n\
274 a=extmap-allow-mixed\r\n\
275 a=msid-semantic: WMS\r\n\
276 m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
277 c=IN IP4 0.0.0.0\r\n\
278 a=rtcp:9 IN IP4 0.0.0.0\r\n\
279 a=ice-ufrag:EsAw\r\n\
280 a=ice-pwd:bP+XJMM09aR8AiX1jdukzR6Y\r\n\
281 a=ice-options:trickle\r\n\
282 a=fingerprint:sha-256 DA:7B:57:DC:28:CE:04:4F:31:79:85:C4:31:67:EB:27:58:29:ED:77:2A:0D:24:AE:ED:AD:30:BC:BD:F1:9C:02\r\n\
283 a=setup:actpass\r\n\
284 a=mid:0\r\n\
285 a=bundle-only\r\n\
286 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
287 a=sendonly\r\n\
288 a=msid:- d46fb922-d52a-4e9c-aa87-444eadc1521b\r\n\
289 a=rtcp-mux\r\n\
290 a=rtpmap:111 opus/48000/2\r\n\
291 a=fmtp:111 minptime=10;useinbandfec=1\r\n\
292 m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
293 c=IN IP4 0.0.0.0\r\n\
294 a=rtcp:9 IN IP4 0.0.0.0\r\n\
295 a=ice-ufrag:EsAw\r\n\
296 a=ice-pwd:bP+XJMM09aR8AiX1jdukzR6Y\r\n\
297 a=ice-options:trickle\r\n\
298 a=fingerprint:sha-256 DA:7B:57:DC:28:CE:04:4F:31:79:85:C4:31:67:EB:27:58:29:ED:77:2A:0D:24:AE:ED:AD:30:BC:BD:F1:9C:02\r\n\
299 a=setup:actpass\r\n\
300 a=mid:1\r\n\
301 a=bundle-only\r\n\
302 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
303 a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
304 a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
305 a=sendonly\r\n\
306 a=msid:- d46fb922-d52a-4e9c-aa87-444eadc1521b\r\n\
307 a=rtcp-mux\r\n\
308 a=rtcp-rsize\r\n\
309 a=rtpmap:96 VP8/90000\r\n\
310 a=rtcp-fb:96 ccm fir\r\n\
311 a=rtcp-fb:96 nack\r\n\
312 a=rtcp-fb:96 nack pli\r\n\
313 a=rtpmap:97 rtx/90000\r\n\
314 a=fmtp:97 apt=96\r\n";
315
316 if let Some(parser) = HttpRequest::unmarshal(request) {
317 println!(" parser: {parser:?}");
318 let marshal_result = parser.marshal();
319 print!("marshal result: =={marshal_result}==");
320 assert_eq!(request, marshal_result);
321 }
322 }
323
324 #[test]
test_whep_request()325 fn test_whep_request() {
326 let request = "POST /whep?app=live&stream=test HTTP/1.1\r\n\
327 Host: localhost:3000\r\n\
328 Accept: */*\r\n\
329 Sec-Fetch-Site: same-origin\r\n\
330 Accept-Language: zh-cn\r\n\
331 Accept-Encoding: gzip, deflate\r\n\
332 Sec-Fetch-Mode: cors\r\n\
333 Content-Type: application/sdp\r\n\
334 Origin: http://localhost:3000\r\n\
335 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15\r\n\
336 Referer: http://localhost:3000/\r\n\
337 Content-Length: 3895\r\n\
338 Connection: keep-alive\r\n\
339 Sec-Fetch-Dest: empty\r\n\
340 \r\n\
341 v=0\r\n\
342 o=- 6550659986740559335 2 IN IP4 127.0.0.1\r\n\
343 s=-\r\n\
344 t=0 0\r\n\
345 a=group:BUNDLE 0 1\r\n\
346 a=extmap-allow-mixed\r\n\
347 a=msid-semantic: WMS\r\n\
348 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 125 104 124 106 107 108 109 127 35\r\n\
349 c=IN IP4 0.0.0.0\r\n\
350 a=rtcp:9 IN IP4 0.0.0.0\r\n\
351 a=ice-ufrag:0mum\r\n\
352 a=ice-pwd:DD4LnAhZLQNLSzRZWZRh9Jm4\r\n\
353 a=ice-options:trickle\r\n\
354 a=fingerprint:sha-256 6C:61:89:FF:9D:2F:BA:0A:A4:80:0D:98:C3:CA:43:05:82:EB:59:13:BC:C8:DE:33:2F:26:4A:27:D8:D0:D1:3D\r\n\
355 a=setup:actpass\r\n\
356 a=mid:0\r\n\
357 a=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\n\
358 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
359 a=extmap:3 urn:3gpp:video-orientation\r\n\
360 a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
361 a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n\
362 a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n\
363 a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n\
364 a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n\
365 a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
366 a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
367 a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
368 a=recvonly\r\n\
369 a=rtcp-mux\r\n\
370 a=rtcp-rsize\r\n\
371 a=rtpmap:96 H264/90000\r\n\
372 a=rtcp-fb:96 goog-remb\r\n\
373 a=rtcp-fb:96 transport-cc\r\n\
374 a=rtcp-fb:96 ccm fir\r\n\
375 a=rtcp-fb:96 nack\r\n\
376 a=rtcp-fb:96 nack pli\r\n\
377 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f\r\n\
378 a=rtpmap:97 rtx/90000\r\n\
379 a=fmtp:97 apt=96\r\n\
380 a=rtpmap:98 H264/90000\r\n\
381 a=rtcp-fb:98 goog-remb\r\n\
382 a=rtcp-fb:98 transport-cc\r\n\
383 a=rtcp-fb:98 ccm fir\r\n\
384 a=rtcp-fb:98 nack\r\n\
385 a=rtcp-fb:98 nack pli\r\n\
386 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n\
387 a=rtpmap:99 rtx/90000\r\n\
388 a=fmtp:99 apt=98\r\n\
389 a=rtpmap:100 H264/90000\r\n\
390 a=rtcp-fb:100 goog-remb\r\n\
391 a=rtcp-fb:100 transport-cc\r\n\
392 a=rtcp-fb:100 ccm fir\r\n\
393 a=rtcp-fb:100 nack\r\n\
394 a=rtcp-fb:100 nack pli\r\n\
395 a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f\r\n\
396 a=rtpmap:101 rtx/90000\r\n\
397 a=fmtp:101 apt=100\r\n\
398 a=rtpmap:102 H264/90000\r\n\
399 a=rtcp-fb:102 goog-remb\r\n\
400 a=rtcp-fb:102 transport-cc\r\n\
401 a=rtcp-fb:102 ccm fir\r\n\
402 a=rtcp-fb:102 nack\r\n\
403 a=rtcp-fb:102 nack pli\r\n\
404 a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\r\n\
405 a=rtpmap:125 rtx/90000\r\n\
406 a=fmtp:125 apt=102\r\n\
407 a=rtpmap:104 VP8/90000\r\n\
408 a=rtcp-fb:104 goog-remb\r\n\
409 a=rtcp-fb:104 transport-cc\r\n\
410 a=rtcp-fb:104 ccm fir\r\n\
411 a=rtcp-fb:104 nack\r\n\
412 a=rtcp-fb:104 nack pli\r\n\
413 a=rtpmap:124 rtx/90000\r\n\
414 a=fmtp:124 apt=104\r\n\
415 a=rtpmap:106 VP9/90000\r\n\
416 a=rtcp-fb:106 goog-remb\r\n\
417 a=rtcp-fb:106 transport-cc\r\n\
418 a=rtcp-fb:106 ccm fir\r\n\
419 a=rtcp-fb:106 nack\r\n\
420 a=rtcp-fb:106 nack pli\r\n\
421 a=fmtp:106 profile-id=0\r\n\
422 a=rtpmap:107 rtx/90000\r\n\
423 a=fmtp:107 apt=106\r\n\
424 a=rtpmap:108 red/90000\r\n\
425 a=rtpmap:109 rtx/90000\r\n\
426 a=fmtp:109 apt=108\r\n\
427 a=rtpmap:127 ulpfec/90000\r\n\
428 a=rtpmap:35 flexfec-03/90000\r\n\
429 a=rtcp-fb:35 goog-remb\r\n\
430 a=rtcp-fb:35 transport-cc\r\n\
431 a=fmtp:35 repair-window=10000000\r\n\
432 m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 9 0 8 105 13 110 113 126\r\n\
433 c=IN IP4 0.0.0.0\r\n\
434 a=rtcp:9 IN IP4 0.0.0.0\r\n\
435 a=ice-ufrag:0mum\r\n\
436 a=ice-pwd:DD4LnAhZLQNLSzRZWZRh9Jm4\r\n\
437 a=ice-options:trickle\r\n\
438 a=fingerprint:sha-256 6C:61:89:FF:9D:2F:BA:0A:A4:80:0D:98:C3:CA:43:05:82:EB:59:13:BC:C8:DE:33:2F:26:4A:27:D8:D0:D1:3D\r\n\
439 a=setup:actpass\r\n\
440 a=mid:1\r\n\
441 a=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
442 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
443 a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
444 a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
445 a=recvonly\r\n\
446 a=rtcp-mux\r\n\
447 a=rtpmap:111 opus/48000/2\r\n\
448 a=rtcp-fb:111 transport-cc\r\n\
449 a=fmtp:111 minptime=10;useinbandfec=1\r\n\
450 a=rtpmap:63 red/48000/2\r\n\
451 a=fmtp:63 111/111\r\n\
452 a=rtpmap:103 ISAC/16000\r\n\
453 a=rtpmap:9 G722/8000\r\n\
454 a=rtpmap:0 PCMU/8000\r\n\
455 a=rtpmap:8 PCMA/8000\r\n\
456 a=rtpmap:105 CN/16000\r\n\
457 a=rtpmap:13 CN/8000\r\n\
458 a=rtpmap:110 telephone-event/48000\r\n\
459 a=rtpmap:113 telephone-event/16000\r\n\
460 a=rtpmap:126 telephone-event/8000\r\n";
461
462 if let Some(l) = super::parse_content_length(request) {
463 println!("content length is : {l}");
464 }
465
466 if let Some(parser) = HttpRequest::unmarshal(request) {
467 println!(" parser: {parser:?}");
468 let marshal_result = parser.marshal();
469 print!("marshal result: =={marshal_result}==");
470 assert_eq!(request, marshal_result);
471 }
472 }
473
474 #[test]
test_parse_http_response()475 fn test_parse_http_response() {
476 let response = "HTTP/1.1 201 Created\r\n\
477 Content-Type: application/sdp\r\n\
478 Location: https://whip.example.com/resource/id\r\n\
479 Content-Length: 1392\r\n\
480 \r\n\
481 v=0\r\n\
482 o=- 1657793490019 1 IN IP4 127.0.0.1\r\n\
483 s=-\r\n\
484 t=0 0\r\n\
485 a=group:BUNDLE 0 1\r\n\
486 a=extmap-allow-mixed\r\n\
487 a=ice-lite\r\n\
488 a=msid-semantic: WMS *\r\n\
489 m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
490 c=IN IP4 0.0.0.0\r\n\
491 a=rtcp:9 IN IP4 0.0.0.0\r\n\
492 a=ice-ufrag:38sdf4fdsf54\r\n\
493 a=ice-pwd:2e13dde17c1cb009202f627fab90cbec358d766d049c9697\r\n\
494 a=fingerprint:sha-256 F7:EB:F3:3E:AC:D2:EA:A7:C1:EC:79:D9:B3:8A:35:DA:70:86:4F:46:D9:2D:CC:D0:BC:81:9F:67:EF:34:2E:BD\r\n\
495 a=candidate:1 1 UDP 2130706431 198.51.100.1 39132 typ host\r\n\
496 a=setup:passive\r\n\
497 a=mid:0\r\n\
498 a=bundle-only\r\n\
499 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
500 a=recvonly\r\n\
501 a=rtcp-mux\r\n\
502 a=rtcp-rsize\r\n\
503 a=rtpmap:111 opus/48000/2\r\n\
504 a=fmtp:111 minptime=10;useinbandfec=1\r\n\
505 m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
506 c=IN IP4 0.0.0.0\r\n\
507 a=rtcp:9 IN IP4 0.0.0.0\r\n\
508 a=ice-ufrag:38sdf4fdsf54\r\n\
509 a=ice-pwd:2e13dde17c1cb009202f627fab90cbec358d766d049c9697\r\n\
510 a=fingerprint:sha-256 F7:EB:F3:3E:AC:D2:EA:A7:C1:EC:79:D9:B3:8A:35:DA:70:86:4F:46:D9:2D:CC:D0:BC:81:9F:67:EF:34:2E:BD\r\n\
511 a=candidate:1 1 UDP 2130706431 198.51.100.1 39132 typ host\r\n\
512 a=setup:passive\r\n\
513 a=mid:1\r\n\
514 a=bundle-only\r\n\
515 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
516 a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
517 a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
518 a=recvonly\r\n\
519 a=rtcp-mux\r\n\
520 a=rtcp-rsize\r\n\
521 a=rtpmap:96 VP8/90000\r\n\
522 a=rtcp-fb:96 ccm fir\r\n\
523 a=rtcp-fb:96 nack\r\n\
524 a=rtcp-fb:96 nack pli\r\n\
525 a=rtpmap:97 rtx/90000\r\n\
526 a=fmtp:97 apt=96\r\n";
527
528 if let Some(parser) = HttpResponse::unmarshal(response) {
529 println!(" parser: {parser:?}");
530 let marshal_result = parser.marshal();
531 print!("marshal result: =={marshal_result}==");
532 assert_eq!(response, marshal_result);
533 }
534 }
535 }
536