jitsi_jingle_sdp/
lib.rs

1use std::collections::{hash_map::Entry, HashMap};
2
3use itertools::Itertools;
4pub use jitsi_xmpp_parsers::jingle::{Action, Jingle};
5use jitsi_xmpp_parsers::{
6  jingle::{Content, Description, Transport},
7  jingle_dtls_srtp::Fingerprint,
8  jingle_ice_udp::Transport as IceUdpTransport,
9  jingle_rtp::Description as RtpDescription,
10  jingle_ssma::{Group as SourceGroup, Parameter as SourceParameter, Source},
11};
12pub use sdp::SessionDescription;
13use sdp::{direction::Direction, extmap::ExtMap, MediaDescription};
14use xmpp_parsers::{
15  hashes::Algo,
16  jingle::{ContentId, Creator, Senders, SessionId},
17  jingle_dtls_srtp::Setup,
18  jingle_grouping::{Content as GroupContent, Group},
19  jingle_ice_udp::Type as CandidateType,
20  jingle_rtcp_fb::RtcpFb,
21  jingle_rtp::{Channels, Parameter, PayloadType, RtcpMux},
22  jingle_rtp_hdrext::{RtpHdrext, Senders as RtpHdrextSenders},
23};
24
25#[derive(thiserror::Error, Debug)]
26pub enum Error {
27  #[error("invalid Jingle IQ")]
28  InvalidJingle,
29  #[error("invalid JID")]
30  InvalidJid,
31  #[error("unknown error")]
32  Unknown,
33}
34
35pub type Result<T> = std::result::Result<T, Error>;
36
37pub trait SessionDescriptionJingleConversionsExt {
38  fn try_from_jingle(jingle: &Jingle) -> Result<SessionDescription>;
39  fn try_to_jingle(
40    &self,
41    action: Action,
42    session_id: &str,
43    initiator: &str,
44    responder: &str,
45  ) -> Result<Jingle>;
46  fn add_sources_from_jingle(&mut self, jingle: &Jingle) -> Result<()>;
47  fn remove_sources_from_jingle(&mut self, jingle: &Jingle) -> Result<()>;
48}
49
50fn group_sources(description: &RtpDescription) -> Vec<(Option<&SourceGroup>, Vec<&Source>)> {
51  let sources: HashMap<u32, &Source> = description
52    .ssrcs
53    .iter()
54    .map(|source| (source.id, source))
55    .collect();
56
57  let mut sources_by_group = vec![];
58
59  sources_by_group.extend(
60    sources
61      .values()
62      .filter(|source| {
63        !description.ssrc_groups.iter().any(|group| {
64          group
65            .sources
66            .iter()
67            .find(|group_source| group_source.id == source.id)
68            .is_some()
69        })
70      })
71      .copied()
72      .map(|source| (None, vec![source])),
73  );
74
75  sources_by_group.extend(description.ssrc_groups.iter().map(|group| {
76    (
77      Some(group),
78      group
79        .sources
80        .iter()
81        .filter_map(|source| sources.get(&source.id))
82        .copied()
83        .collect(),
84    )
85  }));
86
87  sources_by_group.sort_by_key(|(_, source)| {
88    !source
89      .iter()
90      .map(|source| &source.parameters)
91      .flatten()
92      .any(|parameter| {
93        parameter.name == "msid"
94          && parameter
95            .value
96            .as_ref()
97            .map(|value| value.starts_with("mixedmslabel "))
98            .unwrap_or_default()
99      })
100  });
101
102  sources_by_group
103}
104
105fn msid_for_sources<'a>(sources: &'a [&'a Source]) -> Option<&'a str> {
106  sources
107    .iter()
108    .filter_map(|source| {
109      source
110        .parameters
111        .iter()
112        .filter_map(|param| {
113          (param.name == "msid")
114            .then(|| param.value.as_deref())
115            .flatten()
116        })
117        .next()
118    })
119    .next()
120}
121
122fn populate_media_description_from_sources(
123  mut md: MediaDescription,
124  sources: &[&Source],
125  maybe_group: Option<&SourceGroup>,
126) -> MediaDescription {
127  if let Some(msid) = msid_for_sources(sources) {
128    md = md.with_value_attribute("msid".into(), msid.into());
129  }
130
131  if sources
132    .first()
133    .and_then(|source| source.info.as_ref())
134    .map(|info| info.owner.as_str())
135    == Some("jvb")
136  {
137    md = md.with_property_attribute("sendrecv".into());
138  }
139  else {
140    md = md.with_property_attribute("sendonly".into());
141  }
142
143  for source in sources {
144    for param in &source.parameters {
145      md = md.with_value_attribute(
146        "ssrc".into(),
147        format!(
148          "{} {}{}",
149          source.id,
150          param.name,
151          param
152            .value
153            .as_ref()
154            .map(|value| format!(":{}", value))
155            .unwrap_or_default(),
156        ),
157      );
158    }
159  }
160
161  if let Some(group) = maybe_group {
162    md = md.with_value_attribute(
163      "ssrc-group".into(),
164      format!(
165        "{} {}",
166        group.semantics,
167        group.sources.iter().map(|source| source.id).join(" ")
168      ),
169    );
170  }
171
172  md
173}
174
175impl SessionDescriptionJingleConversionsExt for SessionDescription {
176  fn try_from_jingle(jingle: &Jingle) -> Result<SessionDescription> {
177    let mut sd = SessionDescription::new_jsep_session_description(false)
178      .with_value_attribute("msid-semantic".into(), " WMS *".into());
179
180    let mut mid = 0;
181
182    for content in &jingle.contents {
183      if let Some(Description::Rtp(description)) = &content.description {
184        let sources_by_group = group_sources(&description);
185
186        for (maybe_group, sources) in sources_by_group {
187          let mut md =
188            MediaDescription::new_jsep_media_description(description.media.clone(), vec![]);
189
190          for pt in &description.payload_types {
191            md = md.with_codec(
192              pt.id,
193              pt.name.as_ref().ok_or(Error::InvalidJingle)?.clone(),
194              pt.clockrate.ok_or(Error::InvalidJingle)?,
195              if pt.channels.0 == 1 {
196                0
197              }
198              else {
199                pt.channels.0.into()
200              },
201              pt.parameters
202                .iter()
203                .map(|param| {
204                  format!(
205                    "{}{}",
206                    (!param.name.is_empty())
207                      .then(|| format!("{}=", param.name))
208                      .unwrap_or_default(),
209                    param.value,
210                  )
211                })
212                .join(";"),
213            );
214
215            md = md.with_value_attribute("rtcp".into(), "1 IN IP4 0.0.0.0".into());
216
217            for rtcp_fb in &pt.rtcp_fbs {
218              md = md.with_value_attribute(
219                "rtcp-fb".into(),
220                format!(
221                  "{} {}{}",
222                  pt.id,
223                  rtcp_fb.type_,
224                  rtcp_fb
225                    .subtype
226                    .as_ref()
227                    .map(|subtype| format!(" {}", subtype))
228                    .unwrap_or_default(),
229                ),
230              );
231            }
232          }
233
234          for hdrext in &description.hdrexts {
235            md = md.with_extmap(ExtMap {
236              value: hdrext.id.try_into().map_err(|_| Error::InvalidJingle)?,
237              uri: Some(hdrext.uri.parse().map_err(|_| Error::InvalidJingle)?),
238              direction: Direction::Unspecified,
239              ext_attr: None,
240            });
241          }
242
243          if let Some(Transport::IceUdp(transport)) = &content.transport {
244            if let Some(setup) = transport
245              .fingerprint
246              .as_ref()
247              .and_then(|fingerprint| fingerprint.setup.as_ref())
248            {
249              md = md.with_value_attribute(
250                "setup".into(),
251                match setup {
252                  Setup::Active => "active",
253                  Setup::Passive => "passive",
254                  Setup::Actpass => "actpass",
255                }
256                .into(),
257              );
258            }
259          }
260
261          md = md.with_value_attribute("mid".into(), mid.to_string());
262          md = populate_media_description_from_sources(md, &sources, maybe_group);
263
264          if let Some(Transport::IceUdp(transport)) = &content.transport {
265            if let (Some(ufrag), Some(pwd)) = (&transport.ufrag, &transport.pwd) {
266              md = md.with_ice_credentials(ufrag.into(), pwd.into());
267            }
268
269            if let Some(fingerprint) = &transport.fingerprint {
270              md = md.with_fingerprint(
271                // RFC 4572 section 5
272                // https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml
273                match &fingerprint.hash {
274                  Algo::Sha_1 => "sha-1",
275                  Algo::Sha_256 => "sha-256",
276                  Algo::Sha_512 => "sha-512",
277                  Algo::Unknown(algo)
278                    if ["sha-224", "sha-384", "shake128", "shake256", "md5", "md2"]
279                      .contains(&algo.as_str()) =>
280                  {
281                    &algo
282                  },
283                  _ => return Err(Error::InvalidJingle),
284                }
285                .into(),
286                fingerprint
287                  .value
288                  .iter()
289                  .map(|byte| format!("{:02X}", byte))
290                  .join(":"),
291              );
292            }
293
294            for candidate in &transport.candidates {
295              // https://datatracker.ietf.org/doc/html/rfc8839#section-5.1
296              let mut candidate_str = format!(
297                "{} {} {} {} {} {} typ {}",
298                candidate.foundation,
299                candidate.component,
300                candidate.protocol,
301                candidate.priority,
302                candidate.ip,
303                candidate.port,
304                candidate.type_,
305              );
306              match candidate.type_ {
307                CandidateType::Host => {},
308                CandidateType::Prflx | CandidateType::Srflx | CandidateType::Relay => {
309                  candidate_str.push_str(&format!(
310                    "{}{}",
311                    candidate
312                      .rel_addr
313                      .map(|raddr| format!(" raddr {}", raddr))
314                      .unwrap_or_default(),
315                    candidate
316                      .rel_port
317                      .map(|rport| format!(" rport {}", rport))
318                      .unwrap_or_default(),
319                  ));
320                },
321              };
322              candidate_str.push_str(&format!(" generation {}", candidate.generation));
323              md = md.with_candidate(candidate_str);
324            }
325          }
326
327          if description.rtcp_mux.is_some() {
328            md = md.with_property_attribute("rtcp-mux".into());
329          }
330
331          sd = sd.with_media(md);
332          mid += 1;
333        }
334      }
335    }
336
337    if let Some(group) = &jingle.group {
338      sd = sd.with_value_attribute(
339        "group".into(),
340        format!("{} {}", group.semantics, (0..mid).join(" "),),
341      );
342    }
343
344    Ok(sd)
345  }
346
347  fn try_to_jingle(
348    &self,
349    action: Action,
350    session_id: &str,
351    initiator: &str,
352    responder: &str,
353  ) -> Result<Jingle> {
354    let mut jingle = Jingle::new(action, SessionId(session_id.into()))
355      .with_initiator(initiator.parse().map_err(|_| Error::InvalidJid)?)
356      .with_responder(responder.parse().map_err(|_| Error::InvalidJid)?);
357
358    let mut contents: HashMap<&str, Content> = HashMap::new();
359
360    for md in &self.media_descriptions {
361      let content = match contents.entry(md.media_name.media.as_str()) {
362        Entry::Occupied(entry) => entry.into_mut(),
363        Entry::Vacant(entry) => {
364          let mut description = RtpDescription::new(md.media_name.media.clone());
365
366          description.ssrc = md
367            .attributes
368            .iter()
369            .filter(|attribute| attribute.key == "ssrc")
370            .next()
371            .and_then(|attribute| {
372              let mut parts = attribute.value.as_ref()?.split(' ');
373              Some(parts.next().unwrap().into())
374            });
375
376          for rtpmap in md
377            .attributes
378            .iter()
379            .filter(|attribute| attribute.key == "rtpmap")
380          {
381            if let Some(value) = &rtpmap.value {
382              let mut parts = value.splitn(2, ' ');
383              let id: u8 = parts
384                .next()
385                .unwrap()
386                .parse()
387                .map_err(|_| Error::InvalidJingle)?;
388              let mut parts = parts.next().ok_or(Error::InvalidJingle)?.split('/');
389              let mut pt = PayloadType {
390                id,
391                name: Some(parts.next().unwrap().into()),
392                clockrate: Some(
393                  parts
394                    .next()
395                    .ok_or(Error::InvalidJingle)?
396                    .parse()
397                    .map_err(|_| Error::InvalidJingle)?,
398                ),
399                channels: Channels(
400                  parts
401                    .next()
402                    .map(|channels| channels.parse())
403                    .transpose()
404                    .map_err(|_| Error::InvalidJingle)?
405                    .unwrap_or(1),
406                ),
407                ptime: None,
408                maxptime: None,
409                parameters: vec![],
410                rtcp_fbs: vec![],
411              };
412
413              for fmtp in md
414                .attributes
415                .iter()
416                .filter(|attribute| attribute.key == "fmtp")
417              {
418                if let Some(value) = &fmtp.value {
419                  let mut parts = value.splitn(2, ' ');
420                  let fmtp_id: u8 = parts
421                    .next()
422                    .unwrap()
423                    .parse()
424                    .map_err(|_| Error::InvalidJingle)?;
425                  if fmtp_id == id {
426                    let parameters = parts.next().ok_or(Error::InvalidJingle)?.split(';');
427                    for parameter in parameters {
428                      if let Some((name, value)) = parameter.split_once('=') {
429                        pt.parameters.push(Parameter {
430                          name: name.into(),
431                          value: value.into(),
432                        });
433                      }
434                    }
435                  }
436                }
437              }
438
439              for rtcp_fb in md
440                .attributes
441                .iter()
442                .filter(|attribute| attribute.key == "rtcp-fb")
443              {
444                if let Some(value) = &rtcp_fb.value {
445                  let mut parts = value.splitn(3, ' ');
446                  let rtcp_fb_id: u8 = parts
447                    .next()
448                    .unwrap()
449                    .parse()
450                    .map_err(|_| Error::InvalidJingle)?;
451                  if rtcp_fb_id == id {
452                    pt.rtcp_fbs.push(RtcpFb {
453                      type_: parts.next().ok_or(Error::InvalidJingle)?.into(),
454                      subtype: parts.next().map(Into::into),
455                    });
456                  }
457                }
458              }
459
460              description.payload_types.push(pt);
461            }
462          }
463
464          if md.attribute("rtcp-mux").is_some() {
465            description.rtcp_mux = Some(RtcpMux);
466          }
467
468          for extmap in md
469            .attributes
470            .iter()
471            .filter(|attribute| attribute.key == "extmap")
472          {
473            if let Some(value) = &extmap.value {
474              let mut parts = value.splitn(2, ' ');
475              let id = parts
476                .next()
477                .unwrap()
478                .parse()
479                .map_err(|_| Error::InvalidJingle)?;
480              let uri = parts.next().ok_or(Error::InvalidJingle)?;
481              description.hdrexts.push(RtpHdrext {
482                id,
483                uri: uri.into(),
484                senders: RtpHdrextSenders::Both,
485              });
486            }
487          }
488
489          let mut transport = IceUdpTransport::new();
490
491          if let Some(maybe_ufrag) = md.attribute("ice-ufrag") {
492            transport.ufrag = maybe_ufrag.map(Into::into);
493          }
494
495          if let Some(maybe_pwd) = md.attribute("ice-pwd") {
496            transport.pwd = maybe_pwd.map(Into::into);
497          }
498
499          if let Some(maybe_fingerprint) = md.attribute("fingerprint") {
500            transport.fingerprint = maybe_fingerprint
501              .and_then(|fingerprint| {
502                fingerprint.split_once(' ').map(|(hash, value)| {
503                  Fingerprint::from_colon_separated_hex(
504                    match md
505                      .attribute("setup")
506                      .flatten()
507                      .ok_or(Error::InvalidJingle)?
508                    {
509                      "passive" => Setup::Passive,
510                      "active" => Setup::Active,
511                      "actpass" => Setup::Actpass,
512                      _ => return Err(Error::InvalidJingle),
513                    },
514                    hash,
515                    value,
516                  )
517                  .map_err(|_| Error::InvalidJingle)
518                })
519              })
520              .transpose()?;
521          }
522
523          entry.insert(
524            Content::new(Creator::Responder, ContentId(md.media_name.media.clone()))
525              .with_senders(if md.attribute("sendrecv").is_some() {
526                Senders::Both
527              }
528              else if md.attribute("sendonly").is_some() {
529                Senders::Responder
530              }
531              else if md.attribute("recvonly").is_some() {
532                Senders::Initiator
533              }
534              else {
535                Senders::None
536              })
537              .with_description(description)
538              .with_transport(transport),
539          )
540        },
541      };
542
543      if let Some(Description::Rtp(description)) = &mut content.description {
544        let mut ssrcs: HashMap<u32, Vec<SourceParameter>> = HashMap::new();
545
546        for ssrc in md
547          .attributes
548          .iter()
549          .filter(|attribute| attribute.key == "ssrc")
550        {
551          if let Some(value) = &ssrc.value {
552            let mut parts = value.splitn(2, ' ');
553            let parameters = ssrcs
554              .entry(
555                parts
556                  .next()
557                  .unwrap()
558                  .parse()
559                  .map_err(|_| Error::InvalidJingle)?,
560              )
561              .or_default();
562            if let Some(parameter) = parts.next() {
563              let mut parts = parameter.splitn(2, ':');
564              parameters.push(SourceParameter {
565                name: parts.next().unwrap().into(),
566                value: parts.next().map(Into::into),
567              });
568            }
569          }
570        }
571
572        for parameters in ssrcs.values_mut() {
573          if !parameters.iter().any(|parameter| parameter.name == "msid") {
574            if let Some(msid) = md
575              .attributes
576              .iter()
577              .find(|attribute| attribute.key == "msid")
578            {
579              parameters.push(SourceParameter {
580                name: "msid".into(),
581                value: msid.value.clone(),
582              });
583            }
584          }
585        }
586
587        description
588          .ssrcs
589          .extend(ssrcs.into_iter().map(|(id, parameters)| Source {
590            id,
591            parameters,
592            info: None,
593          }));
594
595        for ssrc_group in md
596          .attributes
597          .iter()
598          .filter(|attribute| attribute.key == "ssrc-group")
599        {
600          if let Some(value) = &ssrc_group.value {
601            let mut parts = value.split(' ');
602            description.ssrc_groups.push(SourceGroup {
603              semantics: parts
604                .next()
605                .unwrap()
606                .parse()
607                .map_err(|_| Error::InvalidJingle)?,
608              sources: parts
609                .map(|id| {
610                  Ok(Source {
611                    id: id.parse().map_err(|_| Error::InvalidJingle)?,
612                    parameters: vec![],
613                    info: None,
614                  })
615                })
616                .collect::<Result<_>>()?,
617            });
618          }
619        }
620      }
621    }
622
623    if let Some(group) = self.attribute("group") {
624      let mut parts = group.splitn(2, ' ');
625      jingle.group = Some(Group {
626        semantics: parts
627          .next()
628          .unwrap()
629          .parse()
630          .map_err(|_| Error::InvalidJingle)?,
631        // TODO
632        contents: contents
633          .keys()
634          .map(|&name| GroupContent {
635            name: ContentId(name.into()),
636          })
637          .collect(),
638      });
639    }
640
641    jingle.contents = contents.into_values().collect();
642
643    Ok(jingle)
644  }
645
646  fn add_sources_from_jingle(&mut self, jingle: &Jingle) -> Result<()> {
647    let mut mid = self.media_descriptions.len();
648    for content in &jingle.contents {
649      if let Some(Description::Rtp(description)) = &content.description {
650        if let Some(template_md) = self
651          .media_descriptions
652          .iter()
653          .find(|md| md.media_name.media == description.media)
654        {
655          let mut template_md = template_md.clone();
656          template_md.attributes.retain(|attribute| {
657            !["ssrc", "ssrc-group", "mid", "msid", "sendrecv", "sendonly"]
658              .contains(&&*attribute.key)
659          });
660
661          let sources_by_group = group_sources(&description);
662          for (maybe_group, sources) in sources_by_group {
663            let mut md = template_md.clone();
664
665            md = md.with_value_attribute("mid".into(), mid.to_string());
666            md = populate_media_description_from_sources(md, &sources, maybe_group);
667
668            self.media_descriptions.push(md);
669            mid += 1;
670          }
671        }
672      }
673    }
674    Ok(())
675  }
676
677  fn remove_sources_from_jingle(&mut self, jingle: &Jingle) -> Result<()> {
678    unimplemented!()
679  }
680}