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 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 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 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}