From 7c15348c946356801944c0853cd56169fba728e9 Mon Sep 17 00:00:00 2001 From: Haakon Sporsheim Date: Tue, 9 Jan 2024 17:23:06 +0100 Subject: [PATCH] sdp: simulcast: Properly parse and set 'a=simulcast:' This improves parsing and generation of SDP for simulcast attribute according to RFC8853. Support for alternative formats are not implemented 100%. Comparing to Pion implementation, this commit at least covers more, hopefully most usecases for webrtc-rs users. --- webrtc/src/lib.rs | 1 + webrtc/src/peer_connection/sdp/mod.rs | 146 ++++++++++++++++++--- webrtc/src/peer_connection/sdp/sdp_test.rs | 53 ++++++-- 3 files changed, 172 insertions(+), 28 deletions(-) diff --git a/webrtc/src/lib.rs b/webrtc/src/lib.rs index 8eaeff247..c414d0f3d 100644 --- a/webrtc/src/lib.rs +++ b/webrtc/src/lib.rs @@ -27,6 +27,7 @@ pub(crate) const UNSPECIFIED_STR: &str = "Unspecified"; pub(crate) const RECEIVE_MTU: usize = 1460; pub(crate) const SDP_ATTRIBUTE_RID: &str = "rid"; +pub(crate) const SDP_ATTRIBUTE_SIMULCAST: &str = "simulcast"; pub(crate) const GENERATED_CERTIFICATE_ORIGIN: &str = "WebRTC"; pub(crate) const SDES_REPAIR_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"; diff --git a/webrtc/src/peer_connection/sdp/mod.rs b/webrtc/src/peer_connection/sdp/mod.rs index 28e3db15b..99036efd9 100644 --- a/webrtc/src/peer_connection/sdp/mod.rs +++ b/webrtc/src/peer_connection/sdp/mod.rs @@ -33,7 +33,7 @@ use smol_str::SmolStr; use url::Url; use crate::peer_connection::MEDIA_SECTION_APPLICATION; -use crate::SDP_ATTRIBUTE_RID; +use crate::{SDP_ATTRIBUTE_RID, SDP_ATTRIBUTE_SIMULCAST}; /// TrackDetails represents any media source that can be represented in a SDP /// This isn't keyed by SSRC because it also needs to support rid based sources @@ -239,16 +239,49 @@ pub(crate) fn track_details_from_sdp( incoming_tracks } -pub(crate) fn get_rids(media: &MediaDescription) -> HashMap { +pub(crate) fn get_rids(media: &MediaDescription) -> HashMap { let mut rids = HashMap::new(); + let mut simulcast_attr: Option = None; for attr in &media.attributes { if attr.key.as_str() == SDP_ATTRIBUTE_RID { - if let Some(value) = &attr.value { - let split: Vec<&str> = value.split(' ').collect(); - rids.insert(split[0].to_owned(), value.to_owned()); + if let Err(err) = attr + .value + .as_ref() + .ok_or(SimulcastRidParseError::SyntaxIdDirSplit) + .and_then(SimulcastRid::try_from) + .map(|rid| rids.insert(rid.id.to_owned(), rid)) + { + log::warn!("Failed to parse RID: {}", err); + } + } else if attr.key.as_str() == SDP_ATTRIBUTE_SIMULCAST { + simulcast_attr = attr.value.clone(); + } + } + + if let Some(attr) = simulcast_attr { + let mut split = attr.split(' '); + loop { + let _dir = split.next(); + let sc_str_list = split.next(); + if let Some(list) = sc_str_list { + let sc_list: Vec<&str> = list.split(';').flat_map(|alt| alt.split(',')).collect(); + for sc_id in sc_list { + let (sc_id, paused) = if let Some(sc_id) = sc_id.strip_prefix('~') { + (sc_id, true) + } else { + (sc_id, false) + }; + + if let Some(rid) = rids.get_mut(sc_id) { + rid.paused = paused; + } + } + } else { + break; } } } + rids } @@ -515,18 +548,40 @@ pub(crate) async fn add_transceiver_sdp( } if !media_section.rid_map.is_empty() { - let mut recv_rids: Vec = vec![]; - - for rid in media_section.rid_map.keys() { - media = - media.with_value_attribute(SDP_ATTRIBUTE_RID.to_owned(), rid.to_owned() + " recv"); - recv_rids.push(rid.to_owned()); + let mut recv_sc_list: Vec = vec![]; + let mut send_sc_list: Vec = vec![]; + + for rid in media_section.rid_map.values() { + let rid_syntax = match rid.direction { + SimulcastDirection::Send => { + if rid.paused { + send_sc_list.push(format!("~{}", rid.id)); + } else { + send_sc_list.push(rid.id.to_owned()); + } + format!("{} send", rid.id) + } + SimulcastDirection::Recv => { + if rid.paused { + recv_sc_list.push(format!("~{}", rid.id)); + } else { + recv_sc_list.push(rid.id.to_owned()); + } + format!("{} recv", rid.id) + } + }; + media = media.with_value_attribute(SDP_ATTRIBUTE_RID.to_owned(), rid_syntax); } + // Simulcast - media = media.with_value_attribute( - "simulcast".to_owned(), - "recv ".to_owned() + recv_rids.join(";").as_str(), - ); + let mut sc_attr = String::new(); + if !recv_sc_list.is_empty() { + sc_attr.push_str(&format!("recv {}", recv_sc_list.join(";"))); + } + if !send_sc_list.is_empty() { + sc_attr.push_str(&format!("send {}", send_sc_list.join(";"))); + } + media = media.with_value_attribute(SDP_ATTRIBUTE_SIMULCAST.to_owned(), sc_attr); } for mt in transceivers { @@ -628,12 +683,71 @@ pub(crate) async fn add_transceiver_sdp( Ok((d.with_media(media), true)) } +#[derive(thiserror::Error, Debug, PartialEq)] +pub(crate) enum SimulcastRidParseError { + /// SyntaxIdDirSplit indicates rid-syntax could not be parsed. + #[error("RFC8851 mandates rid-syntax = %s\"a=rid:\" rid-id SP rid-dir")] + SyntaxIdDirSplit, + /// UnknownDirection indicates rid-dir was not parsed. Should be "send" or "recv". + #[error("RFC8851 mandates rid-dir = %s\"send\" / %s\"recv\"")] + UnknownDirection, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum SimulcastDirection { + Send, + Recv, +} + +impl TryFrom<&str> for SimulcastDirection { + type Error = SimulcastRidParseError; + fn try_from(value: &str) -> std::result::Result { + match value.to_lowercase().as_str() { + "send" => Ok(SimulcastDirection::Send), + "recv" => Ok(SimulcastDirection::Recv), + _ => Err(SimulcastRidParseError::UnknownDirection), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct SimulcastRid { + pub(crate) id: String, + pub(crate) direction: SimulcastDirection, + pub(crate) params: String, + pub(crate) paused: bool, +} + +impl TryFrom<&String> for SimulcastRid { + type Error = SimulcastRidParseError; + fn try_from(value: &String) -> std::result::Result { + let mut split = value.split(' '); + let id = split + .next() + .ok_or(SimulcastRidParseError::SyntaxIdDirSplit)? + .to_owned(); + let direction = SimulcastDirection::try_from( + split + .next() + .ok_or(SimulcastRidParseError::SyntaxIdDirSplit)?, + )?; + let params = split.collect(); + + Ok(Self { + id, + direction, + params, + paused: false, + }) + } +} + #[derive(Default)] pub(crate) struct MediaSection { pub(crate) id: String, pub(crate) transceivers: Vec>, pub(crate) data: bool, - pub(crate) rid_map: HashMap, + pub(crate) rid_map: HashMap, pub(crate) offered_direction: Option, } diff --git a/webrtc/src/peer_connection/sdp/sdp_test.rs b/webrtc/src/peer_connection/sdp/sdp_test.rs index cd04ac234..ed551aa39 100644 --- a/webrtc/src/peer_connection/sdp/sdp_test.rs +++ b/webrtc/src/peer_connection/sdp/sdp_test.rs @@ -703,7 +703,26 @@ async fn test_populate_sdp() -> Result<()> { .await; let mut rid_map = HashMap::new(); - rid_map.insert("ridkey".to_owned(), "some".to_owned()); + let rid_id = "ridkey".to_owned(); + rid_map.insert( + rid_id.to_owned(), + SimulcastRid { + id: rid_id, + direction: SimulcastDirection::Recv, + params: "some".to_owned(), + paused: false, + }, + ); + let rid_id = "ridpaused".to_owned(); + rid_map.insert( + rid_id.to_owned(), + SimulcastRid { + id: rid_id, + direction: SimulcastDirection::Recv, + params: "some2".to_owned(), + paused: true, + }, + ); let media_sections = vec![MediaSection { id: "video".to_owned(), transceivers: vec![tr], @@ -732,23 +751,33 @@ async fn test_populate_sdp() -> Result<()> { .await?; // Test contains rid map keys - let mut found = false; + let mut found = 0; for desc in &offer_sdp.media_descriptions { if desc.media_name.media != "video" { continue; } - for a in &desc.attributes { - if a.key == SDP_ATTRIBUTE_RID { - if let Some(value) = &a.value { - if value.contains("ridkey") { - found = true; - break; - } - } - } + + let rid_map = get_rids(desc); + if let Some(rid) = rid_map.get("ridkey") { + assert!(!rid.paused, "Rid should be active"); + assert_eq!( + rid.direction, + SimulcastDirection::Recv, + "Rid should be recv" + ); + found += 1; + } + if let Some(rid) = rid_map.get("ridpaused") { + assert!(rid.paused, "Rid should be paused"); + assert_eq!( + rid.direction, + SimulcastDirection::Recv, + "Rid should be recv" + ); + found += 1; } } - assert!(found, "Rid key should be present"); + assert_eq!(found, 2, "All Rid key should be present"); } //"SetCodecPreferences"