Skip to content

Commit 425a851

Browse files
kc5nraalgesten
authored andcommitted
Add simulcast send support
1 parent a7b5e60 commit 425a851

24 files changed

+354
-148
lines changed

examples/chat.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,13 @@ impl Client {
495495
if let TrackOutState::ToOpen = track.state {
496496
if let Some(track_in) = track.track_in.upgrade() {
497497
let stream_id = track_in.origin.to_string();
498-
let mid =
499-
change.add_media(track_in.kind, Direction::SendOnly, Some(stream_id), None);
498+
let mid = change.add_media(
499+
track_in.kind,
500+
Direction::SendOnly,
501+
Some(stream_id),
502+
None,
503+
None,
504+
);
500505
track.state = TrackOutState::Negotiating(mid);
501506
}
502507
}

src/change/direct.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,19 @@ impl<'a> DirectApi<'a> {
232232
mid: Mid,
233233
rid: Option<Rid>,
234234
) -> &mut StreamTx {
235-
let Some(media) = self.rtc.session.media_by_mid(mid) else {
235+
let Some(media) = self.rtc.session.media_by_mid_mut(mid) else {
236236
panic!("No media declared for mid: {}", mid);
237237
};
238238

239239
let is_audio = media.kind().is_audio();
240240

241241
let midrid = MidRid(mid, rid);
242242

243+
// If there is a RID tx, declare it so we an use it in Writer API
244+
if let Some(rid) = rid {
245+
media.add_to_rid_tx(rid);
246+
}
247+
243248
let stream = self
244249
.rtc
245250
.session

src/change/sdp.rs

+147-58
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::crypto::Fingerprint;
88
use crate::format::CodecConfig;
99
use crate::format::PayloadParams;
1010
use crate::io::Id;
11-
use crate::media::Media;
11+
use crate::media::{Media, Simulcast};
1212
use crate::packet::MediaKind;
1313
use crate::rtp_::MidRid;
1414
use crate::rtp_::Rid;
@@ -128,7 +128,7 @@ impl<'a> SdpApi<'a> {
128128
/// let mut rtc = Rtc::new();
129129
///
130130
/// let mut changes = rtc.sdp_api();
131-
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None);
131+
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
132132
/// let (offer, pending) = changes.apply().unwrap();
133133
///
134134
/// // send offer to remote peer, receive answer back
@@ -201,7 +201,7 @@ impl<'a> SdpApi<'a> {
201201
/// let mut changes = rtc.sdp_api();
202202
/// assert!(!changes.has_changes());
203203
///
204-
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None);
204+
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None);
205205
/// assert!(changes.has_changes());
206206
/// # }
207207
/// ```
@@ -227,7 +227,7 @@ impl<'a> SdpApi<'a> {
227227
///
228228
/// let mut changes = rtc.sdp_api();
229229
///
230-
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None);
230+
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None);
231231
/// # }
232232
/// ```
233233
pub fn add_media(
@@ -236,6 +236,7 @@ impl<'a> SdpApi<'a> {
236236
dir: Direction,
237237
stream_id: Option<String>,
238238
track_id: Option<String>,
239+
simulcast: Option<crate::media::Simulcast>,
239240
) -> Mid {
240241
let mid = self.rtc.new_mid();
241242

@@ -266,8 +267,15 @@ impl<'a> SdpApi<'a> {
266267
Id::<20>::random().to_string()
267268
};
268269

269-
let rtx = kind.is_video().then(|| self.rtc.session.streams.new_ssrc());
270-
let ssrcs = vec![(self.rtc.session.streams.new_ssrc(), rtx)];
270+
let mut ssrcs = Vec::new();
271+
272+
// Main SSRC, not counting RTX.
273+
let main_ssrc_count = simulcast.as_ref().map(|s| s.send.len()).unwrap_or(1);
274+
275+
for _ in 0..main_ssrc_count {
276+
let rtx = kind.is_video().then(|| self.rtc.session.streams.new_ssrc());
277+
ssrcs.push((self.rtc.session.streams.new_ssrc(), rtx));
278+
}
271279

272280
// TODO: let user configure stream/track name.
273281
let msid = Msid {
@@ -282,6 +290,7 @@ impl<'a> SdpApi<'a> {
282290
kind,
283291
dir,
284292
ssrcs,
293+
simulcast,
285294

286295
// Added later
287296
pts: vec![],
@@ -459,11 +468,11 @@ impl<'a> SdpApi<'a> {
459468
/// # use str0m::Rtc;
460469
/// let mut rtc = Rtc::new();
461470
/// let mut changes = rtc.sdp_api();
462-
/// changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None);
471+
/// changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
463472
/// let (_offer, pending) = changes.apply().unwrap();
464473
///
465474
/// let mut changes = rtc.sdp_api();
466-
/// changes.add_media(MediaKind::Video, Direction::SendOnly, None, None);
475+
/// changes.add_media(MediaKind::Video, Direction::SendOnly, None, None, None);
467476
/// changes.merge(pending);
468477
///
469478
/// // This `SdpOffer` will have changes from the first `SdpPendingChanges`
@@ -489,7 +498,7 @@ impl<'a> SdpApi<'a> {
489498
/// let mut rtc = Rtc::new();
490499
///
491500
/// let mut changes = rtc.sdp_api();
492-
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None);
501+
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
493502
/// let (offer, pending) = changes.apply().unwrap();
494503
///
495504
/// // send offer to remote peer, receive answer back
@@ -561,6 +570,7 @@ pub(crate) struct AddMedia {
561570
pub kind: MediaKind,
562571
pub dir: Direction,
563572
pub ssrcs: Vec<(Ssrc, Option<Ssrc>)>,
573+
pub simulcast: Option<Simulcast>,
564574

565575
// pts and index are filled in when creating the SDP OFFER.
566576
// The default PT order is set by the Session (BUNDLE).
@@ -894,9 +904,12 @@ fn add_pending_changes(session: &mut Session, pending: Changes) {
894904
media.set_cname(add_media.cname);
895905
media.set_msid(add_media.msid);
896906

897-
for (ssrc, rtx) in add_media.ssrcs {
898-
// TODO: When we allow sending RID, we need to add that here.
899-
let midrid = MidRid(add_media.mid, None);
907+
// If there are RIDs, the SSRC order matches that of the rid order.
908+
let rids = add_media.simulcast.map(|x| x.send).unwrap_or(vec![]);
909+
910+
for (i, (ssrc, rtx)) in add_media.ssrcs.into_iter().enumerate() {
911+
let maybe_rid = rids.get(i).cloned();
912+
let midrid = MidRid(add_media.mid, maybe_rid);
900913

901914
let stream = session.streams.declare_stream_tx(ssrc, rtx, midrid);
902915

@@ -1068,8 +1081,14 @@ fn update_media(
10681081
media.set_direction(new_dir);
10691082
}
10701083

1071-
for rid in m.rids().iter() {
1072-
media.expect_rid(*rid);
1084+
if new_dir.is_sending() {
1085+
// The other side has declared how it EXPECTING to receive. We must only send
1086+
// the RIDs declared in the answer.
1087+
media.set_rid_tx(m.rids().into());
1088+
}
1089+
if new_dir.is_receiving() {
1090+
// The other side has declared what it proposes to send. We are accepting it.
1091+
media.set_rid_rx(m.rids().into());
10731092
}
10741093

10751094
// Narrowing/ordering of of PT
@@ -1102,44 +1121,47 @@ fn update_media(
11021121
}
11031122
media.set_remote_extmap(remote_extmap);
11041123

1105-
if new_dir.is_receiving() {
1106-
// SSRC changes
1107-
// This will always be for ReceiverSource since any incoming a=ssrc line will be
1108-
// about the remote side's SSRC.
1109-
let infos = m.ssrc_info();
1110-
let main = infos.iter().filter(|i| i.repairs.is_none());
1111-
1112-
if m.simulcast().is_none() {
1113-
// Only use pre-communicated SSRC if we are running without simulcast.
1114-
// We found a bug in FF where the order of the simulcast lines does not
1115-
// correspond to the order of the simulcast declarations. In this case
1116-
// it's better to fall back on mid/rid dynamic mapping.
1117-
1118-
for i in main {
1119-
// TODO: If the remote is communicating _BOTH_ rid and a=ssrc this will fail.
1120-
info!("Adding pre-communicated SSRC: {:?}", i);
1121-
let repair_ssrc = infos
1122-
.iter()
1123-
.find(|r| r.repairs == Some(i.ssrc))
1124-
.map(|r| r.ssrc);
1125-
1126-
// If remote communicated a main a=ssrc, but no RTX, we will not send nacks.
1127-
let midrid = MidRid(media.mid(), None);
1128-
let suppress_nack = repair_ssrc.is_none();
1129-
streams.expect_stream_rx(i.ssrc, repair_ssrc, midrid, suppress_nack);
1130-
}
1131-
}
1124+
// SSRC changes
1125+
// This will always be for ReceiverSource since any incoming a=ssrc line will be
1126+
// about the remote side's SSRC.
1127+
if !new_dir.is_receiving() {
1128+
return;
1129+
}
11321130

1133-
// Simulcast configuration
1134-
if let Some(s) = m.simulcast() {
1135-
if s.is_munged {
1136-
warn!("Not supporting simulcast via munging SDP");
1137-
} else if media.simulcast().is_none() {
1138-
// Invert before setting, since it has a recv and send config.
1139-
media.set_simulcast(s.invert());
1140-
}
1131+
// Simulcast configuration
1132+
if let Some(s) = m.simulcast() {
1133+
if s.is_munged {
1134+
warn!("Not supporting simulcast via munging SDP");
1135+
} else if media.simulcast().is_none() {
1136+
// Invert before setting, since it has a recv and send config.
1137+
media.set_simulcast(s.invert());
11411138
}
11421139
}
1140+
1141+
// Only use pre-communicated SSRC if we are running without simulcast.
1142+
// We found a bug in FF where the order of the simulcast lines does not
1143+
// correspond to the order of the simulcast declarations. In this case
1144+
// it's better to fall back on mid/rid dynamic mapping.
1145+
if m.simulcast().is_some() {
1146+
return;
1147+
}
1148+
1149+
let infos = m.ssrc_info();
1150+
let main = infos.iter().filter(|i| i.repairs.is_none());
1151+
1152+
for i in main {
1153+
// TODO: If the remote is communicating _BOTH_ rid and a=ssrc this will fail.
1154+
info!("Adding pre-communicated SSRC: {:?}", i);
1155+
let repair_ssrc = infos
1156+
.iter()
1157+
.find(|r| r.repairs == Some(i.ssrc))
1158+
.map(|r| r.ssrc);
1159+
1160+
// If remote communicated a main a=ssrc, but no RTX, we will not send nacks.
1161+
let midrid = MidRid(media.mid(), None);
1162+
let suppress_nack = repair_ssrc.is_none();
1163+
streams.expect_stream_rx(i.ssrc, repair_ssrc, midrid, suppress_nack);
1164+
}
11431165
}
11441166

11451167
trait AsSdpMediaLine {
@@ -1291,18 +1313,13 @@ impl AsSdpMediaLine for Media {
12911313
}
12921314
}
12931315

1294-
let count = ssrcs_tx.len();
1295-
#[allow(clippy::comparison_chain)]
1296-
if count == 1 {
1297-
let (ssrc, ssrc_rtx) = &ssrcs_tx[0];
1316+
for (ssrc, ssrc_rtx) in ssrcs_tx {
12981317
if let Some(ssrc_rtx) = ssrc_rtx {
12991318
attrs.push(MediaAttribute::SsrcGroup {
13001319
semantics: "FID".to_string(),
13011320
ssrcs: vec![*ssrc, *ssrc_rtx],
13021321
});
13031322
}
1304-
} else {
1305-
// TODO: handle simulcast
13061323
}
13071324

13081325
MediaLine {
@@ -1549,7 +1566,10 @@ impl Change {
15491566

15501567
#[cfg(test)]
15511568
mod test {
1569+
use sdp::RestrictionId;
1570+
15521571
use crate::format::Codec;
1572+
use crate::media::Simulcast;
15531573
use crate::sdp::RtpMap;
15541574

15551575
use super::*;
@@ -1595,11 +1615,11 @@ mod test {
15951615

15961616
let mut rtc = Rtc::new();
15971617
let mut changes = rtc.sdp_api();
1598-
changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None);
1618+
changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
15991619
let (offer, pending) = changes.apply().unwrap();
16001620

16011621
let mut changes = rtc.sdp_api();
1602-
changes.add_media(MediaKind::Video, Direction::SendOnly, None, None);
1622+
changes.add_media(MediaKind::Video, Direction::SendOnly, None, None, None);
16031623
changes.merge(pending);
16041624
let (new_offer, _) = changes.apply().unwrap();
16051625

@@ -1624,7 +1644,7 @@ mod test {
16241644
.build();
16251645

16261646
let mut change1 = rtc1.sdp_api();
1627-
change1.add_media(MediaKind::Video, Direction::SendOnly, None, None);
1647+
change1.add_media(MediaKind::Video, Direction::SendOnly, None, None, None);
16281648
let (offer1, _) = change1.apply().unwrap();
16291649

16301650
let answer = rtc2.sdp_api().accept_offer(offer1).unwrap();
@@ -1652,4 +1672,73 @@ mod test {
16521672
"VP9 was not offered, so it should not be present in the answer"
16531673
);
16541674
}
1675+
1676+
#[test]
1677+
fn simulcast_ssrc_allocation() {
1678+
crate::init_crypto_default();
1679+
1680+
let mut rtc1 = Rtc::new();
1681+
1682+
let mut change = rtc1.sdp_api();
1683+
change.add_media(
1684+
MediaKind::Video,
1685+
Direction::SendOnly,
1686+
None,
1687+
None,
1688+
Some(Simulcast {
1689+
send: vec!["m".into(), "h".into(), "l".into()],
1690+
recv: vec![],
1691+
}),
1692+
);
1693+
1694+
let Change::AddMedia(am) = &change.changes[0] else {
1695+
panic!("Not AddMedia?!");
1696+
};
1697+
1698+
// these should be organized in order: m, h, l
1699+
let pending_ssrcs = am.ssrcs.clone();
1700+
assert_eq!(pending_ssrcs.len(), 3);
1701+
1702+
for p in &pending_ssrcs {
1703+
assert!(p.1.is_some()); // all should have rtx
1704+
}
1705+
1706+
let (offer, _) = change.apply().unwrap();
1707+
let sdp = offer.into_inner();
1708+
let line = &sdp.media_lines[0];
1709+
1710+
assert_eq!(
1711+
line.simulcast().unwrap().send,
1712+
SimulcastGroups(vec![
1713+
RestrictionId("m".into(), true),
1714+
RestrictionId("h".into(), true),
1715+
RestrictionId("l".into(), true),
1716+
])
1717+
);
1718+
1719+
// Each SSRC, both regular and RTX get their own a=ssrc line.
1720+
assert_eq!(line.ssrc_info().len(), pending_ssrcs.len() * 2);
1721+
1722+
let fids: Vec<_> = line
1723+
.attrs
1724+
.iter()
1725+
.filter_map(|a| {
1726+
if let MediaAttribute::SsrcGroup { semantics, ssrcs } = a {
1727+
// We don't have any other semantics right now.
1728+
assert_eq!(semantics, "FID");
1729+
assert_eq!(ssrcs.len(), 2);
1730+
Some((ssrcs[0], ssrcs[1]))
1731+
} else {
1732+
None
1733+
}
1734+
})
1735+
.collect();
1736+
1737+
assert_eq!(fids.len(), pending_ssrcs.len());
1738+
1739+
for (a, b) in fids.iter().zip(pending_ssrcs.iter()) {
1740+
assert_eq!(a.0, b.0);
1741+
assert_eq!(Some(a.1), b.1);
1742+
}
1743+
}
16551744
}

0 commit comments

Comments
 (0)