sipx_sdp/session.rs
1//! The session description AST.
2//!
3//! Every line is kept. SDP grows new attributes constantly, and an element that silently drops
4//! what it does not understand breaks features it has never heard of — the typed fields here
5//! are a view over the lines, not a replacement for them.
6
7use std::fmt::{self, Write as _};
8use std::net::IpAddr;
9
10/// A unicast address as written on an `o=` or `c=` line.
11///
12/// RFC 8866 §5.2 and §5.7 allow a fully-qualified domain name here, not just a literal. A
13/// name is kept as written: resolving it takes a resolver, which is I/O this crate does not
14/// do, and re-emitting it verbatim is what keeps a round trip faithful.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Address {
17 /// An IP literal.
18 Ip(IpAddr),
19 /// A fully-qualified domain name, kept as written.
20 Host(String),
21}
22
23impl Address {
24 /// The IP, when the address is a literal. A name yields `None`; turning it into an
25 /// address is the caller's job.
26 #[must_use]
27 pub fn ip(&self) -> Option<IpAddr> {
28 match self {
29 Self::Ip(ip) => Some(*ip),
30 Self::Host(_) => None,
31 }
32 }
33
34 fn address_type(&self) -> &'static str {
35 match self {
36 Self::Ip(IpAddr::V6(_)) => "IP6",
37 // RFC 8866 §5.7 requires an addrtype even for a name, whose family the
38 // description alone cannot reveal; IP4 is the one every implementation accepts.
39 _ => "IP4",
40 }
41 }
42}
43
44impl From<IpAddr> for Address {
45 fn from(ip: IpAddr) -> Self {
46 Self::Ip(ip)
47 }
48}
49
50impl fmt::Display for Address {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Ip(ip) => ip.fmt(f),
54 Self::Host(host) => f.write_str(host),
55 }
56 }
57}
58
59/// Which way media flows, from the point of view of the description that carries it.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum Direction {
62 /// Both ways. The default when no direction attribute is present (RFC 4566 §6).
63 #[default]
64 SendRecv,
65 /// This side sends only.
66 SendOnly,
67 /// This side receives only.
68 RecvOnly,
69 /// Neither, but the stream stays negotiated.
70 Inactive,
71}
72
73impl Direction {
74 /// The direction an answer must carry to match an offer.
75 ///
76 /// This is a mirror, not a copy. An offer of `sendonly` means "I will send, you will
77 /// receive", so the answer says `recvonly`. Copying the offer's direction instead is a
78 /// common bug and produces a call where both ends wait for audio.
79 #[must_use]
80 pub fn mirrored(self) -> Self {
81 match self {
82 Self::SendRecv => Self::SendRecv,
83 Self::SendOnly => Self::RecvOnly,
84 Self::RecvOnly => Self::SendOnly,
85 Self::Inactive => Self::Inactive,
86 }
87 }
88
89 /// Parse a direction attribute name.
90 #[must_use]
91 pub fn parse(name: &str) -> Option<Self> {
92 match name {
93 "sendrecv" => Some(Self::SendRecv),
94 "sendonly" => Some(Self::SendOnly),
95 "recvonly" => Some(Self::RecvOnly),
96 "inactive" => Some(Self::Inactive),
97 _ => None,
98 }
99 }
100
101 /// The attribute name.
102 #[must_use]
103 pub fn as_str(self) -> &'static str {
104 match self {
105 Self::SendRecv => "sendrecv",
106 Self::SendOnly => "sendonly",
107 Self::RecvOnly => "recvonly",
108 Self::Inactive => "inactive",
109 }
110 }
111
112 /// Whether this side will send media.
113 #[must_use]
114 pub fn sends(self) -> bool {
115 matches!(self, Self::SendRecv | Self::SendOnly)
116 }
117
118 /// Whether this side will receive media.
119 #[must_use]
120 pub fn receives(self) -> bool {
121 matches!(self, Self::SendRecv | Self::RecvOnly)
122 }
123}
124
125/// An `a=` line: either `a=name` or `a=name:value`.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Attribute {
128 /// The attribute name.
129 pub name: String,
130 /// Its value, if it has one.
131 pub value: Option<String>,
132}
133
134impl Attribute {
135 /// A flag attribute, like `a=sendrecv`.
136 #[must_use]
137 pub fn flag(name: impl Into<String>) -> Self {
138 Self {
139 name: name.into(),
140 value: None,
141 }
142 }
143
144 /// A valued attribute, like `a=rtpmap:0 PCMU/8000`.
145 #[must_use]
146 pub fn valued(name: impl Into<String>, value: impl Into<String>) -> Self {
147 Self {
148 name: name.into(),
149 value: Some(value.into()),
150 }
151 }
152
153 fn write_to(&self, out: &mut String) {
154 match &self.value {
155 Some(value) => {
156 let _ = writeln!(out, "a={}:{value}\r", self.name);
157 }
158 None => {
159 let _ = writeln!(out, "a={}\r", self.name);
160 }
161 }
162 }
163}
164
165/// An `o=` line.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct Origin {
168 /// The originator's name, or `-`.
169 pub username: String,
170 /// A session identifier.
171 pub session_id: u64,
172 /// The version, which increases with each modified offer.
173 pub session_version: u64,
174 /// The address the session is described from.
175 pub address: Address,
176}
177
178impl Origin {
179 /// An origin for an address, with the identifier and version supplied by the caller.
180 ///
181 /// The caller supplies them deliberately: a session version has to *increase* across
182 /// re-offers, and only the caller knows what it used last.
183 #[must_use]
184 pub fn new(address: IpAddr, session_id: u64, session_version: u64) -> Self {
185 Self {
186 username: "-".to_owned(),
187 session_id,
188 session_version,
189 address: Address::Ip(address),
190 }
191 }
192}
193
194/// A `c=` line.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Connection {
197 /// Where media should be sent.
198 pub address: Address,
199}
200
201impl Connection {
202 /// A connection line for an address.
203 #[must_use]
204 pub fn new(address: IpAddr) -> Self {
205 Self {
206 address: Address::Ip(address),
207 }
208 }
209}
210
211/// A `t=` line.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
213pub struct Timing {
214 /// Start time, 0 for unbounded.
215 pub start: u64,
216 /// Stop time, 0 for unbounded.
217 pub stop: u64,
218}
219
220/// An `m=` line and everything under it.
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct MediaDescription {
223 /// `audio`, `video`, `application`…
224 pub media: String,
225 /// The port. Zero means the stream is rejected — and a rejected stream is still *present*,
226 /// which is what keeps the answer's media lines aligned with the offer's.
227 pub port: u16,
228 /// The transport protocol, such as `RTP/AVP`.
229 pub protocol: String,
230 /// Payload type numbers, in preference order.
231 pub formats: Vec<String>,
232 /// A `c=` line for this stream, overriding the session's.
233 pub connection: Option<Connection>,
234 /// Attributes under this media line.
235 pub attributes: Vec<Attribute>,
236 /// Lines under this media line that this crate does not model, kept so they survive a
237 /// round trip.
238 pub other: Vec<(char, String)>,
239}
240
241/// Whether one media section uses a separate RTCP port or multiplexes it with RTP (RFC 5761).
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
243pub enum RtcpMode {
244 /// RTP uses the media port and RTCP uses its control port.
245 #[default]
246 Separate,
247 /// RTP and RTCP share the media port.
248 Mux,
249}
250
251impl RtcpMode {
252 /// Settle an offer/answer exchange.
253 ///
254 /// RFC 5761 §5.1.3: mux is active only when the offer and the answer both carry the flag.
255 /// Omission in the answer is the separate-port fallback and needs no second exchange.
256 #[must_use]
257 pub fn from_exchange(offer: &MediaDescription, answer: &MediaDescription) -> Self {
258 if !offer.is_rejected() && !answer.is_rejected() && offer.rtcp_mux() && answer.rtcp_mux() {
259 Self::Mux
260 } else {
261 Self::Separate
262 }
263 }
264}
265
266impl MediaDescription {
267 /// An audio stream offering these payload types.
268 #[must_use]
269 pub fn audio(port: u16, formats: Vec<String>) -> Self {
270 Self {
271 media: "audio".to_owned(),
272 port,
273 protocol: "RTP/AVP".to_owned(),
274 formats,
275 connection: None,
276 attributes: Vec::new(),
277 other: Vec::new(),
278 }
279 }
280
281 /// Whether this stream is rejected.
282 #[must_use]
283 pub fn is_rejected(&self) -> bool {
284 self.port == 0
285 }
286
287 /// The direction, defaulting to `sendrecv` when no attribute says otherwise.
288 #[must_use]
289 pub fn direction(&self) -> Direction {
290 self.declared_direction().unwrap_or_default()
291 }
292
293 /// The direction attribute written under this `m=` line, if any.
294 ///
295 /// RFC 8866 §6.7: a stream without a direction of its own takes the session-level one,
296 /// so an absent attribute is meaningful and not the same thing as `sendrecv`.
297 #[must_use]
298 pub fn declared_direction(&self) -> Option<Direction> {
299 self.attributes.iter().find_map(|a| {
300 a.value
301 .is_none()
302 .then(|| Direction::parse(&a.name))
303 .flatten()
304 })
305 }
306
307 /// Set the direction, replacing any existing one.
308 pub fn set_direction(&mut self, direction: Direction) {
309 self.attributes
310 .retain(|a| !(a.value.is_none() && Direction::parse(&a.name).is_some()));
311 self.attributes.push(Attribute::flag(direction.as_str()));
312 }
313
314 /// The first `a=crypto` line this stream carries that sipx can act on (RFC 4568).
315 ///
316 /// Several may be offered, in preference order. sipx takes the first it can perform rather
317 /// than the first listed: an offer whose favourite suite is one sipx does not implement is
318 /// still an offer worth answering.
319 #[must_use]
320 pub fn crypto(&self) -> Option<crate::crypto::Crypto> {
321 self.attributes
322 .iter()
323 .filter(|attribute| attribute.name == "crypto")
324 .filter_map(|attribute| attribute.value.as_deref())
325 .find_map(crate::crypto::Crypto::parse)
326 }
327
328 /// The first `a=fingerprint` this stream carries that sipx may act on (RFC 8122 §5).
329 ///
330 /// §5.1 has an endpoint offer a fingerprint under *several* hash functions — "the 'SHA-256'
331 /// hash function algorithm and the hash function used to generate the signature on the
332 /// certificate" — so more than one line is normal and any of them identifies the same
333 /// certificate. Taking the first sipx can compute is therefore correct rather than a shortcut;
334 /// the ones it skips are `md5` and `md2`, which §5 forbids acting on.
335 ///
336 /// Looked for on the media description and not the session: a fingerprint may be given at
337 /// either level, and the media-level one wins where both appear. A caller that wants the
338 /// session-level fallback reads [`SessionDescription::fingerprint`].
339 #[must_use]
340 pub fn fingerprint(&self) -> Option<crate::fingerprint::Fingerprint> {
341 self.attributes
342 .iter()
343 .filter(|attribute| attribute.name == "fingerprint")
344 .filter_map(|attribute| attribute.value.as_deref())
345 .find_map(crate::fingerprint::Fingerprint::parse)
346 }
347
348 /// The `a=setup` role this stream declares (RFC 4145 §4).
349 #[must_use]
350 pub fn setup(&self) -> Option<crate::fingerprint::Setup> {
351 self.attributes
352 .iter()
353 .find(|attribute| attribute.name == "setup")
354 .and_then(|attribute| attribute.value.as_deref())
355 .and_then(crate::fingerprint::Setup::parse)
356 }
357
358 /// Whether this stream carries the `a=rtcp-mux` flag (RFC 5761 §5).
359 #[must_use]
360 pub fn rtcp_mux(&self) -> bool {
361 self.attributes
362 .iter()
363 .any(|attribute| attribute.name == "rtcp-mux" && attribute.value.is_none())
364 }
365
366 /// Every `a=candidate` under this stream that sipx can act on (RFC 8839 §5.1).
367 ///
368 /// Media-level, and only media-level: §5.1 defines the attribute there and nowhere else.
369 ///
370 /// Lines sipx cannot act on are **left out of the result rather than turned into an error** —
371 /// an FQDN, an unsupported address family, a transport other than UDP, an unknown candidate
372 /// type. §5.1 requires that a candidate be ignored, and ignoring it means ignoring the line:
373 /// the attribute is still on the description and still round-trips, and the rest of the
374 /// stream is still usable. A stack that refused the description instead would fail calls with
375 /// peers doing nothing wrong.
376 #[must_use]
377 pub fn ice_candidates(&self) -> Vec<crate::ice::Candidate> {
378 self.attributes
379 .iter()
380 .filter(|attribute| attribute.name == "candidate")
381 .filter_map(|attribute| attribute.value.as_deref())
382 .filter_map(crate::ice::Candidate::parse)
383 .collect()
384 }
385
386 /// The `a=remote-candidates` this stream carries (RFC 8839 §5.2). Media-level.
387 ///
388 /// Present only in an offer from a controlling agent for a stream that is Completed, so an
389 /// empty result is the normal case rather than a sign of anything.
390 #[must_use]
391 pub fn ice_remote_candidates(&self) -> Vec<crate::ice::RemoteCandidate> {
392 self.attributes
393 .iter()
394 .filter(|attribute| attribute.name == "remote-candidates")
395 .filter_map(|attribute| attribute.value.as_deref())
396 .filter_map(crate::ice::RemoteCandidate::parse_list)
397 .flatten()
398 .collect()
399 }
400
401 /// This stream's own `a=ice-ufrag` (RFC 8839 §5.4), before the session-level default.
402 ///
403 /// Read [`SessionDescription::ice_credentials_for`] instead unless the distinction matters:
404 /// a stream with no fragment of its own inherits the session's, and RFC 8839 §4.4.1.1.1
405 /// makes the *pair* of values, not either alone, what an ICE restart changes.
406 #[must_use]
407 pub fn ice_ufrag(&self) -> Option<&str> {
408 self.attribute_value("ice-ufrag")
409 }
410
411 /// This stream's own `a=ice-pwd` (RFC 8839 §5.4), before the session-level default.
412 #[must_use]
413 pub fn ice_pwd(&self) -> Option<&str> {
414 self.attribute_value("ice-pwd")
415 }
416
417 /// The option tags this stream advertises (RFC 8839 §5.6).
418 pub fn ice_options(&self) -> impl Iterator<Item = &str> {
419 self.attributes
420 .iter()
421 .filter(|attribute| attribute.name == "ice-options")
422 .filter_map(|attribute| attribute.value.as_deref())
423 .flat_map(crate::ice::option_tags)
424 }
425
426 /// Whether this stream carries `a=ice-mismatch` (RFC 8839 §5.3). Media-level, in an answer.
427 ///
428 /// It means the offer's default destination for a component had no matching `candidate`
429 /// attribute, and therefore that ICE MUST NOT be used for this stream — RFC 3264 procedures
430 /// apply instead. Not a failure: it is the answerer saying an intermediary rewrote the
431 /// addresses, which is what ICE was going to discover the hard way.
432 #[must_use]
433 pub fn ice_mismatch(&self) -> bool {
434 self.has_flag("ice-mismatch")
435 }
436
437 fn attribute_value(&self, name: &str) -> Option<&str> {
438 self.attribute(name)
439 .and_then(|attribute| attribute.value.as_deref())
440 }
441
442 fn has_flag(&self, name: &str) -> bool {
443 self.attributes
444 .iter()
445 .any(|attribute| attribute.name == name && attribute.value.is_none())
446 }
447
448 /// The `rtpmap` for a payload type, if the description gives one.
449 #[must_use]
450 pub fn rtpmap(&self, format: &str) -> Option<&str> {
451 self.attributes.iter().find_map(|a| {
452 if a.name != "rtpmap" {
453 return None;
454 }
455 let value = a.value.as_deref()?;
456 let (payload, rest) = value.split_once(' ')?;
457 (payload == format).then_some(rest)
458 })
459 }
460
461 /// The first attribute with this name.
462 #[must_use]
463 pub fn attribute(&self, name: &str) -> Option<&Attribute> {
464 self.attributes.iter().find(|a| a.name == name)
465 }
466
467 fn write_to(&self, out: &mut String) {
468 let _ = write!(out, "m={} {} {}", self.media, self.port, self.protocol);
469 for format in &self.formats {
470 let _ = write!(out, " {format}");
471 }
472 let _ = writeln!(out, "\r");
473 // RFC 8866 §5.14 fixes the order inside a media description: `i=` before `c=`, then
474 // `b=` and `k=`, then the attributes.
475 write_other_lines(out, &self.other, |kind| kind == 'i');
476 if let Some(connection) = &self.connection {
477 let _ = writeln!(
478 out,
479 "c=IN {} {}\r",
480 connection.address.address_type(),
481 connection.address
482 );
483 }
484 write_other_lines(out, &self.other, |kind| kind != 'i');
485 for attribute in &self.attributes {
486 attribute.write_to(out);
487 }
488 }
489}
490
491fn write_other_lines(out: &mut String, lines: &[(char, String)], take: impl Fn(char) -> bool) {
492 for (kind, value) in lines {
493 if take(*kind) {
494 let _ = writeln!(out, "{kind}={value}\r");
495 }
496 }
497}
498
499/// A whole session description.
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct SessionDescription {
502 /// The `o=` line.
503 pub origin: Origin,
504 /// The `s=` line.
505 pub session_name: String,
506 /// The session-level `c=` line.
507 pub connection: Option<Connection>,
508 /// The `t=` lines.
509 pub timing: Vec<Timing>,
510 /// Session-level attributes.
511 pub attributes: Vec<Attribute>,
512 /// The media streams, in order. The order is load-bearing: an answer's streams correspond
513 /// to the offer's by position.
514 pub media: Vec<MediaDescription>,
515 /// Lines this crate does not model, kept so they survive a round trip.
516 pub other: Vec<(char, String)>,
517}
518
519impl SessionDescription {
520 /// A session description for an address.
521 #[must_use]
522 pub fn new(address: IpAddr, session_id: u64, session_version: u64) -> Self {
523 Self {
524 origin: Origin::new(address, session_id, session_version),
525 session_name: "-".to_owned(),
526 connection: Some(Connection::new(address)),
527 timing: vec![Timing::default()],
528 attributes: Vec::new(),
529 media: Vec::new(),
530 other: Vec::new(),
531 }
532 }
533
534 /// The connection address for a stream: its own `c=` if it has one, else the session's.
535 ///
536 /// A domain-name address yields `None` here — the name is preserved in the description,
537 /// but only a resolver can turn it into somewhere to send media.
538 #[must_use]
539 pub fn address_for(&self, media: &MediaDescription) -> Option<IpAddr> {
540 media
541 .connection
542 .as_ref()
543 .or(self.connection.as_ref())
544 .and_then(|connection| connection.address.ip())
545 }
546
547 /// The session-level direction, defaulting to `sendrecv`.
548 #[must_use]
549 pub fn direction(&self) -> Direction {
550 self.attributes
551 .iter()
552 .find_map(|a| {
553 a.value
554 .is_none()
555 .then(|| Direction::parse(&a.name))
556 .flatten()
557 })
558 .unwrap_or_default()
559 }
560
561 /// The session-level `a=fingerprint`, if the description carries one (RFC 8122 §5).
562 ///
563 /// §5 allows the attribute at either level, and one given here applies to every stream that
564 /// does not override it. A browser puts it here and on no `m=` line at all, so a stack that
565 /// reads only the media level finds nothing and refuses a perfectly good offer.
566 #[must_use]
567 pub fn fingerprint(&self) -> Option<crate::fingerprint::Fingerprint> {
568 self.attributes
569 .iter()
570 .filter(|attribute| attribute.name == "fingerprint")
571 .filter_map(|attribute| attribute.value.as_deref())
572 .find_map(crate::fingerprint::Fingerprint::parse)
573 }
574
575 /// The session-level `a=ice-ufrag` (RFC 8839 §5.4), which is a default for every stream.
576 #[must_use]
577 pub fn ice_ufrag(&self) -> Option<&str> {
578 self.attribute_value("ice-ufrag")
579 }
580
581 /// The session-level `a=ice-pwd` (RFC 8839 §5.4), which is a default for every stream.
582 #[must_use]
583 pub fn ice_pwd(&self) -> Option<&str> {
584 self.attribute_value("ice-pwd")
585 }
586
587 /// The short-term credentials that apply to one stream (RFC 8839 §5.4).
588 ///
589 /// **Media level wins.** §5.4 allows the attributes at either level and makes the session
590 /// level a default, so a stream with its own `ice-ufrag` uses it and a stream without it
591 /// inherits — and the two must not be mixed: taking the fragment from the media line and the
592 /// password from the session line produces a credential neither end can authenticate with,
593 /// and it looks exactly like a network fault. The pair is therefore resolved together, from
594 /// whichever level supplied the fragment.
595 ///
596 /// `None` when the description gives no usable pair at either level, which per §5.4 means
597 /// the stream is not doing ICE. Values up to 256 characters are accepted, as §5.4 requires,
598 /// even though sipx will not send one longer than 32.
599 #[must_use]
600 pub fn ice_credentials_for(&self, media: &MediaDescription) -> Option<crate::ice::Credentials> {
601 let level = |ufrag: Option<&str>, pwd: Option<&str>| match (ufrag, pwd) {
602 (Some(ufrag), Some(pwd)) => crate::ice::Credentials::received(ufrag, pwd),
603 _ => None,
604 };
605 level(media.ice_ufrag(), media.ice_pwd())
606 .or_else(|| level(self.ice_ufrag(), self.ice_pwd()))
607 }
608
609 /// The session-level option tags (RFC 8839 §5.6).
610 pub fn ice_options(&self) -> impl Iterator<Item = &str> {
611 self.attributes
612 .iter()
613 .filter(|attribute| attribute.name == "ice-options")
614 .filter_map(|attribute| attribute.value.as_deref())
615 .flat_map(crate::ice::option_tags)
616 }
617
618 /// The option tags that apply to one stream: the session's and the stream's together.
619 ///
620 /// A union and not an override, which is where this differs from the credentials above.
621 /// §5.6 makes the attribute a statement that "a certain extension is supported by the agent",
622 /// and an agent does not stop supporting an extension because a particular `m=` line named a
623 /// different one. Tags may repeat if both levels name the same one.
624 pub fn ice_options_for<'a>(
625 &'a self,
626 media: &'a MediaDescription,
627 ) -> impl Iterator<Item = &'a str> {
628 self.ice_options().chain(media.ice_options())
629 }
630
631 /// Whether the description carries `a=ice-lite` (RFC 8839 §5.3). Session-level only.
632 ///
633 /// A lite peer never gathers, never sends a check and never nominates, so sipx takes the
634 /// controlling role unconditionally against one (RFC 8445 §6.1.1) and must not wait for
635 /// checks that will never arrive. sipx itself is always a full agent and never sends this.
636 #[must_use]
637 pub fn is_ice_lite(&self) -> bool {
638 self.has_flag("ice-lite")
639 }
640
641 /// The `a=ice-pacing` the description asks for (RFC 8839 §5.5). Session-level only.
642 ///
643 /// [`Pacing::DEFAULT`] when the attribute is absent or unreadable, because §5.5 gives the
644 /// absent case a value — 50 ms — rather than leaving it undefined.
645 ///
646 /// [`Pacing::DEFAULT`]: crate::ice::Pacing::DEFAULT
647 #[must_use]
648 pub fn ice_pacing(&self) -> crate::ice::Pacing {
649 self.attribute_value("ice-pacing")
650 .and_then(crate::ice::Pacing::parse)
651 .unwrap_or(crate::ice::Pacing::DEFAULT)
652 }
653
654 fn attribute_value(&self, name: &str) -> Option<&str> {
655 self.attributes
656 .iter()
657 .find(|attribute| attribute.name == name)
658 .and_then(|attribute| attribute.value.as_deref())
659 }
660
661 fn has_flag(&self, name: &str) -> bool {
662 self.attributes
663 .iter()
664 .any(|attribute| attribute.name == name && attribute.value.is_none())
665 }
666
667 /// Serialize to the wire format.
668 ///
669 /// Line order follows RFC 8866 §5, which is not a style preference: the grammar fixes the
670 /// order, and receivers do reject descriptions that get it wrong.
671 #[must_use]
672 pub fn to_string_sdp(&self) -> String {
673 let mut out = String::with_capacity(256);
674 let _ = writeln!(out, "v=0\r");
675 let _ = writeln!(
676 out,
677 "o={} {} {} IN {} {}\r",
678 self.origin.username,
679 self.origin.session_id,
680 self.origin.session_version,
681 self.origin.address.address_type(),
682 self.origin.address
683 );
684 let _ = writeln!(out, "s={}\r", self.session_name);
685 // RFC 8866 §5 gives every line type a fixed slot: `i=`, `u=`, `e=` and `p=` before
686 // `c=`, `b=` between `c=` and the timing lines, everything else after them. Kept
687 // lines go into their slot, not wherever is convenient, because receivers enforce
688 // the grammar's order.
689 write_other_lines(&mut out, &self.other, |kind| {
690 matches!(kind, 'i' | 'u' | 'e' | 'p')
691 });
692 if let Some(connection) = &self.connection {
693 let _ = writeln!(
694 out,
695 "c=IN {} {}\r",
696 connection.address.address_type(),
697 connection.address
698 );
699 }
700 write_other_lines(&mut out, &self.other, |kind| kind == 'b');
701 if self.timing.is_empty() {
702 let _ = writeln!(out, "t=0 0\r");
703 }
704 for timing in &self.timing {
705 let _ = writeln!(out, "t={} {}\r", timing.start, timing.stop);
706 }
707 write_other_lines(&mut out, &self.other, |kind| {
708 !matches!(kind, 'i' | 'u' | 'e' | 'p' | 'b')
709 });
710 for attribute in &self.attributes {
711 attribute.write_to(&mut out);
712 }
713 for media in &self.media {
714 media.write_to(&mut out);
715 }
716 out
717 }
718}