//! Strategy that amends the [`Rtc`] via SDP OFFER/ANSWER negotiation.
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::slice::Iter;
use crate::Rtc;
use crate::RtcError;
use crate::channel::ChannelId;
use crate::crypto::Fingerprint;
use crate::format::CodecConfig;
use crate::format::PayloadParams;
use crate::media::{Media, Rids, Simulcast};
use crate::packet::MediaKind;
use crate::rtp_::MidRid;
use crate::rtp_::Rid;
use crate::rtp_::{Direction, Extension, ExtensionMap, Mid, Pt, Ssrc};
use crate::sctp::ChannelConfig;
use crate::sctp::RtcSctp;
use crate::sdp::{self, MediaAttribute, MediaLine, MediaType, Msid, Sdp};
use crate::sdp::{Proto, SessionAttribute, Setup, SimulcastGroups};
use crate::session::Session;
use crate::{Candidate, IceCreds};
use str0m_proto::Id;
pub use crate::sdp::{SdpAnswer, SdpOffer};
use crate::streams::{DEFAULT_RTX_CACHE_DURATION, DEFAULT_RTX_RATIO_CAP, Streams};
/// Changes to the Rtc via SDP Offer/Answer dance.
pub struct SdpApi<'a> {
rtc: &'a mut Rtc,
changes: Changes,
}
impl<'a> SdpApi<'a> {
pub(crate) fn new(rtc: &'a mut Rtc) -> Self {
SdpApi {
rtc,
changes: Changes::default(),
}
}
/// Accept an [`SdpOffer`] from the remote peer. If this call returns successfully, the
/// changes will have been made to the session. The resulting [`SdpAnswer`] should be
/// sent to the remote peer.
///
/// Note. Pending changes from a previous non-completed [`SdpApi`][super::SdpApi] will be
/// considered rolled back when calling this function.
///
/// The incoming SDP is validated in various ways which can cause this call to fail.
/// Example of such problems would be an SDP without any m-lines, missing `a=fingerprint`
/// or if `a=group` doesn't match the number of m-lines.
///
/// ```no_run
/// # use std::time::Instant;
/// # use str0m::Rtc;
/// # use str0m::change::{SdpOffer};
/// // obtain offer from remote peer.
/// let json_offer: &[u8] = todo!();
/// let offer: SdpOffer = serde_json::from_slice(json_offer).unwrap();
///
/// let mut rtc = Rtc::new(Instant::now());
/// let answer = rtc.sdp_api().accept_offer(offer).unwrap();
///
/// // send json_answer to remote peer.
/// let json_answer = serde_json::to_vec(&answer).unwrap();
/// ```
pub fn accept_offer(self, offer: SdpOffer) -> Result {
debug!("Accept offer");
// Invalidate any outstanding PendingOffer.
self.rtc.next_change_id();
if offer.media_lines.is_empty() {
return Err(RtcError::RemoteSdp("No m-lines in offer".into()));
}
if self.rtc.ice.ice_lite() && offer.session.ice_lite() {
return Err(RtcError::RemoteSdp(
"Both peers being ICE-Lite not supported".into(),
));
}
add_ice_details(self.rtc, &offer, None)?;
if self.rtc.remote_fingerprint.is_none() {
if let Some(f) = offer.fingerprint() {
self.rtc.remote_fingerprint = Some(f);
} else {
self.rtc.disconnect();
return Err(RtcError::RemoteSdp("missing a=fingerprint".into()));
}
}
if !self.rtc.dtls.is_inited() {
// The side that makes the first offer is the controlling side, unless they
// are ICE Lite, in which case the roles are reversed (see RFC 5245).
self.rtc.ice.set_controlling(offer.session.ice_lite());
}
// Ensure setup=active/passive is corresponding remote and init dtls.
init_dtls(self.rtc, &offer)?;
let remote_max_message_size = extract_max_message_size(offer.media_lines.iter());
// Extract a=sctp-init from remote offer before apply_offer consumes it.
let remote_sctp_init = offer.sctp_init().map(|v| v.to_owned());
let has_snap = process_remote_sctp_init(&mut self.rtc.sctp, remote_sctp_init.as_deref())?;
// Modify session with offer.
apply_offer(&mut self.rtc.session, offer)?;
// Handle potentially new m=application line.
let client = self.rtc.dtls.is_active().expect("DTLS active to be set");
// Generate local sctp-init for the answer:
// When the remote included a=sctp-init, we reciprocate (§5.4).
if has_snap && !self.rtc.sctp.is_inited() && !self.rtc.sctp.ensure_local_snap_init() {
warn!("Failed to generate SNAP INIT chunk, degrading to non-SNAP");
}
if self.rtc.session.app().is_some() {
let init_data = self.rtc.sctp.build_snap_init_data();
self.rtc
.try_init_sctp(client, init_data, remote_max_message_size)?;
}
let params = AsSdpParams::new(self.rtc, None);
let sdp = as_sdp(&self.rtc.session, params);
debug!("Create answer");
Ok(sdp.into())
}
/// Accept an answer to a previously created [`SdpOffer`].
///
/// This function returns an [`RtcError::ChangesOutOfOrder`] if we have created and applied another
/// [`SdpApi`][super::SdpApi] before calling this. The same also happens if we use
/// [`SdpApi::accept_offer()`] before using this pending instance.
///
/// ```no_run
/// # use std::time::Instant;
/// # use str0m::Rtc;
/// # use str0m::media::{MediaKind, Direction};
/// # use str0m::change::SdpAnswer;
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
/// let (offer, pending) = changes.apply().unwrap();
///
/// // send offer to remote peer, receive answer back
/// let answer: SdpAnswer = todo!();
///
/// rtc.sdp_api().accept_answer(pending, answer).unwrap();
/// ```
pub fn accept_answer(
self,
mut pending: SdpPendingOffer,
answer: SdpAnswer,
) -> Result<(), RtcError> {
debug!("Accept answer");
// Ensure we don't use the wrong changes below. We must use that of pending.
drop(self.changes);
if !self.rtc.is_correct_change_id(pending.change_id) {
return Err(RtcError::ChangesOutOfOrder);
}
if self.rtc.ice.ice_lite() && answer.session.ice_lite() {
return Err(RtcError::RemoteSdp(
"Both peers being ICE-Lite not supported".into(),
));
}
add_ice_details(self.rtc, &answer, Some(&pending))?;
// Ensure setup=active/passive is corresponding remote and init dtls.
init_dtls(self.rtc, &answer)?;
if self.rtc.remote_fingerprint.is_none() {
if let Some(f) = answer.fingerprint() {
self.rtc.remote_fingerprint = Some(f);
} else {
self.rtc.disconnect();
return Err(RtcError::RemoteSdp("missing a=fingerprint".into()));
}
}
// Extract a=sctp-init from remote answer before apply_answer consumes it.
let remote_sctp_init = answer.sctp_init().map(|v| v.to_owned());
let expected_snap_answer =
!self.rtc.sctp.is_inited() && self.rtc.sctp.local_sctp_init_for_sdp().is_some();
// Validate or cache the remote value before mutating the session. This
// keeps a bad re-offer/re-answer from partially applying local state.
let has_snap = process_remote_sctp_init(&mut self.rtc.sctp, remote_sctp_init.as_deref())?;
// Split out new channels, since that is not handled by the Session.
let new_channels = pending.changes.take_new_channels();
let remote_max_message_size = extract_max_message_size(answer.media_lines.iter());
// Modify session with answer
apply_answer(&mut self.rtc.session, pending.changes, answer)?;
// Handle potentially new m=application line.
let client = self.rtc.dtls.is_active().expect("DTLS to be inited");
if expected_snap_answer && !has_snap {
debug!("Remote answer did not accept SNAP, falling back to regular SCTP handshake");
self.rtc.sctp.disable_pending_snap();
}
if self.rtc.session.app().is_some() {
let init_data = self.rtc.sctp.build_snap_init_data();
self.rtc
.try_init_sctp(client, init_data, remote_max_message_size)?;
}
for (id, config) in new_channels {
self.rtc.chan.confirm(id, config);
}
Ok(())
}
/// Test if any changes have been made.
///
/// If changes have been made, nothing happens until we call [`SdpApi::apply()`].
///
/// ```
/// # #[cfg(feature = "openssl")] {
/// # use std::time::Instant;
/// # use str0m::{Rtc, media::MediaKind, media::Direction};
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
/// assert!(!changes.has_changes());
///
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None);
/// assert!(changes.has_changes());
/// # }
/// ```
pub fn has_changes(&self) -> bool {
!self.changes.0.is_empty()
}
/// Add audio or video media and get the `mid` that will be used.
///
/// Each call will result in a new m-line in the offer identified by the [`Mid`].
///
/// The mid is not valid to use until the SDP offer-answer dance is complete and
/// the mid been advertised via [`Event::MediaAdded`][crate::Event::MediaAdded].
///
/// * `stream_id` is used to synchronize media. It is `a=msid-semantic: WMS ` line in SDP.
/// * `track_id` is becomes both the track id in `a=msid ` as well as the
/// CNAME in the RTP SDES.
///
/// ```
/// # #[cfg(feature = "openssl")] {
/// # use std::time::Instant;
/// # use str0m::{Rtc, media::MediaKind, media::Direction};
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
///
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None);
/// # }
/// ```
pub fn add_media(
&mut self,
kind: MediaKind,
dir: Direction,
stream_id: Option,
track_id: Option,
simulcast: Option,
) -> Mid {
let mid = self.rtc.new_mid();
// https://www.rfc-editor.org/rfc/rfc8830
// msid-id = 1*64token-char
fn is_token_char(c: &char) -> bool {
// token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39
// / %x41-5A / %x5E-7E
let u = *c as u32;
u == 0x21
|| (0x23..=0x27).contains(&u)
|| (0x2a..=0x2b).contains(&u)
|| (0x2d..=0x2e).contains(&u)
|| (0x30..=0x39).contains(&u)
|| (0x41..=0x5a).contains(&u)
|| (0x5e..0x7e).contains(&u)
}
let stream_id = if let Some(stream_id) = stream_id {
stream_id.chars().filter(is_token_char).take(64).collect()
} else {
Id::<20>::random().to_string()
};
let track_id = if let Some(track_id) = track_id {
track_id.chars().filter(is_token_char).take(64).collect()
} else {
Id::<20>::random().to_string()
};
let mut ssrcs = Vec::new();
// Main SSRC, not counting RTX.
let main_ssrc_count = simulcast.as_ref().map(|s| s.send.len()).unwrap_or(1);
for _ in 0..main_ssrc_count {
let rtx = kind.is_video().then(|| self.rtc.session.streams.new_ssrc());
ssrcs.push((self.rtc.session.streams.new_ssrc(), rtx));
}
// TODO: let user configure stream/track name.
let msid = Msid {
stream_id,
track_id: track_id.clone(),
};
let add = AddMedia {
mid,
cname: track_id,
msid,
kind,
dir,
ssrcs,
simulcast,
// Added later
pts: vec![],
exts: ExtensionMap::empty(),
index: 0,
};
self.changes.0.push(Change::AddMedia(add));
mid
}
/// Change the direction of an already existing media.
///
/// All media have a direction. The media can be added by this side via
/// [`SdpApi::add_media()`] or by the remote peer. Either way, the direction
/// of the line can be changed at any time.
///
/// It's possible to set the direction [`Direction::Inactive`] for media that
/// will not be used by the session anymore.
///
/// If the direction is set for media that doesn't exist, or if the direction is
/// the same that's already set [`SdpApi::apply()`] not require a negotiation.
pub fn set_direction(&mut self, mid: Mid, dir: Direction) {
let changed = self.rtc.session.set_direction(mid, dir);
if changed {
self.changes.0.push(Change::Direction(mid, dir));
}
}
/// Stop an already existing media.
///
/// The next generated offer emits the m-line with port 0 and excludes
/// it from the BUNDLE group, per [RFC 8843] §7.5.3. The remote
/// transceiver transitions to the "stopped" state and the m-line slot
/// becomes eligible for recycling.
///
/// Unlike [`SdpApi::set_direction()`] with [`Direction::Inactive`],
/// a stopped m-line cannot be reactivated.
///
/// If the media doesn't exist, or is already stopped, [`SdpApi::apply()`]
/// will not require a negotiation.
///
/// [RFC 8843]: https://datatracker.ietf.org/doc/html/rfc8843#section-7.5.3
pub fn stop_media(&mut self, mid: Mid) {
let changed = self.rtc.session.stop_media(mid);
if changed {
self.changes
.0
.push(Change::Direction(mid, Direction::Inactive));
}
}
/// Add a new reliable ordered data channel and get the `id` that will be used.
///
/// Use `add_channel_with_config` when unreliable or unordered data channels are preferred.
///
/// The first ever data channel added to a WebRTC session results in a media
/// of a special "application" type in the SDP. The m-line is for a SCTP association over
/// DTLS, and all data channels are multiplexed over this single association.
///
/// That means only the first ever `add_channel` will result in an [`SdpOffer`].
/// Consecutive channels will be opened without needing a negotiation.
///
/// The label is used to identify the data channel to the remote peer. This is mostly
/// useful when multiple channels are in use at the same time.
///
/// ```
/// # #[cfg(feature = "openssl")] {
/// # use std::time::Instant;
/// # use str0m::Rtc;
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
///
/// let cid = changes.add_channel("my special channel".to_string());
/// # }
/// ```
pub fn add_channel(&mut self, label: String) -> ChannelId {
self.add_channel_with_config(ChannelConfig {
label,
..Default::default()
})
}
/// Add a new data channel with a given configuration and get the `id` that will be used.
///
/// Refer to `add_channel` for more details.
///
/// ```
/// # #[cfg(feature = "openssl")] {
/// # use std::time::Instant;
/// # use str0m::{channel::{ChannelConfig, Reliability}, Rtc};
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
///
/// let cid = changes.add_channel_with_config(ChannelConfig {
/// label: "my special channel".to_string(),
/// reliability: Reliability::MaxRetransmits{ retransmits: 0 },
/// ordered: false,
/// ..Default::default()
/// });
/// # }
/// ```
pub fn add_channel_with_config(&mut self, config: ChannelConfig) -> ChannelId {
let has_media = self.rtc.session.app().is_some();
let changes_contains_add_app = self.changes.contains_add_app();
if !has_media && !changes_contains_add_app {
let mid = self.rtc.new_mid();
self.changes.0.push(Change::AddApp(mid));
}
let id = self.rtc.chan.new_channel(&config);
self.changes.0.push(Change::AddChannel((id, config)));
id
}
/// Perform an ICE restart.
///
/// Only one ICE restart can be pending at the time. Calling this repeatedly removes any other
/// pending ICE restart.
///
/// The local ICE candidates can be kept as is, or be cleared out, in which case new ice
/// candidates must be added via [`Rtc::add_local_candidate`] before connectivity can be
/// re-established.
///
/// Returns the new ICE credentials that will be used going forward.
pub fn ice_restart(&mut self, keep_local_candidates: bool) -> IceCreds {
self.changes
.retain(|c| !matches!(c, Change::IceRestart(_, _)));
let new_creds = IceCreds::new();
self.changes
.push(Change::IceRestart(new_creds.clone(), keep_local_candidates));
new_creds
}
/// Attempt to apply the changes made.
///
/// If this returns [`SdpOffer`], the caller the changes are
/// not happening straight away, and the caller is expected to do a negotiation with the remote
/// peer and apply the answer using [`SdpPendingOffer`].
///
/// In case this returns `None`, there either were no changes, or the changes could be applied
/// without doing a negotiation. Specifically for additional [`SdpApi::add_channel()`]
/// after the first, there is no negotiation needed.
///
/// The [`SdpPendingOffer`] is valid until the next time we call this function, at which
/// point using it will raise an error. Using [`SdpApi::accept_offer()`] will also invalidate
/// the current [`SdpPendingOffer`].
///
/// ```
/// # #[cfg(feature = "openssl")] {
/// # use std::time::Instant;
/// # use str0m::Rtc;
/// let mut rtc = Rtc::new(Instant::now());
///
/// let changes = rtc.sdp_api();
/// assert!(changes.apply().is_none());
/// # }
/// ```
pub fn apply(self) -> Option<(SdpOffer, SdpPendingOffer)> {
if self.changes.is_empty() {
return None;
}
let change_id = self.rtc.next_change_id();
let requires_negotiation = self.changes.0.iter().any(requires_negotiation);
if requires_negotiation {
let offer = create_offer(self.rtc, &self.changes);
let pending = SdpPendingOffer {
change_id,
changes: self.changes,
};
debug!("Create offer");
Some((offer, pending))
} else {
debug!("Apply direct changes");
apply_direct_changes(self.rtc, self.changes);
None
}
}
/// Combines the modifications made in [`SdpApi`] with those in [`SdpPendingOffer`].
///
/// This function merges the changes present in [`SdpApi`] with the changes
/// in [`SdpPendingOffer`]. In result this [`SdpApi`] will incorporate modifications
/// from both the previous [`SdpPendingOffer`] and any newly added changes.
///
/// ## Example
///
/// ```no_run
/// # use std::time::Instant;
/// # use str0m::media::{Direction, MediaKind};
/// # use str0m::Rtc;
/// let mut rtc = Rtc::new(Instant::now());
/// let mut changes = rtc.sdp_api();
/// changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
/// let (_offer, pending) = changes.apply().unwrap();
///
/// let mut changes = rtc.sdp_api();
/// changes.add_media(MediaKind::Video, Direction::SendOnly, None, None, None);
/// changes.merge(pending);
///
/// // This `SdpOffer` will have changes from the first `SdpPendingChanges`
/// // and new changes from `SdpApi`
/// let (_offer, pending) = changes.apply().unwrap();
/// ```
pub fn merge(&mut self, mut pending_offer: SdpPendingOffer) {
pending_offer.retain_relevant(self.rtc);
// Prepend the original pending changes before the current SdpApi's own changes.
//
// AddMedia / AddApp entries from the pending offer carry already-allocated MIDs
// that the remote peer may have committed to specific m-line positions. They must
// appear before any new changes so that as_new_medias() assigns them the same
// indices as in the original offer.
pending_offer.changes.0.append(&mut self.changes.0);
self.changes.0 = pending_offer.changes.0;
}
}
/// Pending offer from a previous [`Rtc::sdp_api()`] call.
///
/// This allows us to accept a remote answer. No changes have been made to the session
/// before we call [`SdpApi::accept_answer()`], which means that rolling back a
/// change is as simple as dropping this instance.
///
/// ```no_run
/// # use std::time::Instant;
/// # use str0m::Rtc;
/// # use str0m::media::{MediaKind, Direction};
/// # use str0m::change::SdpAnswer;
/// let mut rtc = Rtc::new(Instant::now());
///
/// let mut changes = rtc.sdp_api();
/// let mid = changes.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None);
/// let (offer, pending) = changes.apply().unwrap();
///
/// // send offer to remote peer, receive answer back
/// let answer: SdpAnswer = todo!();
///
/// rtc.sdp_api().accept_answer(pending, answer).unwrap();
/// ```
pub struct SdpPendingOffer {
change_id: usize,
changes: Changes,
}
impl SdpPendingOffer {
/// Retains only the relevant changes in the `changes` vector based on the provided `Rtc` instance.
///
/// This function filters the vector of `Change` instances stored in the current object and retains
/// only those changes that are considered relevant with respect to the provided `Rtc` instance.
fn retain_relevant(&mut self, rtc: &Rtc) {
fn is_relevant(rtc: &Rtc, c: &Change) -> bool {
match c {
Change::AddMedia(v) => rtc.media(v.mid).is_none(),
Change::AddApp(_) => rtc.session.app().is_none(),
Change::AddChannel(v) => rtc.chan.stream_id_by_channel_id(v.0).is_none(),
Change::Direction(m, d) => {
// If mid is missing, this is not relevant.
rtc.media(*m).map(|m| m.direction() != *d).unwrap_or(false)
}
Change::IceRestart(v, _) => rtc.ice.local_credentials() != v,
}
}
self.changes.retain(|c| is_relevant(rtc, c));
}
}
#[derive(Default)]
pub(crate) struct Changes(pub Vec);
impl Changes {
/// Details of the active ICE restart, if any.
///
/// Returns the new local ICE credentials and the whether to keep local ICE candidates if an
/// ICE restart has been initiated in the offer, otherwise [`None`].
fn ice_restart(&self) -> Option<(IceCreds, bool)> {
self.iter().find_map(|c| match c {
Change::IceRestart(creds, keep_local_candidates) => {
Some((creds.clone(), *keep_local_candidates))
}
_ => None,
})
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Change {
AddMedia(AddMedia),
AddApp(Mid),
AddChannel((ChannelId, ChannelConfig)),
Direction(Mid, Direction),
IceRestart(IceCreds, bool),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AddMedia {
pub mid: Mid,
pub cname: String,
pub msid: Msid,
pub kind: MediaKind,
pub dir: Direction,
pub ssrcs: Vec<(Ssrc, Option)>,
pub simulcast: Option,
// pts and index are filled in when creating the SDP OFFER.
// The default PT order is set by the Session (BUNDLE).
// TODO: We can make this configurable here too.
pub pts: Vec,
pub exts: ExtensionMap,
pub index: usize,
}
impl Deref for Changes {
type Target = Vec;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Changes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn requires_negotiation(c: &Change) -> bool {
match c {
Change::IceRestart(_, _) => true,
Change::AddMedia(_) => true,
Change::AddApp(_) => true,
Change::AddChannel(_) => false,
Change::Direction(_, _) => true,
}
}
fn apply_direct_changes(rtc: &mut Rtc, mut changes: Changes) {
// Split out new channels, since that is not handled by the Session.
let new_channels = changes.take_new_channels();
for (id, config) in new_channels {
rtc.chan.confirm(id, config);
}
}
fn create_offer(rtc: &mut Rtc, changes: &Changes) -> SdpOffer {
if !rtc.dtls.is_inited() {
// The side that makes the first offer is the controlling side, unless they
// are ICE Lite, in which case the roles are reversed (see RFC 5245).
rtc.ice.set_controlling(!rtc.ice.ice_lite());
}
// Generate local sctp-init for SNAP if enabled and we have/will have an app m-line.
if rtc.sctp.snap_enabled()
&& (rtc.session.app().is_some() || changes.contains_add_app())
&& !rtc.sctp.is_inited()
&& !rtc.sctp.ensure_local_snap_init()
{
warn!("Failed to generate SNAP INIT chunk, degrading to non-SNAP");
}
let params = AsSdpParams::new(rtc, Some(changes));
let sdp = as_sdp(&rtc.session, params);
sdp.into()
}
fn add_ice_details(
rtc: &mut Rtc,
sdp: &Sdp,
pending: Option<&SdpPendingOffer>,
) -> Result<(), RtcError> {
let Some(creds) = sdp.ice_creds() else {
return Err(RtcError::RemoteSdp("missing a=ice-ufrag/pwd".into()));
};
// If we are handling an **offer** from the remote, differing ICE credentials indicate an ICE
// restart initiated by the remote.
//
// If we are handling an **answer** from the remote, differing ICE credentials indicate an
// acceptance of an ICE restart we requested.
let ice_restart = match rtc.ice.remote_credentials() {
Some(v) => *v != creds,
None => false,
};
if ice_restart {
let (new_local_creds, keep_local_candidates) = if let Some(pending) = pending {
// Since we have a pending, this is an answer to our offer.
pending.changes.ice_restart().ok_or_else(||
// Answer contained changed remote creds, indicating an ice restart
// but since we have no pending ice-creds, we didn't initiate it
// Ice restart in an ANSWER breaks spec.
RtcError::RemoteSdp(
"Ice restart in answer without one in the preceeding offer".into(),
))?
} else {
// The remote OFFER had an ice restart, and we need to respond with
// new credentials in the ANSWER.
(IceCreds::new(), true)
};
rtc.ice
.ice_restart(new_local_creds.clone(), keep_local_candidates);
}
rtc.ice.set_remote_credentials(creds);
for r in sdp.ice_candidates() {
rtc.ice.add_remote_candidate(r.clone());
}
Ok(())
}
fn init_dtls(rtc: &mut Rtc, remote_sdp: &Sdp) -> Result<(), RtcError> {
let setup = match remote_sdp.setup() {
Some(v) => match v {
// Remote being ActPass, we take Passive role.
Setup::ActPass => Setup::Passive,
_ => v.invert(),
},
None => {
warn!("Missing a=setup line");
Setup::Passive
}
};
let active = setup == Setup::Active;
rtc.init_dtls(active)?;
Ok(())
}
/// Shared logic for processing a remote `a=sctp-init` attribute from an offer or answer.
///
/// Returns `true` if this is a new SNAP negotiation (remote init was accepted),
/// `false` otherwise.
///
/// The remote init bytes are stored in `RtcSctp.snap_init` for §5.6
/// re-offer validation.
///
/// When the remote includes `a=sctp-init`, we always accept and reciprocate —
/// Section 5.4 of draft-hancke-tsvwg-snap says the answerer MAY include the
/// attribute, and doing so is beneficial.
///
/// When an initial answer omits or rejects `a=sctp-init`, callers are expected
/// to fall back to a regular SCTP handshake.
fn process_remote_sctp_init(
sctp: &mut RtcSctp,
remote_init: Option<&str>,
) -> Result {
if let Some(remote_init_str) = remote_init {
if sctp.is_inited() {
// §5.6: SCTP already established — remote MUST re-send the
// same sctp-init value on subsequent offers/answers.
match sctp.snap_remote_init_string() {
Some(cached) if cached == remote_init_str => {
debug!("Remote re-sent expected a=sctp-init for established association");
}
Some(_) => {
return Err(RtcError::RemoteSdp(
"Changed a=sctp-init for existing SCTP association".into(),
));
}
None => {
// SCTP was established without SNAP but the remote is now
// sending a=sctp-init. We can't transition to SNAP
// mid-session, so just ignore it.
debug!("Ignoring a=sctp-init for non-SNAP established SCTP association");
}
}
Ok(false)
} else if sctp.set_remote_snap_init_string(remote_init_str) {
Ok(true)
} else {
debug!("Ignoring malformed a=sctp-init");
Ok(false)
}
} else if sctp.is_snap_established() {
Err(RtcError::RemoteSdp(
"Missing a=sctp-init for established SNAP SCTP association".into(),
))
} else {
Ok(false)
}
}
fn as_sdp(session: &Session, params: AsSdpParams) -> Sdp {
let (media_lines, mids, stream_ids) = {
let mut v = as_media_lines(session);
let mut new_lines = vec![];
// When creating new m-lines from the pending changes, the m-line index starts from this.
let new_index_start = v.len();
// If there are additions in the pending changes, prepend them now.
if let Some(pending) = params.pending {
new_lines = pending
.as_new_medias(new_index_start, &session.codec_config, &session.exts)
.collect();
}
// Add potentially new m-lines to the existing ones.
v.extend(new_lines.iter().map(|n| n as &dyn AsSdpMediaLine));
// Turn into sdp::MediaLine (m-line).
let mut lines = v
.iter()
.map(|m| {
// Candidates should only be in the first BUNDLE mid
let include_candidates = m.index() == 0;
let attrs = params.media_attributes(include_candidates);
// Already made send stream SSRCs
let mut ssrcs = session.streams.ssrcs_tx(m.mid());
// Merged with pending stream SSRCs
if let Some(pending) = params.pending {
ssrcs.extend(pending.ssrcs_for_mid(m.mid()))
}
let params: Vec<_> = session
.codec_config
.all_for_kind(m.kind())
.cloned()
.collect();
m.as_media_line(attrs, &ssrcs, &session.exts, ¶ms)
})
.collect::>();
// Use the same local limit for SDP and the SCTP reassembly policy.
for line in &mut lines {
if line.typ.is_channel() {
line.attrs
.retain(|a| !matches!(a, MediaAttribute::MaxMessageSize(_)));
line.attrs.push(MediaAttribute::MaxMessageSize(
params.local_max_message_size as usize,
));
}
}
// Add a=sctp-init to the application m-line if SNAP is configured.
if let Some(sctp_init) = ¶ms.local_sctp_init {
for line in &mut lines {
if line.typ.is_channel() {
line.attrs
.push(sdp::MediaAttribute::SctpInit(sctp_init.clone()));
}
}
}
if let Some(pending) = params.pending {
pending.apply_to(&mut lines);
}
// Mids go into the session part of the SDP.
// Rejected (disabled) m-lines must not be part of the BUNDLE group.
let mids = lines
.iter()
.filter(|l| !l.disabled)
.map(|l| l.mid())
.collect();
let mut stream_ids = vec![];
for msid in v.iter().filter_map(|v| v.msid()) {
if !stream_ids.contains(&msid.stream_id) {
stream_ids.push(msid.stream_id.clone());
}
}
(lines, mids, stream_ids)
};
// AllowMixedExts adds "a=extmap-allow-mixed" at session level to signal
// support for mixing one-byte and two-byte RTP header extensions.
// TODO: It would make sense to perform an actual negotiation, however
// just adding this line should work fine:
// https://github.com/meetecho/janus-gateway/blob/d2e74fdf9bb8aa7a39ed68ed28394afe1e0cd22d/src/sdp.c#L1519
let mut attrs = vec![
SessionAttribute::Group {
typ: "BUNDLE".into(),
mids,
},
SessionAttribute::AllowMixedExts,
SessionAttribute::MsidSemantic {
semantic: "WMS".to_string(),
stream_ids,
},
];
if session.ice_lite {
attrs.push(SessionAttribute::IceLite);
}
Sdp {
session: sdp::Session {
id: session.id(),
bw: None,
attrs,
},
media_lines,
}
}
fn apply_offer(session: &mut Session, offer: SdpOffer) -> Result<(), RtcError> {
offer.assert_consistency()?;
update_session(session, &offer);
let bundle_mids = offer.bundle_mids();
let new_lines = sync_medias(session, &offer, true).map_err(RtcError::RemoteSdp)?;
add_new_lines(session, &new_lines, true, bundle_mids).map_err(RtcError::RemoteSdp)?;
ensure_stream_tx(session);
Ok(())
}
fn apply_answer(
session: &mut Session,
pending: Changes,
answer: SdpAnswer,
) -> Result<(), RtcError> {
answer.assert_consistency()?;
update_session(session, &answer);
let bundle_mids = answer.bundle_mids();
let new_lines = sync_medias(session, &answer, false).map_err(RtcError::RemoteSdp)?;
// The new_lines from the answer must correspond to what we sent in the offer.
if let Some(err) = pending.ensure_correct_answer(&new_lines) {
return Err(RtcError::RemoteSdp(err));
}
add_new_lines(session, &new_lines, false, bundle_mids).map_err(RtcError::RemoteSdp)?;
// Add all pending changes (since we pre-allocated SSRC communicated in the Offer).
add_pending_changes(session, pending);
ensure_stream_tx(session);
Ok(())
}
fn ensure_stream_tx(session: &mut Session) {
for media in &session.medias {
// Only make send streams when we have to.
if !media.direction().is_sending() {
continue;
}
let mut rids: Vec