xref: /xiu/protocol/webrtc/src/http/mod.rs (revision f3f517c7)
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