Skip to content
File

Blob: firmware/vendor/str0m/src/sdp/parser.rs

rust1819 lines
1use std::net::{IpAddr, SocketAddr};
2use std::str::FromStr;
3 
4use crate::crypto::Fingerprint;
5use crate::io::TcpType;
6use crate::rtp_::{Direction, Extension, Frequency, Mid, Pt, SessionId, Ssrc};
7use crate::{Candidate, CandidateKind};
8use combine::error::*;
9use combine::parser::char::*;
10use combine::parser::combinator::*;
11use combine::stream::StreamErrorFor;
12use combine::*;
13use combine::{ParseError, Parser, Stream};
14 
15use super::data::*;
16 
17/// Creates a parser of SDP
18pub fn sdp_parser<Input>() -> impl Parser<Input, Output = Sdp>
19where
20 Input: Stream<Token = char>,
21 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
22{
23 (session_parser(), many::<Vec<_>, _, _>(media_parser())).map(|(session, media)| Sdp {
24 session,
25 media_lines: media,
26 })
27}
28 
29// /////////////////////////////////////////////////// Session description
30 
31/// 1. First line must be v=0
32/// 2. The second SDP line MUST be an "o=" line The sess-id MUST be representable by a 64-bit signed
33/// integer, and the initial value MUST be less than (2**62)-1
34/// 3. Third line is the session name. The "s=" field MUST NOT be empty and SHOULD contain
35/// ISO 10646 characters (RFC 8866 section 5.3).
36///
37/// Session is over when we find a "t=" line MUST be added, both <start-time> and <stop-time>
38/// SHOULD be set to zero, e.g. "t=0 0".
39pub fn session_parser<Input>() -> impl Parser<Input, Output = Session>
40where
41 Input: Stream<Token = char>,
42 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
43{
44 (
45 typed_line('v', token('0')), // v=0
46 originator_line(), // o=- 6564425948916445306 2 IN IP4 127.0.0.1
47 // The "s=" field MUST NOT be empty (RFC 8866 section 5.3)
48 typed_line('s', any_value()),
49 many::<Vec<_>, _, _>(ignored_session_line()),
50 optional(bandwidth_line()), // b=CT:1234
51 typed_line('t', string("0 0")), // t=0 0
52 many::<Vec<_>, _, _>(typed_line('r', any_value())), // r should never appear
53 //
54 many::<Vec<_>, _, _>(session_attribute_line()),
55 )
56 .map(|(_, id, _, _, bw, _, _, attrs)| Session { id, bw, attrs })
57}
58 
59/// `o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>`
60fn originator_line<Input>() -> impl Parser<Input, Output = SessionId>
61where
62 Input: Stream<Token = char>,
63 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
64{
65 let session_string = typed_line(
66 'o',
67 // o=- 6564425948916445306 2 IN IP4 127.0.0.1
68 (
69 not_sp(),
70 token(' '),
71 many1::<String, _, _>(digit()),
72 token(' '),
73 any_value(),
74 )
75 .map(|(_, _, sess, _, _)| sess),
76 );
77 from_str(session_string).map(|x: u64| -> SessionId { SessionId::from(x) })
78}
79 
80/// `b=<bwtype>:<bandwidth>`
81fn bandwidth_line<Input>() -> impl Parser<Input, Output = Bandwidth>
82where
83 Input: Stream<Token = char>,
84 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
85{
86 typed_line('b', (many1(satisfy(|c| c != ':')), token(':'), any_value()))
87 .map(|(typ, _, val)| Bandwidth { typ, val })
88}
89 
90/// An a= line that with value like: `a=<attribute>:<value>`.
91fn attribute_line<Input, Pval, Out>(
92 attribute: &'static str,
93 val: Pval,
94) -> impl Parser<Input, Output = Out>
95where
96 Input: Stream<Token = char>,
97 Pval: Parser<Input, Output = Out>,
98 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
99{
100 typed_line('a', (string(attribute), token(':'), val)).map(|(_, _, val)| val)
101}
102 
103/// An a= line that has no value like: `a=ice-lite`.
104fn attribute_line_flag<Input>(attribute: &'static str) -> impl Parser<Input, Output = ()>
105where
106 Input: Stream<Token = char>,
107 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
108{
109 typed_line('a', (string(attribute)).map(|_| ()))
110}
111 
112/// a=foo:bar lines belonging before the first m= line
113fn session_attribute_line<Input>() -> impl Parser<Input, Output = SessionAttribute>
114where
115 Input: Stream<Token = char>,
116 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
117{
118 // a=group:BUNDLE 1 2
119 // a=group:LS 1 2
120 let group = attribute_line(
121 "group",
122 (
123 not_sp(),
124 token(' '),
125 sep_by1(not_sp().map(|m| Mid::from(m.as_str())), token(' ')),
126 ),
127 )
128 .map(|(typ, _, mids)| SessionAttribute::Group { typ, mids });
129 
130 // a=msid-semantic: WMS 9ce81ef6-c7cb-4da5-8f5f-3e7cc9b5f9b0
131 let msid_semantic = attribute_line(
132 "msid-semantic",
133 (
134 optional(token(' ')),
135 not_sp(),
136 token(' '),
137 sep_by1(not_sp(), token(' ')),
138 ),
139 )
140 .map(
141 |(_, semantic, _, stream_ids)| SessionAttribute::MsidSemantic {
142 semantic,
143 stream_ids,
144 },
145 );
146 
147 // a=ice-lite
148 let ice_lite = attribute_line_flag("ice-lite").map(|_| SessionAttribute::IceLite);
149 
150 // a=ice-ufrag:IdNYTNL1fjvjyEzL
151 let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(SessionAttribute::IceUfrag);
152 
153 // a=ice-pwd:4d64pT3T1xfwbZvi9fQKjoPb
154 let ice_pwd = attribute_line("ice-pwd", any_value()).map(SessionAttribute::IcePwd);
155 
156 // a=ice-options:trickle
157 let ice_opt = attribute_line("ice-options", any_value()).map(SessionAttribute::IceOptions);
158 
159 // a=fingerprint:sha-256 45:AD:5C:82:F8:BE:B5:2A:D1:74:A6:16:D0:50:CD:86:9C:97:9D:BD:06:8C:C9:85:C9:CD:AB:2B:A8:56:03:CD
160 // "sha-1" / "sha-224" / "sha-256" /
161 // "sha-384" / "sha-512" /
162 // "md5" / "md2"
163 let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
164 u8::from_str_radix(&x, 16).map_err(StreamErrorFor::<Input>::message_format)
165 });
166 let finger = attribute_line(
167 "fingerprint",
168 (not_sp(), token(' '), sep_by1(hex_byte, token(':'))),
169 )
170 .map(|(hash_func, _, bytes)| SessionAttribute::Fingerprint(Fingerprint { hash_func, bytes }));
171 
172 let setup_val = choice((
173 attempt(string("actpass").map(|_| Setup::ActPass)),
174 attempt(string("active").map(|_| Setup::Active)),
175 attempt(string("passive").map(|_| Setup::Passive)),
176 ));
177 
178 // a=setup:actpass
179 let setup = attribute_line("setup", setup_val).map(SessionAttribute::Setup);
180 
181 // a=candidate
182 let cand = candidate_attribute().map(SessionAttribute::Candidate);
183 
184 // a=end-of-candidates
185 let endof = attribute_line_flag("end-of-candidates").map(|_| SessionAttribute::EndOfCandidates);
186 
187 // tls-id
188 // identity
189 // extmap
190 let unused = typed_line('a', any_value()).map(SessionAttribute::Unused);
191 
192 choice((
193 attempt(group),
194 attempt(msid_semantic),
195 attempt(ice_lite),
196 attempt(ice_ufrag),
197 attempt(ice_pwd),
198 attempt(ice_opt),
199 attempt(finger),
200 attempt(setup),
201 attempt(cand),
202 attempt(endof),
203 unused,
204 ))
205}
206 
207/// Parser for candidate, without attribute prefix (a=).
208fn candidate<Input>() -> impl Parser<Input, Output = Candidate>
209where
210 Input: Stream<Token = char>,
211 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
212{
213 // Reference: https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
214 let port = || {
215 not_sp::<Input>().and_then(|s| {
216 s.parse::<u16>()
217 .map_err(StreamErrorFor::<Input>::message_format)
218 })
219 };
220 
221 let ip_addr = || {
222 not_sp().and_then(|s| {
223 s.parse::<IpAddr>()
224 .map_err(StreamErrorFor::<Input>::message_format)
225 })
226 };
227 
228 let kind = choice((
229 string("host").map(|_| CandidateKind::Host),
230 string("prflx").map(|_| CandidateKind::PeerReflexive),
231 string("srflx").map(|_| CandidateKind::ServerReflexive),
232 string("relay").map(|_| CandidateKind::Relayed),
233 ));
234 
235 (
236 string("candidate:").and_then(|s| {
237 s.parse::<String>()
238 .map_err(StreamErrorFor::<Input>::message_format)
239 }),
240 not_sp(),
241 token(' '),
242 not_sp().and_then(|s| {
243 s.parse::<u16>()
244 .map_err(StreamErrorFor::<Input>::message_format)
245 }),
246 token(' '),
247 not_sp().and_then(|s| {
248 s.as_str().try_into().map_err(|_| {
249 StreamErrorFor::<Input>::message_format(format!("invalid protocol: {}", s))
250 })
251 }),
252 token(' '),
253 not_sp().and_then(|s| {
254 s.parse::<u32>()
255 .map_err(StreamErrorFor::<Input>::message_format)
256 }),
257 token(' '),
258 ip_addr(),
259 token(' '),
260 port(),
261 string(" typ "),
262 kind,
263 optional((
264 attempt(string(" raddr ")),
265 ip_addr(),
266 string(" rport "),
267 port(),
268 )),
269 optional((
270 attempt(string(" tcptype ")),
271 not_sp().and_then(|s| {
272 TcpType::from_str(s.as_str()).map_err(StreamErrorFor::<Input>::message_format)
273 }),
274 )),
275 optional((attempt(string(" generation ")), not_sp())),
276 optional((attempt(string(" network-id ")), not_sp())),
277 optional((attempt(string(" ufrag ")), not_sp())),
278 optional((attempt(string(" network-cost ")), not_sp())),
279 )
280 .map(
281 |(
282 _,
283 found,
284 _,
285 comp_id,
286 _,
287 proto,
288 _,
289 prio,
290 _,
291 addr,
292 _,
293 port,
294 _,
295 kind,
296 raddr, // (" raddr ", addr, " rport ", port)
297 tcptype, // (" tcptype ", tcptype)
298 _, // (" generation ", generation)
299 _, // (" network-id ", network_id)
300 ufrag, // (" ufrag ", ufrag)
301 _, // ("network-cost", network_cost)
302 )| {
303 Candidate::from_parts(
304 found,
305 comp_id,
306 proto,
307 prio, // remote candidates calculate prio on their side
308 SocketAddr::from((addr, port)),
309 kind,
310 raddr.map(|(_, addr, _, port)| SocketAddr::from((addr, port))),
311 tcptype.map(|(_, tcptype)| tcptype),
312 ufrag.map(|(_, u)| u),
313 )
314 },
315 )
316}
317 
318/// Parser for a=candidate lines.
319pub(crate) fn candidate_attribute<Input>() -> impl Parser<Input, Output = Candidate>
320where
321 Input: Stream<Token = char>,
322 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
323{
324 (string("a="), candidate(), line_end()).map(|(_, c, _)| c)
325}
326 
327/// Session line with a key we ignore (spec says we should validate them, but meh).
328fn ignored_session_line<Input>() -> impl Parser<Input, Output = ()>
329where
330 Input: Stream<Token = char>,
331 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
332{
333 // TODO: there must be a better way
334 let ignored = choice((
335 token('i'),
336 token('u'),
337 token('e'),
338 token('p'),
339 token('c'),
340 token('z'),
341 token('k'),
342 ));
343 line(ignored, any_value()).map(|_| ())
344}
345 
346// /////////////////////////////////////////////////// Media description
347 
348/// A m= section with attributes, until next m= or EOF
349fn media_parser<Input>() -> impl Parser<Input, Output = MediaLine>
350where
351 Input: Stream<Token = char>,
352 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
353{
354 (
355 media_line(),
356 optional(typed_line('c', any_value())), // c=IN IP4 0.0.0.0
357 optional(bandwidth_line()), // b=AS:2500
358 many::<Vec<_>, _, _>(media_attribute_line()),
359 )
360 .and_then(|((typ, port, proto, pts), _, bw, attrs)| {
361 let m = MediaLine {
362 typ,
363 disabled: port == "0",
364 proto,
365 pts,
366 bw,
367 attrs,
368 };
369 if let Some(err) = m.check_consistent() {
370 warn!("{:?}", err);
371 return Err(StreamErrorFor::<Input>::message_format(err));
372 }
373 Ok(m)
374 })
375}
376 
377/// The m= line
378// dormammu:
379// m=audio 9 UDP/TLS/RTP/SAVPF 111
380// m=video 9 UDP/TLS/RTP/SAVPF 96 97 125 107 100 101
381// m=application 9 DTLS/SCTP 5000
382// chrome:
383// m=audio 64205 UDP/TLS/RTP/SAVPF 111
384// m=video 53151 UDP/TLS/RTP/SAVPF 96 97 125 107 100 101
385// m=application 54055 DTLS/SCTP 5000
386//
387// newer chrome:
388// m=application 9 UDP/DTLS/SCTP webrtc-datachannel
389fn media_line<Input>() -> impl Parser<Input, Output = (MediaType, String, Proto, Vec<Pt>)>
390where
391 Input: Stream<Token = char>,
392 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
393{
394 let media_type = choice((
395 attempt(string("audio").map(|_| MediaType::Audio)),
396 attempt(string("video").map(|_| MediaType::Video)),
397 attempt(string("application").map(|_| MediaType::Application)),
398 not_sp().map(MediaType::Unknown),
399 ));
400 
401 let proto_line = choice((
402 attempt(string("UDP/TLS/RTP/SAVPF").map(|_| Proto::Srtp)),
403 attempt(string("DTLS/SCTP").map(|_| Proto::Sctp)),
404 attempt(string("UDP/DTLS/SCTP").map(|_| Proto::Sctp)),
405 ));
406 
407 let parse_pt = not_sp().and_then(|s| {
408 s.parse::<u8>()
409 .map(Pt::from)
410 .map_err(StreamErrorFor::<Input>::message_format)
411 });
412 
413 // m=<media> <port> <proto> <fmt> ...
414 // <fmt> is: <pt> <pt> <pt> <pt>
415 // where <pt> either matches a=rtpmap:<pt> or a=sctpmap:<pt>
416 typed_line(
417 'm',
418 (
419 media_type, // type: audio, video etc.
420 token(' '),
421 not_sp(), // port: just set to 9 or something
422 token(' '),
423 proto_line, // proto: or
424 token(' '),
425 choice((
426 attempt(sep_by(parse_pt, token(' '))), // <pt> <pt>
427 any_value().map(|_| vec![]),
428 )),
429 ),
430 )
431 .map(|(typ, _, port, _, proto, _, pts)| (typ, port, proto, pts))
432}
433 
434/// a=foo:bar lines belonging before the first m= line
435fn media_attribute_line<Input>() -> impl Parser<Input, Output = MediaAttribute>
436where
437 Input: Stream<Token = char>,
438 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
439{
440 // a=rtcp:9 IN IP4 0.0.0.0
441 let rtcp = attribute_line("rtcp", any_value()).map(MediaAttribute::Rtcp);
442 
443 // a=ice-ufrag:IdNYTNL1fjvjyEzL
444 let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(MediaAttribute::IceUfrag);
445 
446 // a=ice-pwd:4d64pT3T1xfwbZvi9fQKjoPb
447 let ice_pwd = attribute_line("ice-pwd", any_value()).map(MediaAttribute::IcePwd);
448 
449 // a=ice-options:trickle
450 let ice_opt = attribute_line("ice-options", any_value()).map(MediaAttribute::IceOptions);
451 
452 // a=fingerprint:sha-256 45:AD:5C:82:F8:BE:B5:2A:D1:74:A6:16:D0:50:CD:86:9C:97:9D:BD:06:8C:C9:85:C9:CD:AB:2B:A8:56:03:CD
453 // "sha-1" / "sha-224" / "sha-256" /
454 // "sha-384" / "sha-512" /
455 // "md5" / "md2"
456 let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
457 u8::from_str_radix(&x, 16).map_err(StreamErrorFor::<Input>::message_format)
458 });
459 let finger = attribute_line(
460 "fingerprint",
461 (not_sp(), token(' '), sep_by1(hex_byte, token(':'))),
462 )
463 .map(|(hash_func, _, bytes)| MediaAttribute::Fingerprint(Fingerprint { hash_func, bytes }));
464 
465 let setup_val = choice((
466 attempt(string("actpass").map(|_| Setup::ActPass)),
467 attempt(string("active").map(|_| Setup::Active)),
468 attempt(string("passive").map(|_| Setup::Passive)),
469 ));
470 
471 // a=setup:actpass
472 let setup = attribute_line("setup", setup_val).map(MediaAttribute::Setup);
473 
474 // a=mid:0
475 let mid = attribute_line("mid", any_value())
476 .map(|m| Mid::from(m.as_str()))
477 .map(MediaAttribute::Mid);
478 
479 let sctp_port = attribute_line(
480 "sctp-port",
481 not_sp::<Input>().and_then(|s| {
482 s.parse::<u16>()
483 .map_err(StreamErrorFor::<Input>::message_format)
484 }),
485 )
486 .map(MediaAttribute::SctpPort);
487 
488 let max_message_size = attribute_line(
489 "max-message-size",
490 not_sp::<Input>().and_then(|s| {
491 s.parse::<usize>()
492 .map_err(StreamErrorFor::<Input>::message_format)
493 }),
494 )
495 .map(MediaAttribute::MaxMessageSize);
496 
497 // a=sctp-init:<base64-encoded SCTP INIT chunk>
498 // See draft-hancke-tsvwg-snap (ยง4.2: base64 defined in RFC 4566, i.e. standard with padding)
499 //
500 // The base64 value is stored as-is; decoding happens when consumed.
501 let sctp_init = attribute_line("sctp-init", any_value()).map(MediaAttribute::SctpInit);
502 
503 // a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
504 // a=extmap:<value>["/"<direction>] <URI> <extensionattributes>
505 let extmap = attribute_line(
506 "extmap",
507 (
508 many1::<String, _, _>(satisfy(|c| c != '/' && c != ' ')).and_then(|s| {
509 s.parse::<u8>()
510 .map_err(StreamErrorFor::<Input>::message_format)
511 }),
512 optional((token('/'), not_sp().map(|d| Direction::from(&d[..])))),
513 token(' '),
514 not_sp().map(|uri| Extension::from_sdp_uri(&uri)),
515 optional((token(' '), any_value())),
516 ),
517 )
518 .map(|(id, _dir_opt, _, ext, _ext_opt)| MediaAttribute::ExtMap { id, ext });
519 
520 let direction = choice((
521 attempt(attribute_line_flag("recvonly").map(|_| MediaAttribute::RecvOnly)),
522 attempt(attribute_line_flag("sendrecv").map(|_| MediaAttribute::SendRecv)),
523 attempt(attribute_line_flag("sendonly").map(|_| MediaAttribute::SendOnly)),
524 attempt(attribute_line_flag("inactive").map(|_| MediaAttribute::Inactive)),
525 ));
526 
527 // // a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
528 // // a=msid:- f78dde68-7055-4e20-bb37-433803dd1ed1
529 let msid = attribute_line("msid", (not_sp(), token(' '), any_value())).map(
530 |(stream_id, _, track_id)| {
531 MediaAttribute::Msid(Msid {
532 stream_id,
533 track_id,
534 })
535 },
536 );
537 
538 let rtcpmux = attribute_line_flag("rtcp-mux").map(|_| MediaAttribute::RtcpMux);
539 let rtcpmuxonly = attribute_line_flag("rtcp-mux-only").map(|_| MediaAttribute::RtcpMuxOnly);
540 let rtcprsize = attribute_line_flag("rtcp-rsize").map(|_| MediaAttribute::RtcpRsize);
541 
542 // a=candidate
543 let cand = candidate_attribute().map(MediaAttribute::Candidate);
544 
545 // a=end-of-candidates
546 let endof = attribute_line_flag("end-of-candidates").map(|_| MediaAttribute::EndOfCandidates);
547 
548 let pt = || {
549 not_sp().and_then(|s| {
550 let pt = s
551 .parse::<u8>()
552 .map_err(StreamErrorFor::<Input>::message_format)?;
553 // RTP payload type is 7-bit; reject out-of-range values rather than panicking later
554 // when PTs are claimed against a 128-entry table.
555 if pt > 127 {
556 return Err(StreamErrorFor::<Input>::message_format(format!(
557 "payload type out of range (0-127): {pt}"
558 )));
559 }
560 Ok(Pt::from(pt))
561 })
562 };
563 
564 // a=rtpmap:111 opus/48000/2
565 let rtpmap = attribute_line(
566 "rtpmap",
567 (
568 pt(),
569 token(' '),
570 many1::<String, _, _>(satisfy(|c| c != '/' && c != '\r' && c != '\n')),
571 token('/'),
572 many1::<String, _, _>(satisfy(|c| c != '/' && c != '\r' && c != '\n')).and_then(|s| {
573 s.parse::<Frequency>()
574 .map_err(StreamErrorFor::<Input>::message_format)
575 }),
576 optional((
577 token('/'),
578 any_value().and_then(|s| {
579 s.parse::<u8>()
580 .map_err(StreamErrorFor::<Input>::message_format)
581 }),
582 )), // only audio has the last /2 (channels)
583 ),
584 )
585 .map(|(pt, _, codec, _, clock_rate, opt_channels)| {
586 let channels = opt_channels.map(|(_, e)| e);
587 MediaAttribute::RtpMap {
588 pt,
589 value: RtpMap {
590 codec: codec.as_str().into(),
591 clock_rate,
592 channels,
593 },
594 }
595 });
596 
597 // a=rtcp-fb:111 transport-cc
598 // a=rtcp-fb:111 ccm fir
599 // a=rtcp-fb:111 nack
600 // a=rtcp-fb:111 nack pli
601 let rtcp_fb = attribute_line("rtcp-fb", (pt(), token(' '), any_value()))
602 .map(|(pt, _, value)| MediaAttribute::RtcpFb { pt, value });
603 
604 let fmtp_param = sep_by1(key_val(), token(';')).map(FormatParam::parse_pairs);
605 
606 // a=fmtp:111 minptime=10; useinbandfec=1
607 // a=fmtp:111 minptime=10;useinbandfec=1
608 let fmtp1 = attribute_line("fmtp", (pt(), token(' '), fmtp_param))
609 .map(|(pt, _, values)| MediaAttribute::Fmtp { pt, values });
610 
611 // a=fmtp:101 0-15
612 let fmtp2 = attribute_line("fmtp", (pt(), token(' '), not_sp())).map(|(pt, _, _value)| {
613 MediaAttribute::Fmtp {
614 pt,
615 values: vec![FormatParam::Unknown],
616 }
617 });
618 
619 let fmtp = choice((attempt(fmtp1), attempt(fmtp2)));
620 
621 // a=rid:<rid-id> <direction> [pt=<fmt-list>;]<restriction>=<value>
622 let rid = attribute_line(
623 "rid",
624 (
625 name().map(RestrictionId::new_active),
626 token(' '),
627 choice((string("send"), string("recv"))),
628 optional((
629 token(' '),
630 optional((
631 string("pt="),
632 sep_by1::<Vec<Pt>, _, _, _>(pt(), token(',')),
633 // TODO this is not really optional when there is
634 // a restriction part. It means we are incorrectly
635 // allowing this: a=rid:foo send pt=111max-br=64000
636 optional(token(';')),
637 )),
638 sep_by::<Vec<(String, String)>, _, _, _>(key_val(), token(';')),
639 )),
640 ),
641 )
642 .map(|(id, _, direction, x)| {
643 let mut pt = vec![];
644 let mut restriction = vec![];
645 if let Some((_, ps, rs)) = x {
646 if let Some((_, ps, _)) = ps {
647 pt = ps;
648 }
649 restriction = rs;
650 }
651 
652 MediaAttribute::Rid {
653 id,
654 direction,
655 pt,
656 restriction,
657 }
658 });
659 
660 let simul1 = |direction: &'static str| {
661 (
662 string(direction),
663 token(' '),
664 sep_by1::<Vec<Vec<(String, bool)>>, _, _, _>(
665 sep_by1(
666 optional(token('~'))
667 .and(name())
668 .map(|x| (x.1, x.0.is_none())),
669 token(','),
670 ),
671 token(';'),
672 ),
673 )
674 };
675 
676 // Parser guarantee that it's send => recv or recv => send.
677 let simul2 = choice((
678 (simul1("send"), optional((token(' '), simul1("recv")))),
679 (simul1("recv"), optional((token(' '), simul1("send")))),
680 ));
681 
682 // a=simulcast:<send/recv> <alt A>;<alt B>,<or C> <send/recv> [same]
683 let simulcast = attribute_line("simulcast", simul2).map(|(s1, maybe_s2)| {
684 let mut send = SimulcastGroups(vec![]);
685 let mut recv = SimulcastGroups(vec![]);
686 
687 fn to_simul(to: &mut SimulcastGroups, groups: Vec<Vec<(String, bool)>>) {
688 for group in groups {
689 // TODO: Properly support rid alternatives, for now we ignore any alternatives
690 // provided and use the first rid.
691 let first = group.into_iter().next();
692 
693 if let Some(rid) = first.map(|(rid, active)| RestrictionId::new(rid, active)) {
694 to.0.push(SimulcastLayer {
695 restriction_id: rid,
696 attributes: None,
697 });
698 }
699 }
700 }
701 
702 {
703 let to = if s1.0 == "send" { &mut send } else { &mut recv };
704 to_simul(to, s1.2);
705 }
706 
707 if let Some(s2) = maybe_s2 {
708 let s2 = s2.1;
709 let to = if s2.0 == "send" { &mut send } else { &mut recv };
710 to_simul(to, s2.2);
711 }
712 
713 MediaAttribute::Simulcast(Simulcast {
714 send,
715 recv,
716 is_munged: false,
717 })
718 });
719 
720 // a=ssrc-group:FID 1111 2222
721 let ssrc_group = attribute_line(
722 "ssrc-group",
723 (
724 not_sp(),
725 token(' '),
726 sep_by1(
727 not_sp().and_then(|s| {
728 s.parse::<u32>()
729 .map(Ssrc::from)
730 .map_err(StreamErrorFor::<Input>::message_format)
731 }),
732 token(' '),
733 ),
734 ),
735 )
736 .map(|(semantics, _, ssrcs)| MediaAttribute::SsrcGroup { semantics, ssrcs });
737 
738 // a=ssrc:3948621874 cname:xeXs3aE9AOBn00yJ
739 // a=ssrc:3948621874 msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
740 // a=ssrc:3948621874 mslabel:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK
741 // a=ssrc:3948621874 label:f78dde68-7055-4e20-bb37-433803dd1ed1
742 let ssrc = attribute_line(
743 "ssrc",
744 (
745 not_sp().and_then(|s| {
746 s.parse::<u32>()
747 .map(Ssrc::from)
748 .map_err(StreamErrorFor::<Input>::message_format)
749 }),
750 token(' '),
751 many1::<String, _, _>(satisfy(|c| c != ':')),
752 token(':'),
753 any_value(),
754 ),
755 )
756 .map(|(ssrc, _, attr, _, value)| MediaAttribute::Ssrc { ssrc, attr, value });
757 
758 let unused = typed_line('a', any_value()).map(MediaAttribute::Unused);
759 
760 // Split into two choice() calls because combine's choice macro supports at most 25 parsers.
761 choice((
762 attempt(choice((
763 attempt(ice_ufrag),
764 attempt(ice_pwd),
765 attempt(ice_opt),
766 attempt(finger),
767 attempt(setup),
768 attempt(mid),
769 attempt(sctp_port),
770 attempt(max_message_size),
771 attempt(sctp_init),
772 attempt(extmap),
773 attempt(direction),
774 attempt(msid),
775 attempt(rtcp),
776 ))),
777 attempt(choice((
778 attempt(rtcpmux),
779 attempt(rtcpmuxonly),
780 attempt(rtcprsize),
781 attempt(cand),
782 attempt(endof),
783 attempt(rtpmap),
784 attempt(rtcp_fb),
785 attempt(fmtp),
786 attempt(rid),
787 attempt(simulcast),
788 attempt(ssrc_group),
789 attempt(ssrc),
790 ))),
791 unused,
792 ))
793}
794 
795// /////////////////////////////////////////////////// Generic things below
796 
797/// A specific line
798fn typed_line<Input, Pval, Out>(expected: char, val: Pval) -> impl Parser<Input, Output = Out>
799where
800 Input: Stream<Token = char>,
801 Pval: Parser<Input, Output = Out>,
802 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
803{
804 line(token(expected), val)
805}
806 
807/// A line with some parser for value and parser for type.
808fn line<Input, Ptyp, Pval, Out>(typ: Ptyp, val: Pval) -> impl Parser<Input, Output = Out>
809where
810 Ptyp: Parser<Input, Output = char>,
811 Pval: Parser<Input, Output = Out>,
812 Input: Stream<Token = char>,
813 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
814{
815 attempt((typ, token('='), val, line_end()))
816 .map(|(_, _, value, _)| value)
817 .message("sdp line")
818}
819 
820/// alphanumeric name.
821fn name<Input>() -> impl Parser<Input, Output = String>
822where
823 Input: Stream<Token = char>,
824 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
825{
826 many1(satisfy(|c: char| c.is_alphanumeric()))
827}
828 
829/// Not SP, \r or \n
830fn not_sp<Input>() -> impl Parser<Input, Output = String>
831where
832 Input: Stream<Token = char>,
833 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
834{
835 many1(satisfy(|c| c != ' ' && c != '\r' && c != '\n'))
836}
837 
838/// Any value that isn't \r or \n.
839fn any_value<Input>() -> impl Parser<Input, Output = String>
840where
841 Input: Stream<Token = char>,
842 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
843{
844 many1(satisfy(|c| c != '\r' && c != '\n'))
845}
846 
847/// We discovered a stray \n in safari SDP. This line end handles \r\n, \n or EOF.
848fn line_end<Input>() -> impl Parser<Input, Output = ()>
849where
850 Input: Stream<Token = char>,
851 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
852{
853 choice((crlf().map(|_| ()), newline().map(|_| ()), eof()))
854}
855 
856// minptime=10
857fn key_val<Input>() -> impl Parser<Input, Output = (String, String)>
858where
859 Input: Stream<Token = char>,
860 Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
861{
862 (
863 optional(spaces()),
864 many1(satisfy(|c| c != '=' && c != ' ' && c != '\r' && c != '\n')),
865 token('='),
866 many1(satisfy(|c| c != ';' && c != ' ' && c != '\r' && c != '\n')),
867 )
868 .map(|(_, key, _, val)| (key, val))
869}
870 
871#[cfg(test)]
872mod test {
873 use super::*;
874 use crate::format::Codec;
875 use crate::io::Protocol;
876 
877 #[test]
878 fn line_a() {
879 assert_eq!(
880 line(letter(), any_value()).parse("a=mid:0"),
881 Ok(("mid:0".to_string(), ""))
882 )
883 }
884 
885 #[test]
886 fn line_end_crlf() {
887 assert_eq!(line_end().parse("\r\n"), Ok(((), "")));
888 }
889 
890 #[test]
891 fn line_end_lf() {
892 assert_eq!(line_end().parse("\n"), Ok(((), "")));
893 }
894 
895 #[test]
896 fn line_end_eof() {
897 assert_eq!(line_end().parse(""), Ok(((), "")));
898 }
899 
900 #[test]
901 fn typed_line_v() {
902 assert_eq!(typed_line('v', token('0')).parse("v=0"), Ok(('0', "")))
903 }
904 
905 #[test]
906 fn attribute_line_flag_foo() {
907 assert_eq!(attribute_line_flag("foo").parse("a=foo"), Ok(((), "")))
908 }
909 
910 #[test]
911 fn attribute_line_foo_bar() {
912 assert_eq!(
913 attribute_line("foo", any_value()).parse("a=foo:bar"),
914 Ok(("bar".to_string(), ""))
915 )
916 }
917 
918 #[test]
919 fn session_attribute_line_simple() {
920 let x = session_attribute_line().parse("a=ice-lite");
921 assert_eq!(x, Ok((SessionAttribute::IceLite, "")));
922 }
923 
924 #[test]
925 fn session_attribute_line_finger() {
926 let x = session_attribute_line().parse("a=fingerprint:sha-256 45:AD:5C:82:F8:BE");
927 assert_eq!(
928 x,
929 Ok((
930 SessionAttribute::Fingerprint(Fingerprint {
931 hash_func: "sha-256".to_string(),
932 bytes: vec![69, 173, 92, 130, 248, 190],
933 }),
934 ""
935 ))
936 );
937 }
938 
939 #[test]
940 fn media_attribute_line_rid_simple() {
941 let x = media_attribute_line().parse("a=rid:lo send").unwrap();
942 assert_eq!("a=rid:lo send\r\n", x.0.to_string());
943 }
944 
945 #[test]
946 fn media_attribute_line_rid_pt() {
947 let x = media_attribute_line()
948 .parse("a=rid:lo send pt=99,100")
949 .unwrap();
950 assert_eq!("a=rid:lo send pt=99,100\r\n", x.0.to_string());
951 }
952 
953 #[test]
954 fn media_attribute_line_rid_restr() {
955 let x = media_attribute_line()
956 .parse("a=rid:lo send max-br=64000;max-height=360")
957 .unwrap();
958 assert_eq!(
959 "a=rid:lo send max-br=64000;max-height=360\r\n",
960 x.0.to_string()
961 );
962 }
963 
964 #[test]
965 fn media_attribute_line_rid_pt_restr() {
966 let x = media_attribute_line()
967 .parse("a=rid:lo send pt=99,100;max-br=64000;max-height=360")
968 .unwrap();
969 assert_eq!(
970 "a=rid:lo send pt=99,100;max-br=64000;max-height=360\r\n",
971 x.0.to_string()
972 );
973 }
974 
975 #[test]
976 fn media_attribute_line_simulcast() {
977 let x = media_attribute_line()
978 .parse("a=simulcast:send 3;4")
979 .unwrap();
980 assert_eq!("a=simulcast:send 3;4\r\n", x.0.to_string());
981 }
982 
983 #[test]
984 fn media_attribute_line_simulcast_alt() {
985 let x = media_attribute_line()
986 .parse("a=simulcast:send 2,3;4")
987 .unwrap();
988 assert_eq!("a=simulcast:send 2;4\r\n", x.0.to_string());
989 }
990 
991 #[test]
992 fn media_attribute_line_simulcast_send_recv() {
993 let x = media_attribute_line()
994 .parse("a=simulcast:send 2;3 recv 4")
995 .unwrap();
996 assert_eq!("a=simulcast:send 2;3 recv 4\r\n", x.0.to_string());
997 }
998 
999 #[test]
1000 fn media_attribute_line_simulcast_recv_send() {
1001 let x = media_attribute_line()
1002 .parse("a=simulcast:recv 2;3 send 4")
1003 .unwrap();
1004 assert_eq!("a=simulcast:send 4 recv 2;3\r\n", x.0.to_string());
1005 }
1006 
1007 #[test]
1008 fn media_line_simple() {
1009 let m = media_line().parse("m=audio 9 UDP/TLS/RTP/SAVPF 10\r\n");
1010 assert_eq!(
1011 m,
1012 Ok((
1013 (MediaType::Audio, "9".into(), Proto::Srtp, vec![10.into()],),
1014 ""
1015 ))
1016 );
1017 }
1018 
1019 #[test]
1020 fn rtpmap_payload_type_over_127_is_rejected() {
1021 // RTP payload type is 7-bit. An out-of-range PT in a=rtpmap must not be accepted as an
1022 // RtpMap; otherwise it becomes a PayloadParams that panics when claimed against a
1023 // 128-entry table. It instead falls through to Unused and is ignored (the m-line then
1024 // fails its own "missing a=rtpmap" check). 127 is the largest valid PT.
1025 let bad = media_attribute_line().parse("a=rtpmap:200 opus/48000/2");
1026 assert!(
1027 !matches!(bad, Ok((MediaAttribute::RtpMap { .. }, _))),
1028 "out-of-range rtpmap PT must not parse as RtpMap: {bad:?}"
1029 );
1030 assert!(matches!(
1031 media_attribute_line().parse("a=rtpmap:127 opus/48000/2"),
1032 Ok((MediaAttribute::RtpMap { .. }, _))
1033 ));
1034 }
1035 
1036 #[test]
1037 fn session_parser_simple() {
1038 let sdp = "v=0\n\
1039 o=- 6564425948916445306 2 IN IP4 127.0.0.1\n\
1040 s=-\n\
1041 t=0 0\n\
1042 a=group:BUNDLE 0\n\
1043 a=msid-semantic: WMS\n\
1044 m=application 9 DTLS/SCTP 5000\n\
1045 ";
1046 assert_eq!(
1047 session_parser().parse(sdp),
1048 Ok((
1049 Session {
1050 id: 6_564_425_948_916_445_306.into(),
1051 bw: None,
1052 attrs: vec![
1053 SessionAttribute::Group {
1054 typ: "BUNDLE".into(),
1055 mids: vec!["0".into()],
1056 },
1057 SessionAttribute::Unused("msid-semantic: WMS".into())
1058 ],
1059 },
1060 "m=application 9 DTLS/SCTP 5000\n"
1061 ))
1062 );
1063 }
1064 
1065 #[test]
1066 fn session_parser_named_session() {
1067 // A session name other than "-".
1068 let sdp = "v=0\r\n\
1069 o=publisher 4489045141692799359 2 IN IP4 127.0.0.1\r\n\
1070 s=NamedPublishSession\r\n\
1071 t=0 0\r\n\
1072 a=group:BUNDLE 0 1\r\n\
1073 a=extmap-allow-mixed\r\n\
1074 a=msid-semantic: WMS\r\n";
1075 assert_eq!(
1076 session_parser().parse(sdp),
1077 Ok((
1078 Session {
1079 id: 4_489_045_141_692_799_359.into(),
1080 bw: None,
1081 attrs: vec![
1082 SessionAttribute::Group {
1083 typ: "BUNDLE".into(),
1084 mids: vec!["0".into(), "1".into()],
1085 },
1086 SessionAttribute::Unused("extmap-allow-mixed".into()),
1087 SessionAttribute::Unused("msid-semantic: WMS".into()),
1088 ],
1089 },
1090 ""
1091 ))
1092 );
1093 }
1094 
1095 #[test]
1096 fn session_parser_space_session_name() {
1097 // "If a session has no meaningful name, the value "s= " SHOULD be used
1098 // (i.e., a single space as the session name)." (RFC 8866 section 5.3)
1099 let sdp = "v=0\n\
1100 o=- 6564425948916445306 2 IN IP4 127.0.0.1\n\
1101 s= \n\
1102 t=0 0\n\
1103 ";
1104 assert_eq!(
1105 session_parser().parse(sdp),
1106 Ok((
1107 Session {
1108 id: 6_564_425_948_916_445_306.into(),
1109 bw: None,
1110 attrs: vec![],
1111 },
1112 ""
1113 ))
1114 );
1115 }
1116 
1117 #[test]
1118 fn parse_sdp_firefox() {
1119 let sdp = "v=0\r\n\
1120 o=mozilla...THIS_IS_SDPARTA-83.0 7052848360639826063 0 IN IP4 0.0.0.0\r\n\
1121 s=-\r\n\
1122 t=0 0\r\n\
1123 a=fingerprint:sha-256 37:FC:96:B5:73:98:E6:F9:C5:0B:D9:EE:B1:F8:D0:01:07:2E:75:E8:6C:A4:32:A7:DC:63:99:5E:68:5C:BF:FB\r\n\
1124 a=ice-options:trickle\r\n\
1125 a=msid-semantic:WMS *\r\n";
1126 
1127 assert_eq!(
1128 sdp_parser().parse(sdp),
1129 Ok((
1130 Sdp {
1131 session: Session {
1132 id: 7052848360639826063.into(),
1133 bw: None,
1134 attrs: vec![
1135 SessionAttribute::Fingerprint(Fingerprint {
1136 hash_func: "sha-256".into(),
1137 bytes: vec![
1138 0x37, 0xFC, 0x96, 0xB5, 0x73, 0x98, 0xE6, 0xF9, 0xC5, 0x0B,
1139 0xD9, 0xEE, 0xB1, 0xF8, 0xD0, 0x01, 0x07, 0x2E, 0x75, 0xE8,
1140 0x6C, 0xA4, 0x32, 0xA7, 0xDC, 0x63, 0x99, 0x5E, 0x68, 0x5C,
1141 0xBF, 0xFB
1142 ]
1143 }),
1144 SessionAttribute::IceOptions("trickle".to_string()),
1145 SessionAttribute::MsidSemantic {
1146 semantic: "WMS".into(),
1147 stream_ids: vec!["*".into()]
1148 },
1149 ]
1150 },
1151 media_lines: vec![]
1152 },
1153 ""
1154 ))
1155 );
1156 }
1157 
1158 #[test]
1159 fn parse_offer_sdp_firefox() {
1160 let sdp = "v=0\r\n\
1161 o=mozilla...THIS_IS_SDPARTA-84.0 9033133899747520364 1 IN IP4 0.0.0.0\r\n\
1162 s=-\r\n\
1163 t=0 0\r\n\
1164 a=fingerprint:sha-256 AE:DC:49:AE:CA:55:35:CB:4E:FA:FE:70:99:30:C0:14:C3:B8:06:80:1F:A9:DA:9A:7C:FB:B7:20:AB:83:60:45\r\n\
1165 a=group:BUNDLE 0\r\n\
1166 a=ice-options:trickle\r\n\
1167 a=msid-semantic:WMS *\r\n\
1168 m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n\
1169 c=IN IP4 0.0.0.0\r\n\
1170 a=sendonly\r\n\
1171 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
1172 a=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\n\
1173 a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
1174 a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n\
1175 a=fmtp:101 0-15\r\n\
1176 a=ice-pwd:cd25258044061ec2ecc73378eb3dc6a3\r\n\
1177 a=ice-ufrag:c1e284ad\r\n\
1178 a=mid:0\r\n\
1179 a=msid:- {5c7f12e5-b4bd-7142-9a06-2885a2d1cb66}\r\n\
1180 a=rtcp-mux\r\n\
1181 a=rtpmap:109 opus/48000/2\r\n\
1182 a=rtpmap:9 G722/8000/1\r\n\
1183 a=rtpmap:0 PCMU/8000\r\n\
1184 a=rtpmap:8 PCMA/8000\r\n\
1185 a=rtpmap:101 telephone-event/8000/1\r\n\
1186 a=setup:actpass\r\n\
1187 a=ssrc:1481683531 cname:{326ec0d2-d1ae-974c-b1ad-aea85cdfa0ad}\r\n";
1188 
1189 let parsed = sdp_parser().parse(sdp);
1190 
1191 assert!(parsed.is_ok());
1192 }
1193 
1194 #[test]
1195 fn parse_offer_sdp_chrome() {
1196 let sdp = "v=0\r\n\
1197 o=- 5058682828002148772 3 IN IP4 127.0.0.1\r\n\
1198 s=-\r\n\
1199 t=0 0\r\n\
1200 a=group:BUNDLE 0\r\n\
1201 a=msid-semantic: WMS 5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
1202 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n\
1203 c=IN IP4 0.0.0.0\r\n\
1204 a=rtcp:9 IN IP4 0.0.0.0\r\n\
1205 a=ice-ufrag:S5hk\r\n\
1206 a=ice-pwd:0zV/Yu3y8aDzbHgqWhnVQhqP\r\n\
1207 a=ice-options:trickle\r\n\
1208 a=fingerprint:sha-256 8C:64:ED:03:76:D0:3D:B4:88:08:91:64:08:80:A8:C6:5A:BF:8B:4E:38:27:96:CA:08:49:25:73:46:60:20:DC\r\n\
1209 a=setup:actpass\r\n\
1210 a=mid:0\r\n\
1211 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
1212 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
1213 a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
1214 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
1215 a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
1216 a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
1217 a=sendrecv\r\n\
1218 a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1219 a=rtcp-mux\r\n\
1220 a=rtpmap:111 opus/48000/2\r\n\
1221 a=rtcp-fb:111 transport-cc\r\n\
1222 a=fmtp:111 minptime=10;useinbandfec=1\r\n\
1223 a=rtpmap:103 ISAC/16000\r\n\
1224 a=rtpmap:104 ISAC/32000\r\n\
1225 a=rtpmap:9 G722/8000\r\n\
1226 a=rtpmap:0 PCMU/8000\r\n\
1227 a=rtpmap:8 PCMA/8000\r\n\
1228 a=rtpmap:106 CN/32000\r\n\
1229 a=rtpmap:105 CN/16000\r\n\
1230 a=rtpmap:13 CN/8000\r\n\
1231 a=rtpmap:110 telephone-event/48000\r\n\
1232 a=rtpmap:112 telephone-event/32000\r\n\
1233 a=rtpmap:113 telephone-event/16000\r\n\
1234 a=rtpmap:126 telephone-event/8000\r\n\
1235 a=ssrc:3948621874 cname:xeXs3aE9AOBn00yJ\r\n\
1236 a=ssrc:3948621874 msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1237 a=ssrc:3948621874 mslabel:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
1238 a=ssrc:3948621874 label:f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1239 ";
1240 
1241 let parsed = sdp_parser().parse(sdp);
1242 
1243 assert!(parsed.is_ok());
1244 }
1245 
1246 #[test]
1247 fn parse_offer_sdp_ffmpeg_whip() {
1248 // ffmpeg -re \
1249 // -f lavfi -i "testsrc=size=640x480:rate=30" \
1250 // -f lavfi -i "sine=frequency=440:sample_rate=48000" \
1251 // -c:v libx264 -profile:v baseline -pix_fmt yuv420p -g 60 -tune zerolatency \
1252 // -c:a libopus -ar 48000 -ac 2 \
1253 // -f whip "http://127.0.0.1:8080/whip"
1254 let sdp = "v=0\r\n\
1255 o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1\r\n\
1256 s=FFmpegPublishSession\r\n\
1257 t=0 0\r\n\
1258 a=group:BUNDLE 0 1\r\n\
1259 a=extmap-allow-mixed\r\n\
1260 a=msid-semantic: WMS\r\n\
1261 m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
1262 c=IN IP4 0.0.0.0\r\n\
1263 a=ice-ufrag:08a9c3f4\r\n\
1264 a=ice-pwd:a202d23be47d1decaa6f67d04d4e6c16\r\n\
1265 a=fingerprint:sha-256 21:69:7C:46:D3:DC:4D:D3:66:61:5C:C5:6A:0B:F7:36\
1266 :BD:91:A9:2A:FF:BC:1A:59:D9:38:38:C8:E2:A4:8D:AF\r\n\
1267 a=setup:passive\r\n\
1268 a=mid:0\r\n\
1269 a=sendonly\r\n\
1270 a=msid:FFmpeg audio\r\n\
1271 a=rtcp-mux\r\n\
1272 a=rtpmap:111 opus/48000/2\r\n\
1273 a=ssrc:3166090378 cname:FFmpeg\r\n\
1274 a=ssrc:3166090378 msid:FFmpeg audio\r\n\
1275 m=video 9 UDP/TLS/RTP/SAVPF 106 105\r\n\
1276 c=IN IP4 0.0.0.0\r\n\
1277 a=ice-ufrag:08a9c3f4\r\n\
1278 a=ice-pwd:a202d23be47d1decaa6f67d04d4e6c16\r\n\
1279 a=fingerprint:sha-256 21:69:7C:46:D3:DC:4D:D3:66:61:5C:C5:6A:0B:F7:36\
1280 :BD:91:A9:2A:FF:BC:1A:59:D9:38:38:C8:E2:A4:8D:AF\r\n\
1281 a=setup:passive\r\n\
1282 a=mid:1\r\n\
1283 a=sendonly\r\n\
1284 a=msid:FFmpeg video\r\n\
1285 a=rtcp-mux\r\n\
1286 a=rtcp-rsize\r\n\
1287 a=rtpmap:106 H264/90000\r\n\
1288 a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001e\r\n\
1289 a=rtcp-fb:106 nack\r\n\
1290 a=rtpmap:105 rtx/90000\r\n\
1291 a=fmtp:105 apt=106\r\n\
1292 a=ssrc-group:FID 3166090379 3166090380\r\n\
1293 a=ssrc:3166090379 cname:FFmpeg\r\n\
1294 a=ssrc:3166090379 msid:FFmpeg video\r\n\
1295 ";
1296 
1297 let (sdp, _) = sdp_parser().parse(sdp).unwrap();
1298 
1299 assert_eq!(sdp.media_lines.len(), 2);
1300 assert_eq!(sdp.media_lines[0].mid().to_string(), "0");
1301 assert_eq!(sdp.media_lines[1].mid().to_string(), "1");
1302 }
1303 
1304 #[test]
1305 fn parse_safari_data_channel() {
1306 let sdp = "v=0\r\n\
1307 o=- 4611516372927609806 2 IN IP4 127.0.0.1\r\n\
1308 s=-\r\n\
1309 t=0 0\r\n\
1310 a=group:BUNDLE 0\r\n\
1311 a=extmap-allow-mixed\r\n\
1312 a=msid-semantic: WMS\r\n\
1313 m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n\
1314 c=IN IP4 0.0.0.0\r\n\
1315 a=ice-ufrag:HhS+\r\n\
1316 a=ice-pwd:FhYTGhlAtKCe6KFIX8b+AThW\r\n\
1317 a=ice-options:trickle\r\n\
1318 a=fingerprint:sha-256 B4:12:1C:7C:7D:ED:F1:FA:61:07:57:9C:29:BE:58:E3:BC:41:E7:13:8E:7D:D3:9D:1F:94:6E:A5:23:46:94:23\r\n\
1319 a=setup:actpass\r\n\
1320 a=mid:0\r\n\
1321 a=sctp-port:5000\r\n\
1322 a=max-message-size:262144\r\n\
1323 ";
1324 
1325 let (sdp, _) = sdp_parser().parse(sdp).unwrap();
1326 
1327 assert!(!sdp.media_lines.is_empty());
1328 assert_eq!(sdp.media_lines[0].mid().to_string(), "0");
1329 }
1330 
1331 #[test]
1332 fn parse_minimal() {
1333 let sdp = "v=0\r\n\
1334 o=- 5058682828002148772 3 IN IP4 127.0.0.1\r\n\
1335 s=-\r\n\
1336 t=0 0\r\n\
1337 m=audio 9 UDP/TLS/RTP/SAVPF\r\n\
1338 c=IN IP4 0.0.0.0\r\n\
1339 a=rtcp:9 IN IP4 0.0.0.0\r\n\
1340 a=setup:actpass\r\n\
1341 a=inactive\r\n\
1342 a=mid:0\r\n\
1343 ";
1344 
1345 let parsed = sdp_parser().parse(sdp);
1346 assert!(parsed.is_ok());
1347 }
1348 
1349 #[test]
1350 fn bare_static_audio_pts_are_resolved() {
1351 let sdp = "v=0\r\n\
1352 o=- 1 1 IN IP4 127.0.0.1\r\n\
1353 s=-\r\n\
1354 t=0 0\r\n\
1355 a=group:BUNDLE 0\r\n\
1356 m=audio 9 UDP/TLS/RTP/SAVPF 0 8 9 13\r\n\
1357 c=IN IP4 0.0.0.0\r\n\
1358 a=setup:actpass\r\n\
1359 a=sendrecv\r\n\
1360 a=mid:0\r\n\
1361 ";
1362 
1363 let (sdp, _) = sdp_parser()
1364 .parse(sdp)
1365 .expect("static PTs must not require an a=rtpmap line");
1366 sdp.assert_consistency()
1367 .expect("bare static PTs are consistent");
1368 let params = sdp.media_lines[0].rtp_params();
1369 
1370 let definitions: Vec<_> = params
1371 .iter()
1372 .map(|p| (*p.pt(), p.spec().codec, p.spec().clock_rate))
1373 .collect();
1374 assert_eq!(
1375 definitions,
1376 vec![
1377 (0, Codec::PCMU, Frequency::EIGHT_KHZ),
1378 (8, Codec::PCMA, Frequency::EIGHT_KHZ),
1379 (9, Codec::G722, Frequency::SIXTEEN_KHZ),
1380 (13, Codec::CN, Frequency::EIGHT_KHZ),
1381 ]
1382 );
1383 }
1384 
1385 #[test]
1386 fn parse_candidates() {
1387 let a = "a=candidate:1 1 udp 1845494015 198.51.100.100 11100 typ srflx raddr 203.0.113.100 rport 10100 ufrag abc\r\n";
1388 
1389 let (c, _) = candidate_attribute().parse(a).unwrap();
1390 assert_eq!(c.ufrag(), Some("abc"));
1391 
1392 let a = "a=candidate:1 1 udp 1845494015 198.51.100.100 11100 typ host ufrag abc\r\n";
1393 let (c, _) = candidate_attribute().parse(a).unwrap();
1394 assert_eq!(c.ufrag(), Some("abc"));
1395 
1396 let a = "a=candidate:3684617590 1 udp 2122260223 10.217.229.219 50028 typ host generation 0 network-id 1 network-cost 900";
1397 let (c, _) = candidate_attribute().parse(a).unwrap();
1398 assert_eq!(c.addr(), "10.217.229.219:50028".parse().unwrap());
1399 
1400 let a = "a=candidate:387183333 1 udp 1686052607 113.185.55.72 31267 typ srflx raddr 10.217.229.219 rport 50028 generation 0 network-id 1 network-cost 900";
1401 let (c, _) = candidate_attribute().parse(a).unwrap();
1402 assert_eq!(c.addr(), "113.185.55.72:31267".parse().unwrap());
1403 
1404 let a = "a=candidate:2501718406 1 tcp 1518280447 10.217.229.219 9 typ host tcptype active generation 0 network-id 1 network-cost 900";
1405 let (c, _) = candidate_attribute().parse(a).unwrap();
1406 assert_eq!(c.addr(), "10.217.229.219:9".parse().unwrap());
1407 
1408 let a = "a=candidate:387183333 1 udp 1686052607 113.185.55.72 41775 typ srflx raddr 10.217.229.219 rport 50028 generation 0 network-id 1 network-cost 900";
1409 let (c, _) = candidate_attribute().parse(a).unwrap();
1410 assert_eq!(c.addr(), "113.185.55.72:41775".parse().unwrap());
1411 
1412 let a = "a=candidate:3936339338 1 udp 2122265343 fd00:f8aa:3ff5:5914:90bf:3c5:3378:9d4b 59125 \
1413 typ host generation 0 network-id 4 network-cost 50";
1414 let (c, _) = candidate_attribute().parse(a).unwrap();
1415 assert_eq!(
1416 c.addr(),
1417 "[fd00:f8aa:3ff5:5914:90bf:3c5:3378:9d4b]:59125"
1418 .parse()
1419 .unwrap()
1420 );
1421 assert_eq!(c.proto(), Protocol::Udp);
1422 
1423 let a = "a=candidate:2113932030 1 tcp 2113932030 142.250.82.253 19305 \
1424 typ host tcptype passive generation 0";
1425 let (c, _) = candidate_attribute().parse(a).unwrap();
1426 assert_eq!(c.addr(), "142.250.82.253:19305".parse().unwrap());
1427 assert_eq!(c.proto(), Protocol::Tcp);
1428 assert_eq!(c.tcptype(), Some(TcpType::Passive));
1429 
1430 let a =
1431 "a=candidate:2113932029 1 ssltcp 2113932029 142.250.82.252 443 typ host generation 0";
1432 let (c, _) = candidate_attribute().parse(a).unwrap();
1433 assert_eq!(c.addr(), "142.250.82.252:443".parse().unwrap());
1434 assert_eq!(c.proto(), Protocol::SslTcp);
1435 assert_eq!(c.tcptype(), None);
1436 }
1437 
1438 #[test]
1439 fn parse_firefox_missing_setup_on_mid1() {
1440 let sdp = "v=0\r\n\
1441 o=mozilla...THIS_IS_SDPARTA-99.0 7710052215259647220 2 IN IP4 0.0.0.0\r\n\
1442 s=-\r\n\
1443 t=0 0\r\n\
1444 a=fingerprint:sha-256 A6:64:23:37:94:7E:4B:40:F6:62:86:8C:DD:09:D5:08:7E:D4:0E:68:58:93:45:EC:99:F2:91:F7:19:72:E7:BB\r\n\
1445 a=group:BUNDLE 0 hxI i1X mxk B3D kNI nbB xIZ bKm Hkn\r\n\
1446 a=ice-options:trickle\r\n\
1447 a=msid-semantic:WMS *\r\n\
1448 m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n\
1449 c=IN IP4 0.0.0.0\r\n\
1450 a=inactive\r\n\
1451 a=mid:1\r\n\
1452 a=rtpmap:0 PCMU/8000\r\n\
1453 ";
1454 
1455 let (sdp, _) = sdp_parser().parse(sdp).unwrap();
1456 
1457 // Firefox 'a=setup' attribute can be missing
1458 // the "a=setup:" sdp attribute (which should be mandatory)
1459 // - https://www.rfc-editor.org/rfc/rfc5763#section-5
1460 // - https://www.rfc-editor.org/rfc/rfc4145
1461 
1462 assert!(sdp.media_lines[0].setup().is_none()); // must not crash
1463 }
1464 
1465 #[test]
1466 fn parse_no_media_c_line() {
1467 let sdp = "v=0\r\n\
1468 o=- 0 0 IN IP4 172.17.0.1\r\n\
1469 s=-\r\n\
1470 c=IN IP4 172.17.0.1\r\n\
1471 t=0 0\r\n\
1472 m=application 9999 UDP/DTLS/SCTP webrtc-datachannel\r\n\
1473 a=mid:0\r\n\
1474 a=ice-options:ice2\r\n\
1475 a=ice-ufrag:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1\r\n\
1476 a=ice-pwd:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1\r\n\
1477 a=fingerprint:sha-256 FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF\r\n\
1478 a=setup:actpass\r\n\
1479 a=sctp-port:5000\r\n\
1480 a=max-message-size:16384\r\n\
1481 ";
1482 
1483 let parsed = sdp_parser().parse(sdp);
1484 println!("{:?}", parsed);
1485 parsed.expect("to parse ok");
1486 }
1487 
1488 /// Test parsing an SDP offer containing H.265 with sprop-max-don-diff.
1489 /// Verifies the fmtp line is correctly parsed into FormatParam::SpropMaxDonDiff
1490 /// alongside H265ProfileTierLevel.
1491 #[test]
1492 fn parse_sdp_h265_with_sprop_max_don_diff() {
1493 // Fingerprint split across lines to stay within 110 cols.
1494 let sdp = concat!(
1495 "v=0\r\n",
1496 "o=- 1234567890 2 IN IP4 127.0.0.1\r\n",
1497 "s=-\r\n",
1498 "t=0 0\r\n",
1499 "a=group:BUNDLE 0\r\n",
1500 "a=fingerprint:sha-256 ",
1501 "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:",
1502 "66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:",
1503 "22:33:44:55:66:77:88:99\r\n",
1504 "a=msid-semantic: WMS\r\n",
1505 "m=video 9 UDP/TLS/RTP/SAVPF 102 103\r\n",
1506 "c=IN IP4 0.0.0.0\r\n",
1507 "a=rtcp:9 IN IP4 0.0.0.0\r\n",
1508 "a=ice-ufrag:abcd\r\n",
1509 "a=ice-pwd:abcdefghijklmnopqrstuv\r\n",
1510 "a=ice-options:trickle\r\n",
1511 "a=fingerprint:sha-256 ",
1512 "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:",
1513 "66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:",
1514 "22:33:44:55:66:77:88:99\r\n",
1515 "a=setup:actpass\r\n",
1516 "a=mid:0\r\n",
1517 "a=sendrecv\r\n",
1518 "a=rtpmap:102 H265/90000\r\n",
1519 "a=fmtp:102 profile-id=1;tier-flag=0;",
1520 "level-id=93;sprop-max-don-diff=32\r\n",
1521 "a=rtpmap:103 rtx/90000\r\n",
1522 "a=fmtp:103 apt=102\r\n",
1523 );
1524 
1525 let (sdp, _remaining) = sdp_parser().parse(sdp).expect("SDP should parse");
1526 
1527 // Find the video media section
1528 assert_eq!(sdp.media_lines.len(), 1);
1529 let media = &sdp.media_lines[0];
1530 
1531 // Find fmtp for PT 102 (H.265)
1532 let fmtp_values: Vec<_> = media
1533 .attrs
1534 .iter()
1535 .filter_map(|a| {
1536 if let MediaAttribute::Fmtp { pt, values } = a {
1537 if *pt == 102.into() {
1538 return Some(values.clone());
1539 }
1540 }
1541 None
1542 })
1543 .flatten()
1544 .collect();
1545 
1546 // Should contain H265ProfileTierLevel
1547 assert!(
1548 fmtp_values
1549 .iter()
1550 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_))),
1551 "Should have H265ProfileTierLevel param"
1552 );
1553 
1554 // Should contain SpropMaxDonDiff(32)
1555 assert!(
1556 fmtp_values
1557 .iter()
1558 .any(|p| matches!(p, FormatParam::SpropMaxDonDiff(32))),
1559 "Should have SpropMaxDonDiff(32) param, got: {:?}",
1560 fmtp_values
1561 );
1562 
1563 // Verify PTL values
1564 if let Some(FormatParam::H265ProfileTierLevel(ptl)) = fmtp_values
1565 .iter()
1566 .find(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
1567 {
1568 assert_eq!(ptl.profile_id(), 1);
1569 assert_eq!(ptl.tier_flag(), 0);
1570 assert_eq!(ptl.level_id(), 93);
1571 } else {
1572 panic!("H265ProfileTierLevel not found");
1573 }
1574 }
1575}
1576 
1577// Safari addTransceiver('audio', {direction: 'sendonly'}))
1578 
1579// v=0
1580// o=- 1625652694357831865 2 IN IP4 127.0.0.1
1581// s=-
1582// t=0 0
1583// a=group:BUNDLE 0
1584// a=msid-semantic: WMS
1585// m=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 102 0 8 105 13 110 113 126
1586// c=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0
1587// a=ice-ufrag:CTNF
1588// a=ice-pwd:XbIkl+k8mwLz60TFwb9crzbz
1589// a=ice-options:trickle
1590// a=fingerprint:sha-256 77:8C:32:B8:DA:4E:78:64:C8:4A:2A:E2:1D:60:2A:83:6B:51:B8:D5:EE:5A:ED:75:4E:C6:98:2D:78:4D:94:D8
1591// a=setup:actpass
1592// a=mid:0
1593// a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
1594// a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
1595// a=sendonly
1596// a=msid:- 39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
1597// a=rtcp-mux
1598// a=rtpmap:111 opus/48000/2
1599// a=rtcp-fb:111 transport-cc
1600// a=fmtp:111 minptime=10;useinbandfec=1
1601// a=rtpmap:103 ISAC/16000
1602// a=rtpmap:9 G722/8000
1603// a=rtpmap:102 ILBC/8000
1604// a=rtpmap:0 PCMU/8000
1605// a=rtpmap:8 PCMA/8000
1606// a=rtpmap:105 CN/16000
1607// a=rtpmap:13 CN/8000
1608// a=rtpmap:110 telephone-event/48000
1609// a=rtpmap:113 telephone-event/16000
1610// a=rtpmap:126 telephone-event/8000
1611// a=ssrc:457025658 cname:6OQ+jzZE+UnQgSUr
1612// a=ssrc:457025658 msid:- 39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
1613// a=ssrc:457025658 mslabel:-
1614// a=ssrc:457025658 label:39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
1615 
1616// Chrome addTransceiver('audio', {direction: 'sendonly'}))
1617 
1618// v=0
1619// o=- 6740661649996974832 2 IN IP4 127.0.0.1
1620// s=-
1621// t=0 0
1622// a=group:BUNDLE 0
1623// a=msid-semantic: WMS
1624// m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
1625// c=IN IP4 0.0.0.0
1626// a=rtcp:9 IN IP4 0.0.0.0
1627// a=ice-ufrag:Y28C
1628// a=ice-pwd:W6fHYINgGi9QF0BHdM/kchTW
1629// a=ice-options:trickle
1630// a=fingerprint:sha-256 E0:C0:2D:52:8D:FA:14:69:A0:A5:5D:63:E0:82:92:DB:37:38:D2:F3:12:D0:1F:4E:E5:6F:1A:F5:C3:97:6B:32
1631// a=setup:actpass
1632// a=mid:0
1633// a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
1634// a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
1635// a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
1636// a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
1637// a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
1638// a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
1639// a=sendonly
1640// a=msid:- 7a08dda6-518f-4027-b707-410a6d414176
1641// a=rtcp-mux
1642// a=rtpmap:111 opus/48000/2
1643// a=rtcp-fb:111 transport-cc
1644// a=fmtp:111 minptime=10;useinbandfec=1
1645// a=rtpmap:103 ISAC/16000
1646// a=rtpmap:104 ISAC/32000
1647// a=rtpmap:9 G722/8000
1648// a=rtpmap:0 PCMU/8000
1649// a=rtpmap:8 PCMA/8000
1650// a=rtpmap:106 CN/32000
1651// a=rtpmap:105 CN/16000
1652// a=rtpmap:13 CN/8000
1653// a=rtpmap:110 telephone-event/48000
1654// a=rtpmap:112 telephone-event/32000
1655// a=rtpmap:113 telephone-event/16000
1656// a=rtpmap:126 telephone-event/8000
1657// a=ssrc:2147603131 cname:TbS1Ajv9obq6/63I
1658// a=ssrc:2147603131 msid:- 7a08dda6-518f-4027-b707-410a6d414176
1659// a=ssrc:2147603131 mslabel:-
1660// a=ssrc:2147603131 label:7a08dda6-518f-4027-b707-410a6d414176
1661 
1662// Chrome add video
1663// pc.addTransceiver("video", {
1664// direction: 'sendonly',
1665// sendEncodings: [
1666// { rid: 'hi' },
1667// { rid: 'lo', scaleResolutionDownBy: 2 },
1668// ]
1669// })
1670 
1671// v=0
1672// o=- 2847298852000198709 3 IN IP4 127.0.0.1
1673// s=-
1674// t=0 0
1675// a=group:BUNDLE 0
1676// a=extmap-allow-mixed
1677// a=msid-semantic: WMS
1678// m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 112 113 114
1679// c=IN IP4 0.0.0.0
1680// a=rtcp:9 IN IP4 0.0.0.0
1681// a=ice-ufrag:numD
1682// a=ice-pwd:L/hHurqERpVUACiI8To+vXLv
1683// a=ice-options:trickle
1684// a=fingerprint:sha-256 A5:79:72:59:3D:32:74:9C:44:70:DE:39:15:C3:99:51:32:6E:0D:F0:60:DD:2F:31:90:E5:96:B4:1D:CA:48:E1
1685// a=setup:actpass
1686// a=mid:0
1687// a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
1688// a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
1689// a=extmap:3 urn:3gpp:video-orientation
1690// a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
1691// a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
1692// a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
1693// a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
1694// a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
1695// a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
1696// a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
1697// a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
1698// a=extmap:13 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
1699// a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
1700// a=sendonly
1701// a=msid:- 7d27ad91-4770-4a4d-a053-2b06233753d2
1702// a=rtcp-mux
1703// a=rtcp-rsize
1704// a=rtpmap:96 VP8/90000
1705// a=rtcp-fb:96 goog-remb
1706// a=rtcp-fb:96 transport-cc
1707// a=rtcp-fb:96 ccm fir
1708// a=rtcp-fb:96 nack
1709// a=rtcp-fb:96 nack pli
1710// a=rtpmap:97 rtx/90000
1711// a=fmtp:97 apt=96
1712// a=rtpmap:102 H264/90000
1713// a=rtcp-fb:102 goog-remb
1714// a=rtcp-fb:102 transport-cc
1715// a=rtcp-fb:102 ccm fir
1716// a=rtcp-fb:102 nack
1717// a=rtcp-fb:102 nack pli
1718// a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
1719// a=rtpmap:103 rtx/90000
1720// a=fmtp:103 apt=102
1721// a=rtpmap:104 H264/90000
1722// a=rtcp-fb:104 goog-remb
1723// a=rtcp-fb:104 transport-cc
1724// a=rtcp-fb:104 ccm fir
1725// a=rtcp-fb:104 nack
1726// a=rtcp-fb:104 nack pli
1727// a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
1728// a=rtpmap:105 rtx/90000
1729// a=fmtp:105 apt=104
1730// a=rtpmap:106 H264/90000
1731// a=rtcp-fb:106 goog-remb
1732// a=rtcp-fb:106 transport-cc
1733// a=rtcp-fb:106 ccm fir
1734// a=rtcp-fb:106 nack
1735// a=rtcp-fb:106 nack pli
1736// a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
1737// a=rtpmap:107 rtx/90000
1738// a=fmtp:107 apt=106
1739// a=rtpmap:108 H264/90000
1740// a=rtcp-fb:108 goog-remb
1741// a=rtcp-fb:108 transport-cc
1742// a=rtcp-fb:108 ccm fir
1743// a=rtcp-fb:108 nack
1744// a=rtcp-fb:108 nack pli
1745// a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
1746// a=rtpmap:109 rtx/90000
1747// a=fmtp:109 apt=108
1748// a=rtpmap:127 H264/90000
1749// a=rtcp-fb:127 goog-remb
1750// a=rtcp-fb:127 transport-cc
1751// a=rtcp-fb:127 ccm fir
1752// a=rtcp-fb:127 nack
1753// a=rtcp-fb:127 nack pli
1754// a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
1755// a=rtpmap:125 rtx/90000
1756// a=fmtp:125 apt=127
1757// a=rtpmap:39 H264/90000
1758// a=rtcp-fb:39 goog-remb
1759// a=rtcp-fb:39 transport-cc
1760// a=rtcp-fb:39 ccm fir
1761// a=rtcp-fb:39 nack
1762// a=rtcp-fb:39 nack pli
1763// a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
1764// a=rtpmap:40 rtx/90000
1765// a=fmtp:40 apt=39
1766// a=rtpmap:45 AV1/90000
1767// a=rtcp-fb:45 goog-remb
1768// a=rtcp-fb:45 transport-cc
1769// a=rtcp-fb:45 ccm fir
1770// a=rtcp-fb:45 nack
1771// a=rtcp-fb:45 nack pli
1772// a=fmtp:45 level-idx=5;profile=0;tier=0
1773// a=rtpmap:46 rtx/90000
1774// a=fmtp:46 apt=45
1775// a=rtpmap:98 VP9/90000
1776// a=rtcp-fb:98 goog-remb
1777// a=rtcp-fb:98 transport-cc
1778// a=rtcp-fb:98 ccm fir
1779// a=rtcp-fb:98 nack
1780// a=rtcp-fb:98 nack pli
1781// a=fmtp:98 profile-id=0
1782// a=rtpmap:99 rtx/90000
1783// a=fmtp:99 apt=98
1784// a=rtpmap:100 VP9/90000
1785// a=rtcp-fb:100 goog-remb
1786// a=rtcp-fb:100 transport-cc
1787// a=rtcp-fb:100 ccm fir
1788// a=rtcp-fb:100 nack
1789// a=rtcp-fb:100 nack pli
1790// a=fmtp:100 profile-id=2
1791// a=rtpmap:101 rtx/90000
1792// a=fmtp:101 apt=100
1793// a=rtpmap:112 red/90000
1794// a=rtpmap:113 rtx/90000
1795// a=fmtp:113 apt=112
1796// a=rtpmap:114 ulpfec/90000
1797// a=rid:hi send
1798// a=rid:lo send
1799// a=simulcast:send hi;lo
1800 
1801// DataChannel only in Safari
1802// v=0
1803// o=- 4611516372927609806 2 IN IP4 127.0.0.1
1804// s=-
1805// t=0 0
1806// a=group:BUNDLE 0
1807// a=extmap-allow-mixed
1808// a=msid-semantic: WMS
1809// m=application 9 UDP/DTLS/SCTP webrtc-datachannel
1810// c=IN IP4 0.0.0.0
1811// a=ice-ufrag:HhS+
1812// a=ice-pwd:FhYTGhlAtKCe6KFIX8b+AThW
1813// a=ice-options:trickle
1814// a=fingerprint:sha-256 B4:12:1C:7C:7D:ED:F1:FA:61:07:57:9C:29:BE:58:E3:BC:41:E7:13:8E:7D:D3:9D:1F:94:6E:A5:23:46:94:23
1815// a=setup:actpass
1816// a=mid:0
1817// a=sctp-port:5000
1818// a=max-message-size:262144