Skip to content

Commit

Permalink
sdp: simulcast: Properly parse and set 'a=simulcast:'
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
haaspors committed Jan 9, 2024
1 parent fb59f4b commit 7c15348
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 28 deletions.
1 change: 1 addition & 0 deletions webrtc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
146 changes: 130 additions & 16 deletions webrtc/src/peer_connection/sdp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -239,16 +239,49 @@ pub(crate) fn track_details_from_sdp(
incoming_tracks
}

pub(crate) fn get_rids(media: &MediaDescription) -> HashMap<String, String> {
pub(crate) fn get_rids(media: &MediaDescription) -> HashMap<String, SimulcastRid> {
let mut rids = HashMap::new();
let mut simulcast_attr: Option<String> = 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
}

Expand Down Expand Up @@ -515,18 +548,40 @@ pub(crate) async fn add_transceiver_sdp(
}

if !media_section.rid_map.is_empty() {
let mut recv_rids: Vec<String> = 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<String> = vec![];
let mut send_sc_list: Vec<String> = 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 {
Expand Down Expand Up @@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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<Arc<RTCRtpTransceiver>>,
pub(crate) data: bool,
pub(crate) rid_map: HashMap<String, String>,
pub(crate) rid_map: HashMap<String, SimulcastRid>,
pub(crate) offered_direction: Option<RTCRtpTransceiverDirection>,
}

Expand Down
53 changes: 41 additions & 12 deletions webrtc/src/peer_connection/sdp/sdp_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 7c15348

Please sign in to comment.