pub mod certificate;
pub mod configuration;
pub mod event;
pub(crate) mod handler;
mod internal;
pub mod message;
pub mod sdp;
pub mod state;
pub mod transport;
use crate::data_channel::init::RTCDataChannelInit;
use crate::data_channel::parameters::DataChannelParameters;
use crate::data_channel::state::RTCDataChannelState;
use crate::data_channel::{RTCDataChannel, RTCDataChannelId, internal::RTCDataChannelInternal};
use crate::media_stream::track::MediaStreamTrack;
use crate::peer_connection::configuration::media_engine::MediaEngine;
use crate::peer_connection::configuration::setting_engine::{SctpMaxMessageSize, SettingEngine};
use crate::peer_connection::configuration::{
RTCConfiguration, RTCIceTransportPolicy,
offer_answer_options::{RTCAnswerOptions, RTCOfferOptions},
};
use crate::peer_connection::event::RTCPeerConnectionEvent;
use crate::peer_connection::handler::PipelineContext;
use crate::peer_connection::handler::dtls::DtlsHandlerContext;
use crate::peer_connection::handler::ice::IceHandlerContext;
use crate::peer_connection::handler::sctp::SctpHandlerContext;
use crate::peer_connection::sdp::MediaDescriptionExt;
use crate::peer_connection::sdp::session_description::RTCSessionDescription;
use crate::peer_connection::sdp::{
extract_fingerprint, extract_ice_details, get_application_media,
get_application_media_section_max_message_size, get_application_media_section_sctp_port,
get_mid_value, get_peer_direction, has_ice_trickle_option, is_lite_set, sdp_type::RTCSdpType,
update_sdp_origin,
};
use crate::peer_connection::state::RTCIceGatheringState;
use crate::peer_connection::state::ice_connection_state::RTCIceConnectionState;
use crate::peer_connection::state::peer_connection_state::{
NegotiationNeededState, RTCPeerConnectionState,
};
use crate::peer_connection::state::signaling_state::{RTCSignalingState, StateChangeOp};
use crate::peer_connection::transport::RTCSctpTransport;
use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint;
use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters;
use crate::peer_connection::transport::dtls::role::{
DEFAULT_DTLS_ROLE_ANSWER, DEFAULT_DTLS_ROLE_OFFER, RTCDtlsRole,
};
use crate::peer_connection::transport::dtls::{DtlsTransport, RTCDtlsTransportConfig};
use crate::peer_connection::transport::ice::IceTransport;
use crate::peer_connection::transport::ice::candidate::RTCIceCandidateInit;
use crate::peer_connection::transport::ice::parameters::RTCIceParameters;
use crate::peer_connection::transport::ice::role::RTCIceRole;
use crate::peer_connection::transport::sctp::SctpTransport;
use crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities;
use crate::rtp_transceiver::direction::RTCRtpTransceiverDirection;
use crate::rtp_transceiver::rtp_receiver::RTCRtpReceiver;
use crate::rtp_transceiver::rtp_sender::RTCRtpCodecParameters;
use crate::rtp_transceiver::rtp_sender::RTCRtpSender;
use crate::rtp_transceiver::rtp_sender::internal::RTCRtpSenderInternal;
use crate::rtp_transceiver::rtp_sender::rtp_codec::{
CodecMatch, RtpCodecKind, codec_parameters_fuzzy_search,
};
use crate::rtp_transceiver::{
RTCRtpReceiverId, RTCRtpSenderId, RTCRtpTransceiver, RTCRtpTransceiverId,
RTCRtpTransceiverInit, internal::RTCRtpTransceiverInternal,
};
use crate::statistics::StatsSelector;
use crate::statistics::accumulator::RTCStatsAccumulator;
use crate::statistics::report::RTCStatsReport;
use ::sdp::description::session::Origin;
use ::sdp::util::ConnectionRole;
use ice::AgentConfig;
use ice::candidate::{Candidate, unmarshal_candidate};
use interceptor::{Interceptor, Registry};
use shared::error::{Error, Result};
use shared::util::math_rand_alpha;
use std::collections::HashMap;
use std::time::Instant;
#[derive(Default)]
pub struct RTCPeerConnectionBuilder {
configuration: RTCConfiguration,
media_engine: MediaEngine,
setting_engine: SettingEngine,
interceptor_registry: Registry,
}
impl RTCPeerConnectionBuilder {
pub fn new() -> Self {
Self::default()
}
}
impl RTCPeerConnectionBuilder {
pub fn with_configuration(mut self, configuration: RTCConfiguration) -> Self {
self.configuration = configuration;
self
}
pub fn with_media_engine(mut self, media_engine: MediaEngine) -> Self {
self.media_engine = media_engine;
self
}
pub fn with_setting_engine(mut self, setting_engine: SettingEngine) -> Self {
self.setting_engine = setting_engine;
self
}
pub fn with_interceptor_registry(mut self, interceptor_registry: Registry) -> Self {
self.interceptor_registry = interceptor_registry;
self
}
pub fn build(self, now: Instant) -> Result<RTCPeerConnection> {
RTCPeerConnection::new(
now,
self.configuration,
self.media_engine,
self.setting_engine,
Box::new(self.interceptor_registry.build()),
)
}
}
pub struct RTCPeerConnection {
pub(crate) configuration: RTCConfiguration,
pub(crate) media_engine: MediaEngine,
pub(crate) setting_engine: SettingEngine,
pub(crate) interceptor: Box<dyn Interceptor>,
local_description: Option<RTCSessionDescription>,
current_local_description: Option<RTCSessionDescription>,
pending_local_description: Option<RTCSessionDescription>,
remote_description: Option<RTCSessionDescription>,
current_remote_description: Option<RTCSessionDescription>,
pending_remote_description: Option<RTCSessionDescription>,
pub(crate) signaling_state: RTCSignalingState,
pub(crate) peer_connection_state: RTCPeerConnectionState,
can_trickle_ice_candidates: Option<bool>,
pub(crate) pipeline_context: PipelineContext,
pub(crate) data_channels: HashMap<RTCDataChannelId, RTCDataChannelInternal>,
pub(super) rtp_transceivers: Vec<RTCRtpTransceiverInternal>,
greater_mid: isize,
sdp_origin: Origin,
last_offer: String,
last_answer: String,
ice_restart_requested: Option<RTCOfferOptions>,
negotiation_needed_state: NegotiationNeededState,
is_negotiation_ongoing: bool,
}
impl RTCPeerConnection {
pub fn create_offer(
&mut self,
mut options: Option<RTCOfferOptions>,
) -> Result<RTCSessionDescription> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
let is_ice_restart_requested = self
.ice_restart_requested
.take()
.is_some_and(|options| options.ice_restart)
|| options.take().is_some_and(|options| options.ice_restart);
if is_ice_restart_requested {
self.stage_ice_restart()?;
}
if let Some(d) = self.current_remote_description.as_ref()
&& let Some(parsed) = &d.parsed
{
for media in &parsed.media_descriptions {
if let Some(mid) = get_mid_value(media) {
if mid.is_empty() {
continue;
}
let numeric_mid = match mid.parse::<isize>() {
Ok(n) => n,
Err(_) => continue,
};
if numeric_mid > self.greater_mid {
self.greater_mid = numeric_mid;
}
}
}
}
for transceiver in &mut self.rtp_transceivers {
if let Some(mid) = transceiver.mid()
&& !mid.is_empty()
{
if let Ok(numeric_mid) = mid.parse::<isize>()
&& numeric_mid > self.greater_mid
{
self.greater_mid = numeric_mid;
}
} else {
self.greater_mid += 1;
transceiver.set_mid(format!("{}", self.greater_mid))?;
}
}
let mut d = if self.current_remote_description.is_none() {
self.generate_unmatched_sdp()?
} else {
self.generate_matched_sdp(
true,
DEFAULT_DTLS_ROLE_OFFER.to_connection_role(),
false,
)?
};
update_sdp_origin(&mut self.sdp_origin, &mut d);
let sdp = d.marshal();
let offer = RTCSessionDescription {
sdp_type: RTCSdpType::Offer,
sdp,
parsed: Some(d),
};
self.last_offer.clone_from(&offer.sdp);
Ok(offer)
}
pub fn create_answer(
&mut self,
_options: Option<RTCAnswerOptions>,
) -> Result<RTCSessionDescription> {
if self.remote_description().is_none() {
return Err(Error::ErrNoRemoteDescription);
}
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
if self.signaling_state != RTCSignalingState::HaveRemoteOffer
&& self.signaling_state != RTCSignalingState::HaveLocalPranswer
{
return Err(Error::ErrIncorrectSignalingState);
}
let mut connection_role = self.setting_engine.answering_dtls_role.to_connection_role();
if connection_role == ConnectionRole::Unspecified {
connection_role = DEFAULT_DTLS_ROLE_ANSWER.to_connection_role();
if let Some(remote_description) = self.remote_description()
&& let Some(parsed) = remote_description.parsed.as_ref()
&& is_lite_set(parsed)
&& !self.setting_engine.candidates.ice_lite
{
connection_role = RTCDtlsRole::Server.to_connection_role();
}
}
let mut d = self.generate_matched_sdp(
false,
connection_role,
self.setting_engine.ignore_rid_pause_for_recv,
)?;
update_sdp_origin(&mut self.sdp_origin, &mut d);
let sdp = d.marshal();
let answer = RTCSessionDescription {
sdp_type: RTCSdpType::Answer,
sdp,
parsed: Some(d),
};
self.last_answer.clone_from(&answer.sdp);
Ok(answer)
}
pub fn set_local_description(
&mut self,
now: Instant,
mut local_description: RTCSessionDescription,
) -> Result<()> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
if self.ice_transport().has_pending_restart() {
self.apply_ice_restart(now)?;
}
if local_description.sdp.is_empty() {
match local_description.sdp_type {
RTCSdpType::Answer | RTCSdpType::Pranswer => {
local_description.sdp.clone_from(&self.last_answer);
}
RTCSdpType::Offer => {
local_description.sdp.clone_from(&self.last_offer);
}
RTCSdpType::Rollback => {
}
_ => return Err(Error::ErrPeerConnSDPTypeInvalidValueSetLocalDescription),
}
}
if local_description.sdp_type != RTCSdpType::Rollback {
local_description.parsed = Some(local_description.unmarshal()?);
}
self.set_description(&local_description, StateChangeOp::SetLocal)?;
let we_answer = local_description.sdp_type == RTCSdpType::Answer;
if we_answer && let Some(parsed_local_description) = &local_description.parsed {
for media in &parsed_local_description.media_descriptions {
let mid_value = match get_mid_value(media) {
Some(mid) if !mid.is_empty() => mid,
_ => return Err(Error::ErrPeerConnLocalDescriptionWithoutMidValue),
};
if media.is_webrtc_datachannel() {
continue;
}
let i = match RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers) {
Some(i) => i,
None => return Err(Error::ErrPeerConnTransceiverMidNil),
};
let kind = RtpCodecKind::from(media.media_name.media.as_str());
let mut direction = get_peer_direction(media);
if kind == RtpCodecKind::Unspecified
|| direction == RTCRtpTransceiverDirection::Unspecified
{
continue;
}
if direction == RTCRtpTransceiverDirection::Sendonly
&& self.rtp_transceivers[i].sender().is_none()
{
direction = RTCRtpTransceiverDirection::Inactive;
}
self.rtp_transceivers[i].set_current_direction(direction);
}
if let Some(remote_description) = self.remote_description().cloned()
&& let Some(parsed_remote_description) = remote_description.parsed.as_ref()
{
if let (Some(local_application_media), Some(remote_application_media)) = (
get_application_media(parsed_local_description),
get_application_media(parsed_remote_description),
) {
let (dtls_role, remote_caps, local_sctp_port, remote_sctp_port) = (
self.dtls_transport().role(),
SCTPTransportCapabilities {
max_message_size: get_application_media_section_max_message_size(
remote_application_media,
)
.unwrap_or(SctpMaxMessageSize::DEFAULT_MESSAGE_SIZE),
},
get_application_media_section_sctp_port(local_application_media)
.unwrap_or(5000),
get_application_media_section_sctp_port(remote_application_media)
.unwrap_or(5000),
);
self.sctp_transport_mut().start(
dtls_role,
remote_caps,
local_sctp_port,
remote_sctp_port,
)?;
}
self.start_rtp(remote_description)?;
}
}
self.ice_transport_mut().ice_gathering_state = RTCIceGatheringState::Gathering;
Ok(())
}
pub fn local_description(&self) -> Option<RTCSessionDescription> {
if let Some(pending_local_description) = self.pending_local_description() {
return Some(pending_local_description);
}
self.current_local_description()
}
pub fn current_local_description(&self) -> Option<RTCSessionDescription> {
self.populate_local_candidates(self.current_local_description.as_ref())
}
pub fn pending_local_description(&self) -> Option<RTCSessionDescription> {
self.populate_local_candidates(self.pending_local_description.as_ref())
}
pub fn can_trickle_ice_candidates(&self) -> Option<bool> {
self.can_trickle_ice_candidates
}
pub fn set_remote_description(
&mut self,
now: Instant,
mut remote_description: RTCSessionDescription,
) -> Result<()> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
let is_renegotiation = self.current_remote_description.is_some();
if remote_description.sdp_type != RTCSdpType::Rollback {
remote_description.parsed = Some(remote_description.unmarshal()?);
}
self.set_description(&remote_description, StateChangeOp::SetRemote)?;
if let Some(parsed_remote_description) = &remote_description.parsed {
self.media_engine
.update_from_remote_description(parsed_remote_description)?;
let has_trickle_ice = has_ice_trickle_option(parsed_remote_description);
match remote_description.sdp_type {
RTCSdpType::Offer | RTCSdpType::Answer | RTCSdpType::Pranswer => {
self.can_trickle_ice_candidates = Some(has_trickle_ice);
}
_ => {
self.can_trickle_ice_candidates = None;
}
}
for transceiver in &mut self.rtp_transceivers {
if let Some(sender) = transceiver.sender_mut() {
let (is_rtx_enabled, is_fec_enabled) = (
self.media_engine
.is_rtx_enabled(sender.kind(), RTCRtpTransceiverDirection::Sendonly),
self.media_engine
.is_fec_enabled(sender.kind(), RTCRtpTransceiverDirection::Sendonly),
);
sender.configure_rtx_and_fec(is_rtx_enabled, is_fec_enabled);
}
}
let we_offer = remote_description.sdp_type == RTCSdpType::Answer;
let media_descriptions = self
.remote_description()
.as_ref()
.and_then(|r| r.parsed.as_ref())
.map(|parsed| parsed.media_descriptions.clone());
if let Some(media_descriptions) = media_descriptions {
if !we_offer {
for media in &media_descriptions {
let mid_value = match get_mid_value(media) {
Some(mid) if !mid.is_empty() => mid,
_ => return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue),
};
if media.is_webrtc_datachannel() {
continue;
}
let kind = RtpCodecKind::from(media.media_name.media.as_str());
let direction = get_peer_direction(media);
if kind == RtpCodecKind::Unspecified
|| direction == RTCRtpTransceiverDirection::Unspecified
{
continue;
}
let transceiver = if let Some(i) =
RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers)
{
if direction == RTCRtpTransceiverDirection::Inactive {
self.rtp_transceivers[i]
.stop(&self.media_engine, &mut self.interceptor)?;
}
Some(&mut self.rtp_transceivers[i])
} else {
RTCPeerConnection::satisfy_type_and_direction(
kind,
direction,
&mut self.rtp_transceivers,
)
};
if let Some(transceiver) = transceiver {
if direction == RTCRtpTransceiverDirection::Recvonly {
if transceiver.direction() == RTCRtpTransceiverDirection::Sendrecv {
transceiver.set_direction(RTCRtpTransceiverDirection::Sendonly);
} else if transceiver.direction()
== RTCRtpTransceiverDirection::Recvonly
{
transceiver.set_direction(RTCRtpTransceiverDirection::Inactive);
}
} else if direction == RTCRtpTransceiverDirection::Sendrecv {
if transceiver.direction() == RTCRtpTransceiverDirection::Sendonly {
transceiver.set_direction(RTCRtpTransceiverDirection::Sendrecv);
} else if transceiver.direction()
== RTCRtpTransceiverDirection::Inactive
{
transceiver.set_direction(RTCRtpTransceiverDirection::Recvonly);
}
} else if direction == RTCRtpTransceiverDirection::Sendonly
&& transceiver.direction() == RTCRtpTransceiverDirection::Inactive
{
transceiver.set_direction(RTCRtpTransceiverDirection::Recvonly);
}
transceiver.set_codec_preferences_from_remote_description(
media,
&self.media_engine,
)?;
if transceiver.mid().is_none() {
transceiver.set_mid(mid_value.to_string())?;
}
} else {
let local_direction =
if direction == RTCRtpTransceiverDirection::Recvonly {
RTCRtpTransceiverDirection::Sendonly
} else {
RTCRtpTransceiverDirection::Recvonly
};
let mut transceiver = RTCRtpTransceiverInternal::new(
kind,
None,
RTCRtpTransceiverInit {
direction: local_direction,
streams: vec![],
send_encodings: vec![],
},
);
transceiver.set_codec_preferences_from_remote_description(
media,
&self.media_engine,
)?;
if transceiver.mid().is_none() {
transceiver.set_mid(mid_value.to_string())?;
}
transceiver.set_created_by_remote_description(true);
self.add_rtp_transceiver(transceiver);
}
}
} else {
for media in &media_descriptions {
let mid_value = match get_mid_value(media) {
Some(mid) if !mid.is_empty() => mid,
_ => return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue),
};
if media.is_webrtc_datachannel() {
continue;
}
let kind = RtpCodecKind::from(media.media_name.media.as_str());
let mut direction = get_peer_direction(media);
if kind == RtpCodecKind::Unspecified
|| direction == RTCRtpTransceiverDirection::Unspecified
{
continue;
}
let transceiver = if let Some(i) =
RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers)
{
&mut self.rtp_transceivers[i]
} else {
return Err(Error::ErrPeerConnTransceiverMidNil);
};
if direction == RTCRtpTransceiverDirection::Sendonly {
direction = RTCRtpTransceiverDirection::Recvonly;
} else if direction == RTCRtpTransceiverDirection::Recvonly {
direction = RTCRtpTransceiverDirection::Sendonly;
}
transceiver.set_current_direction(direction);
transceiver.set_codec_preferences_from_remote_description(
media,
&self.media_engine,
)?;
}
}
}
let (remote_ufrag, remote_pwd, candidates) =
extract_ice_details(parsed_remote_description)?;
if is_renegotiation
&& self
.ice_transport()
.have_remote_credentials_change(&remote_ufrag, &remote_pwd)
{
if !we_offer {
self.stage_ice_restart()?;
self.apply_ice_restart(now)?;
}
self.ice_transport_mut()
.set_remote_credentials(remote_ufrag.clone(), remote_pwd.clone())?;
}
for candidate in candidates {
self.ice_transport_mut().add_remote_candidate(candidate)?;
}
if !is_renegotiation {
let remote_is_lite = is_lite_set(parsed_remote_description);
let (remote_fingerprint, remote_fingerprint_hash) =
extract_fingerprint(parsed_remote_description)?;
let local_ice_role = if (we_offer
&& remote_is_lite == self.setting_engine.candidates.ice_lite)
|| (remote_is_lite && !self.setting_engine.candidates.ice_lite)
{
RTCIceRole::Controlling
} else {
RTCIceRole::Controlled
};
let remote_dtls_role = RTCDtlsRole::from(parsed_remote_description);
log::trace!(
"start_transports: local_ice_role={local_ice_role}, remote_dtls_role={remote_dtls_role}"
);
self.start_transports(
now,
local_ice_role,
RTCIceParameters {
username_fragment: remote_ufrag,
password: remote_pwd,
ice_lite: remote_is_lite,
},
RTCDtlsParameters {
role: remote_dtls_role,
fingerprints: vec![RTCDtlsFingerprint {
algorithm: remote_fingerprint_hash,
value: remote_fingerprint,
}],
},
)?;
}
if we_offer
&& let Some(parsed_local_description) = self
.current_local_description
.as_ref()
.and_then(|desc| desc.parsed.as_ref())
{
if let (Some(local_application_media), Some(remote_application_media)) = (
get_application_media(parsed_local_description),
get_application_media(parsed_remote_description),
) {
let (dtls_role, remote_caps, local_sctp_port, remote_sctp_port) = (
self.dtls_transport().role(),
SCTPTransportCapabilities {
max_message_size: get_application_media_section_max_message_size(
remote_application_media,
)
.unwrap_or(SctpMaxMessageSize::DEFAULT_MESSAGE_SIZE),
},
get_application_media_section_sctp_port(local_application_media)
.unwrap_or(5000),
get_application_media_section_sctp_port(remote_application_media)
.unwrap_or(5000),
);
self.sctp_transport_mut().start(
dtls_role,
remote_caps,
local_sctp_port,
remote_sctp_port,
)?;
}
self.start_rtp(remote_description)?;
}
}
Ok(())
}
pub fn remote_description(&self) -> Option<&RTCSessionDescription> {
if self.pending_remote_description.is_some() {
self.pending_remote_description.as_ref()
} else {
self.current_remote_description.as_ref()
}
}
pub fn current_remote_description(&self) -> Option<&RTCSessionDescription> {
self.current_remote_description.as_ref()
}
pub fn pending_remote_description(&self) -> Option<&RTCSessionDescription> {
self.pending_remote_description.as_ref()
}
pub fn add_remote_candidate(&mut self, remote_candidate: RTCIceCandidateInit) -> Result<()> {
if self.remote_description().is_none() {
return Err(Error::ErrNoRemoteDescription);
}
let candidate_value = match remote_candidate.candidate.strip_prefix("candidate:") {
Some(s) => s,
None => remote_candidate.candidate.as_str(),
};
if !candidate_value.is_empty() {
self.add_ice_remote_candidate(candidate_value)?;
}
Ok(())
}
pub fn add_local_candidate(&mut self, local_candidate: RTCIceCandidateInit) -> Result<()> {
let candidate_value = match local_candidate.candidate.strip_prefix("candidate:") {
Some(s) => s,
None => local_candidate.candidate.as_str(),
};
if !candidate_value.is_empty() {
self.add_ice_local_candidate(candidate_value, local_candidate.url.as_deref())?;
} else {
self.ice_transport_mut().ice_gathering_state = RTCIceGatheringState::Complete;
self.pipeline_context.event_outs.push_back(
RTCPeerConnectionEvent::OnIceGatheringStateChangeEvent(
RTCIceGatheringState::Complete,
),
);
}
Ok(())
}
pub fn restart_ice(&mut self) {
self.ice_restart_requested = Some(RTCOfferOptions { ice_restart: true });
}
pub fn get_configuration(&self) -> &RTCConfiguration {
&self.configuration
}
pub fn set_configuration(&mut self, configuration: RTCConfiguration) -> Result<()> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
if !configuration.peer_identity.is_empty() {
if configuration.peer_identity != self.configuration.peer_identity {
return Err(Error::ErrModifyingPeerIdentity);
}
self.configuration.peer_identity = configuration.peer_identity;
}
if !configuration.certificates.is_empty() {
if configuration.certificates.len() != self.configuration.certificates.len() {
return Err(Error::ErrModifyingCertificates);
}
self.configuration.certificates = configuration.certificates;
}
if configuration.bundle_policy != self.configuration.bundle_policy {
return Err(Error::ErrModifyingBundlePolicy);
}
self.configuration.bundle_policy = configuration.bundle_policy;
if configuration.rtcp_mux_policy != self.configuration.rtcp_mux_policy {
return Err(Error::ErrModifyingRTCPMuxPolicy);
}
self.configuration.rtcp_mux_policy = configuration.rtcp_mux_policy;
if configuration.ice_candidate_pool_size != 0 {
if self.configuration.ice_candidate_pool_size != configuration.ice_candidate_pool_size
&& self.local_description().is_some()
{
return Err(Error::ErrModifyingICECandidatePoolSize);
}
self.configuration.ice_candidate_pool_size = configuration.ice_candidate_pool_size;
}
self.configuration.ice_transport_policy = configuration.ice_transport_policy;
if !configuration.ice_servers.is_empty() {
for server in &configuration.ice_servers {
server.validate()?;
}
self.configuration.ice_servers = configuration.ice_servers
}
Ok(())
}
pub fn create_data_channel(
&mut self,
label: &str,
options: Option<RTCDataChannelInit>,
) -> Result<RTCDataChannel<'_>> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
let mut params = DataChannelParameters {
label: label.to_owned(),
..Default::default()
};
let mut id = self.generate_data_channel_id()?;
let options = options.unwrap_or_default();
if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() {
return Err(Error::ErrRetransmitsOrPacketLifeTime);
}
params.ordered = options.ordered;
params.max_packet_life_time = options.max_packet_life_time;
params.max_retransmits = options.max_retransmits;
params.protocol = options.protocol;
if params.protocol.len() > 65535 {
return Err(Error::ErrProtocolTooLarge);
}
params.negotiated = options.negotiated;
if let Some(negotiated_id) = ¶ms.negotiated {
id = *negotiated_id;
}
let mut data_channel = RTCDataChannelInternal::new(id, params);
if let Some(handle) = self
.sctp_transport()
.sctp_associations
.keys()
.next()
.copied()
&& data_channel.ready_state == RTCDataChannelState::Connecting
&& data_channel.data_channel.is_none()
{
data_channel.dial(handle.0)?;
}
self.data_channels.insert(id, data_channel);
self.trigger_negotiation_needed();
Ok(RTCDataChannel {
id,
peer_connection: self,
})
}
pub fn get_senders(&self) -> impl Iterator<Item = RTCRtpSenderId> + use<'_> {
self.rtp_transceivers
.iter()
.enumerate()
.filter(|(_, transceiver)| transceiver.direction().has_send())
.map(|(id, _)| RTCRtpSenderId(id))
}
pub fn get_receivers(&self) -> impl Iterator<Item = RTCRtpReceiverId> + use<'_> {
self.rtp_transceivers
.iter()
.enumerate()
.filter(|(_, transceiver)| transceiver.direction().has_recv())
.map(|(id, _)| RTCRtpReceiverId(id))
}
pub fn get_transceivers(&self) -> impl Iterator<Item = RTCRtpTransceiverId> {
0..self.rtp_transceivers.len()
}
pub fn add_track(&mut self, track: MediaStreamTrack) -> Result<RTCRtpSenderId> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
let send_encodings = self.send_encodings_from_track(&track);
let (track, send_encodings, codec_preferences) =
self.normalize_sender_track(track, send_encodings)?;
for (id, transceiver) in self.rtp_transceivers.iter_mut().enumerate() {
if !transceiver.stopped()
&& transceiver.kind() == track.kind()
&& transceiver.sender().is_none()
{
let mut sender =
RTCRtpSenderInternal::new(track.kind(), track, vec![], send_encodings);
if transceiver.get_codec_preferences().is_empty() && !codec_preferences.is_empty() {
transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
}
sender.set_codec_preferences(transceiver.get_codec_preferences().to_vec());
transceiver.sender_mut().replace(sender);
transceiver.set_direction(RTCRtpTransceiverDirection::from_send_recv(
true,
transceiver.direction().has_recv(),
));
self.trigger_negotiation_needed();
return Ok(RTCRtpSenderId(id));
}
}
let mut transceiver = self.new_transceiver_from_track(
track,
RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Sendrecv,
streams: vec![],
send_encodings,
},
)?;
if !codec_preferences.is_empty() {
transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
}
Ok(RTCRtpSenderId(self.add_rtp_transceiver(transceiver)))
}
pub fn remove_track(&mut self, sender_id: RTCRtpSenderId) -> Result<()> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
if sender_id.0 >= self.rtp_transceivers.len() {
return Err(Error::ErrRTPSenderNotExisted);
}
let has_recv = self.rtp_transceivers[sender_id.0].direction().has_recv();
self.rtp_transceivers[sender_id.0]
.set_direction(RTCRtpTransceiverDirection::from_send_recv(false, has_recv));
if let Some(sender) = self.rtp_transceivers[sender_id.0].sender_mut()
&& sender
.stop(&self.media_engine, &mut self.interceptor)
.is_ok()
{
self.trigger_negotiation_needed();
}
self.rtp_transceivers[sender_id.0].sender_mut().take();
Ok(())
}
pub fn add_transceiver_from_track(
&mut self,
track: MediaStreamTrack,
init: Option<RTCRtpTransceiverInit>,
) -> Result<RTCRtpTransceiverId> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
if let Some(init) = init.as_ref()
&& !init.direction.has_send()
{
return Err(Error::ErrInvalidDirection);
}
let mut init = if let Some(init) = init {
init
} else {
RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Sendrecv,
streams: vec![],
send_encodings: vec![],
}
};
let send_encodings = if init.send_encodings.is_empty() {
self.send_encodings_from_track(&track)
} else {
init.send_encodings.clone()
};
let (track, send_encodings, codec_preferences) =
self.normalize_sender_track(track, send_encodings)?;
init.send_encodings = send_encodings;
let mut transceiver = self.new_transceiver_from_track(track, init)?;
if !codec_preferences.is_empty() {
transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
}
Ok(self.add_rtp_transceiver(transceiver))
}
pub fn add_transceiver_from_kind(
&mut self,
kind: RtpCodecKind,
init: Option<RTCRtpTransceiverInit>,
) -> Result<RTCRtpTransceiverId> {
if self.peer_connection_state == RTCPeerConnectionState::Closed {
return Err(Error::ErrConnectionClosed);
}
let init = if let Some(init) = init {
if init.direction.has_send() && init.send_encodings.is_empty() {
return Err(Error::ErrInvalidDirection);
}
init
} else {
RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}
};
let transceiver = match init.direction {
RTCRtpTransceiverDirection::Sendonly | RTCRtpTransceiverDirection::Sendrecv => {
let mut init = init;
let track = MediaStreamTrack::new(
math_rand_alpha(16), math_rand_alpha(16), math_rand_alpha(16), kind,
init.send_encodings.clone(),
);
let (track, send_encodings, codec_preferences) =
self.normalize_sender_track(track, init.send_encodings)?;
init.send_encodings = send_encodings;
let mut transceiver = self.new_transceiver_from_track(track, init)?;
if !codec_preferences.is_empty() {
transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
}
transceiver
}
RTCRtpTransceiverDirection::Recvonly => {
RTCRtpTransceiverInternal::new(kind, None, init)
}
_ => return Err(Error::ErrPeerConnAddTransceiverFromKindSupport),
};
Ok(self.add_rtp_transceiver(transceiver))
}
pub fn data_channel(&mut self, id: RTCDataChannelId) -> Option<RTCDataChannel<'_>> {
if self.data_channels.contains_key(&id) {
Some(RTCDataChannel {
id,
peer_connection: self,
})
} else {
None
}
}
pub fn sctp(&self) -> Option<RTCSctpTransport<'_>> {
if self.sctp_transport().is_started {
Some(RTCSctpTransport {
peer_connection: self,
})
} else {
None
}
}
pub fn rtp_sender(&mut self, id: RTCRtpSenderId) -> Option<RTCRtpSender<'_>> {
if id.0 < self.rtp_transceivers.len()
&& self.rtp_transceivers[id.0].direction().has_send()
&& self.rtp_transceivers[id.0].sender().is_some()
{
Some(RTCRtpSender {
id,
peer_connection: self,
})
} else {
None
}
}
pub fn rtp_receiver(&mut self, id: RTCRtpReceiverId) -> Option<RTCRtpReceiver<'_>> {
if id.0 < self.rtp_transceivers.len()
&& self.rtp_transceivers[id.0].direction().has_recv()
&& self.rtp_transceivers[id.0].receiver().is_some()
{
Some(RTCRtpReceiver {
id,
peer_connection: self,
})
} else {
None
}
}
pub fn rtp_transceiver(&mut self, id: RTCRtpTransceiverId) -> Option<RTCRtpTransceiver<'_>> {
if id < self.rtp_transceivers.len() {
Some(RTCRtpTransceiver {
id,
peer_connection: self,
})
} else {
None
}
}
pub fn get_stats(&mut self, now: Instant, selector: StatsSelector) -> RTCStatsReport {
self.update_ice_agent_stats(now);
self.update_codec_stats();
self.pipeline_context
.stats
.snapshot_with_selector(now, selector)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_channel::state::RTCDataChannelState;
use crate::peer_connection::configuration::setting_engine::SctpMaxMessageSize;
use crate::peer_connection::configuration::setting_engine::SettingEngineBuilder;
use crate::peer_connection::transport::RTCIceComponent;
use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState;
use sctp::AssociationHandle;
#[test]
fn with_sctp_receive_buffer_size_sets_and_clamps() {
let setting_engine = SettingEngineBuilder::new()
.with_sctp_max_receive_buffer_size(200_000)
.build();
let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine);
assert_eq!(
builder.setting_engine.sctp_max_receive_buffer_size,
Some(200_000)
);
for input in [0u32, 500, 1499] {
let setting_engine = SettingEngineBuilder::new()
.with_sctp_max_receive_buffer_size(input)
.build();
let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine);
assert_eq!(
builder.setting_engine.sctp_max_receive_buffer_size,
Some(1500),
"input {input} should clamp up to the 1500-byte floor"
);
}
}
#[test]
fn sctp_is_none_until_sctp_is_negotiated() {
let pc = RTCPeerConnectionBuilder::new()
.build(Instant::now())
.unwrap();
assert!(
pc.sctp().is_none(),
"nothing has been negotiated, so there is no SCTP transport to expose"
);
}
#[test]
fn the_transport_graph_is_walkable_and_ids_identify() {
let mut pc = RTCPeerConnectionBuilder::new()
.build(Instant::now())
.unwrap();
pc.sctp_transport_mut()
.start(
RTCDtlsRole::Client,
crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
max_message_size: 0,
},
5000,
5000,
)
.expect("start");
let sctp = pc.sctp().expect("SCTP is negotiated");
let dtls = sctp.transport();
let ice = dtls.ice_transport();
assert_ne!(sctp.id(), dtls.id());
assert_ne!(dtls.id(), ice.id());
assert_ne!(sctp.id(), ice.id());
let dtls_again = pc.sctp().unwrap().transport();
assert_eq!(dtls.id(), dtls_again.id());
assert_eq!(ice.id(), dtls_again.ice_transport().id());
assert_eq!(Some(65536), sctp.max_message_size());
assert_eq!(None, sctp.max_channels());
assert_eq!(RTCIceComponent::Rtp, ice.component());
}
#[test]
fn max_message_size_with_no_configured_limit_reports_the_ceiling() {
let setting_engine = SettingEngineBuilder::new()
.with_sctp_max_message_size(SctpMaxMessageSize::Bounded(0))
.build();
let mut pc = RTCPeerConnectionBuilder::new()
.with_setting_engine(setting_engine)
.build(Instant::now())
.unwrap();
pc.sctp_transport_mut()
.start(
RTCDtlsRole::Client,
crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
max_message_size: 0,
},
5000,
5000,
)
.expect("start");
assert_eq!(
Some(SctpMaxMessageSize::MAX_MESSAGE_SIZE),
pc.sctp().expect("negotiated").max_message_size()
);
}
#[test]
fn sender_and_receiver_transport_are_none_until_the_transceiver_is_associated() {
let mut pc = media_pc();
let track = MediaStreamTrack::new(
"stream".to_owned(),
"track".to_owned(),
"label".to_owned(),
RtpCodecKind::Audio,
vec![],
);
let sender_id = pc.add_track(track).expect("add track");
let receiver_id = RTCRtpReceiverId::from(sender_id.0);
assert!(pc.rtp_transceivers[sender_id.0].mid().is_none());
assert!(
pc.rtp_sender(sender_id)
.expect("sender")
.transport()
.is_none(),
"an unassociated sender has a null transport"
);
assert!(
pc.rtp_receiver(receiver_id)
.expect("receiver")
.transport()
.is_none(),
"an unassociated receiver has a null transport"
);
let offer = pc.create_offer(None).expect("create offer");
pc.set_local_description(Instant::now(), offer)
.expect("set local description");
assert!(pc.rtp_transceivers[sender_id.0].mid().is_some());
assert!(
!pc.dtls_transport().is_started(),
"no answer yet, so DTLS has not been brought up"
);
let sender_transport_id = pc
.rtp_sender(sender_id)
.expect("sender")
.transport()
.expect("an associated sender has a transport")
.id();
let receiver_transport_id = pc
.rtp_receiver(receiver_id)
.expect("receiver")
.transport()
.expect("an associated receiver has a transport")
.id();
assert_eq!(sender_transport_id, receiver_transport_id);
assert_eq!(sender_transport_id, pc.dtls_transport().id);
assert_eq!(RTCDtlsTransportState::New, pc.dtls_transport().state());
}
#[test]
fn discard_local_candidates_during_ice_restart_reaches_apply_restart() {
fn restart_with(discard: bool) -> usize {
let setting_engine = SettingEngineBuilder::new()
.with_discard_local_candidates_during_ice_restart(discard)
.build();
let mut pc = RTCPeerConnectionBuilder::new()
.with_setting_engine(setting_engine)
.build(Instant::now())
.unwrap();
pc.add_local_candidate(RTCIceCandidateInit {
candidate: "candidate:1 1 udp 2130706431 127.0.0.1 5000 typ host".to_owned(),
..Default::default()
})
.expect("add local candidate");
assert_eq!(
1,
pc.ice_transport().get_local_candidates().unwrap().len(),
"precondition: the agent holds the gathered candidate"
);
pc.ice_transport_mut()
.generate_restart_credentials(
"newufrag".to_owned(),
"newpasswordlongenough".to_owned(),
)
.expect("stage restart");
pc.apply_ice_restart(Instant::now())
.expect("apply ice restart");
pc.ice_transport().get_local_candidates().unwrap().len()
}
assert_eq!(
0,
restart_with(true),
"with discard enabled the stale generation's candidates are dropped"
);
assert_eq!(
1,
restart_with(false),
"the default keeps them, which is the pre-existing behaviour"
);
}
#[test]
fn transports_of_two_peer_connections_are_never_equal() {
let mut ids = vec![];
for _ in 0..2 {
let mut pc = RTCPeerConnectionBuilder::new()
.build(Instant::now())
.unwrap();
pc.sctp_transport_mut()
.start(
RTCDtlsRole::Client,
crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
max_message_size: 0,
},
5000,
5000,
)
.expect("start");
let sctp = pc.sctp().expect("SCTP is negotiated");
ids.push((
sctp.id(),
sctp.transport().id(),
sctp.transport().ice_transport().id(),
));
}
let (a_sctp, a_dtls, a_ice) = ids[0];
let (b_sctp, b_dtls, b_ice) = ids[1];
assert_ne!(a_sctp, b_sctp);
assert_ne!(a_dtls, b_dtls);
assert_ne!(a_ice, b_ice);
}
#[test]
fn create_data_channel_dials_immediately_when_sctp_association_present() {
let mut pc = RTCPeerConnectionBuilder::new()
.build(Instant::now())
.unwrap();
pc.sctp_transport_mut()
.sctp_associations
.insert(AssociationHandle(0), sctp::Association::default());
let _dc = pc.create_data_channel("test", None).unwrap();
let internal = pc
.data_channels
.values()
.next()
.expect("data channel must be stored internally");
assert!(internal.data_channel.is_some());
assert_eq!(
internal.ready_state,
RTCDataChannelState::Connecting,
"a dialed in-band channel stays Connecting until its DATA_CHANNEL_ACK arrives"
);
}
use crate::peer_connection::configuration::media_engine::MediaEngine;
use crate::rtp_transceiver::RTCRtpTransceiverInit;
fn media_pc() -> RTCPeerConnection {
let mut me = MediaEngine::default();
me.register_default_codecs().unwrap();
RTCPeerConnectionBuilder::new()
.with_media_engine(me)
.build(Instant::now())
.unwrap()
}
fn audio_video_offer() -> RTCSessionDescription {
let mut offerer = media_pc();
offerer
.add_transceiver_from_kind(
RtpCodecKind::Audio,
Some(RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}),
)
.unwrap();
offerer
.add_transceiver_from_kind(
RtpCodecKind::Video,
Some(RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}),
)
.unwrap();
offerer.create_offer(None).unwrap()
}
fn rollback() -> RTCSessionDescription {
RTCSessionDescription {
sdp_type: RTCSdpType::Rollback,
sdp: String::new(),
parsed: None,
}
}
#[test]
fn set_remote_rollback_removes_transceivers_created_by_remote_offer() {
let offer = audio_video_offer();
let mut pc = media_pc();
pc.set_remote_description(Instant::now(), offer).unwrap();
assert_eq!(pc.rtp_transceivers.len(), 2);
assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
assert!(pc.rtp_transceivers.iter().all(|t| t.mid().is_some()));
pc.set_remote_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert!(
pc.rtp_transceivers.is_empty(),
"transceivers created by a rolled-back remote offer must be removed"
);
}
#[test]
fn set_local_rollback_disassociates_but_keeps_app_created_transceivers() {
let mut pc = media_pc();
pc.add_transceiver_from_kind(
RtpCodecKind::Audio,
Some(RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}),
)
.unwrap();
let offer = pc.create_offer(None).unwrap();
pc.set_local_description(Instant::now(), offer).unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::HaveLocalOffer);
assert_eq!(pc.rtp_transceivers.len(), 1);
assert!(pc.rtp_transceivers[0].mid().is_some());
pc.set_local_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert_eq!(
pc.rtp_transceivers.len(),
1,
"application-created transceivers must not be removed by rollback"
);
assert!(
pc.rtp_transceivers[0].mid().is_none(),
"rolled-back transceiver must be disassociated from its m= section"
);
}
#[test]
fn rollback_keeps_transceiver_with_track_attached_via_add_track() {
let offer = audio_video_offer();
let mut pc = media_pc();
pc.set_remote_description(Instant::now(), offer).unwrap();
assert_eq!(pc.rtp_transceivers.len(), 2);
let track = MediaStreamTrack::new(
"stream".to_owned(),
"track".to_owned(),
"label".to_owned(),
RtpCodecKind::Audio,
vec![],
);
pc.add_track(track).unwrap();
pc.set_remote_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert_eq!(
pc.rtp_transceivers.len(),
1,
"transceiver with a track attached via add_track must not be removed"
);
let kept = &pc.rtp_transceivers[0];
assert_eq!(kept.kind(), RtpCodecKind::Audio);
assert!(kept.sender().is_some());
assert!(
kept.mid().is_none(),
"kept transceiver must be disassociated from its m= section on rollback"
);
}
#[test]
fn rollback_keeps_transceiver_negotiated_by_a_previous_exchange() {
let offer = audio_video_offer();
let mut pc = media_pc();
pc.set_remote_description(Instant::now(), offer).unwrap();
assert_eq!(pc.rtp_transceivers.len(), 2);
let answer = pc.create_answer(None).unwrap();
pc.set_local_description(Instant::now(), answer).unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
let negotiated_mids: Vec<_> = pc
.rtp_transceivers
.iter()
.map(|t| t.mid().clone())
.collect();
assert!(negotiated_mids.iter().all(|m| m.is_some()));
let reoffer = audio_video_offer();
pc.set_remote_description(Instant::now(), reoffer).unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
pc.set_remote_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert_eq!(
pc.rtp_transceivers.len(),
2,
"previously-negotiated transceivers must not be removed by a renegotiation rollback"
);
let mids_after: Vec<_> = pc
.rtp_transceivers
.iter()
.map(|t| t.mid().clone())
.collect();
assert_eq!(
mids_after, negotiated_mids,
"previously-negotiated transceivers must keep their mid across rollback"
);
}
#[test]
fn add_track_then_rollback_remote_offer_then_create_offer_includes_track() {
let mut pc = media_pc();
let track = MediaStreamTrack::new(
"stream".to_owned(),
"track".to_owned(),
"label".to_owned(),
RtpCodecKind::Audio,
vec![],
);
pc.add_track(track).unwrap();
assert_eq!(pc.rtp_transceivers.len(), 1);
let mut video_offerer = media_pc();
video_offerer
.add_transceiver_from_kind(
RtpCodecKind::Video,
Some(RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}),
)
.unwrap();
let remote_offer = video_offerer.create_offer(None).unwrap();
pc.set_remote_description(Instant::now(), remote_offer)
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
assert!(
pc.rtp_transceivers
.iter()
.any(|t| t.kind() == RtpCodecKind::Audio && t.sender().is_some())
);
pc.set_remote_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert_eq!(
pc.rtp_transceivers.len(),
1,
"the add_track transceiver must survive rollback; the remote one must be removed"
);
let kept = &pc.rtp_transceivers[0];
assert_eq!(kept.kind(), RtpCodecKind::Audio);
assert!(kept.sender().is_some());
assert!(kept.mid().is_none(), "must be disassociated after rollback");
let offer = pc.create_offer(None).unwrap();
assert_eq!(
offer.sdp.matches("m=audio").count(),
1,
"createOffer after rollback must emit an m=audio section for the added track"
);
assert!(pc.rtp_transceivers[0].mid().is_some());
}
#[test]
fn add_track_then_rollback_local_offer_then_answer_remote_still_renegotiates_track() {
let mut pc = media_pc();
let track = MediaStreamTrack::new(
"stream".to_owned(),
"track".to_owned(),
"label".to_owned(),
RtpCodecKind::Audio,
vec![],
);
pc.add_track(track).unwrap();
let local_offer = pc.create_offer(None).unwrap();
pc.set_local_description(Instant::now(), local_offer)
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::HaveLocalOffer);
assert!(pc.rtp_transceivers[0].mid().is_some());
let mut video_offerer = media_pc();
video_offerer
.add_transceiver_from_kind(
RtpCodecKind::Video,
Some(RTCRtpTransceiverInit {
direction: RTCRtpTransceiverDirection::Recvonly,
streams: vec![],
send_encodings: vec![],
}),
)
.unwrap();
let remote_offer = video_offerer.create_offer(None).unwrap();
pc.set_local_description(Instant::now(), rollback())
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
assert_eq!(pc.rtp_transceivers.len(), 1);
assert_eq!(pc.rtp_transceivers[0].kind(), RtpCodecKind::Audio);
assert!(pc.rtp_transceivers[0].sender().is_some());
assert!(pc.rtp_transceivers[0].mid().is_none());
pc.set_remote_description(Instant::now(), remote_offer)
.unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
let answer = pc.create_answer(None).unwrap();
assert_eq!(answer.sdp.matches("m=video").count(), 1);
assert_eq!(answer.sdp.matches("m=audio").count(), 0);
pc.set_local_description(Instant::now(), answer).unwrap();
assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
let followup_offer = pc.create_offer(None).unwrap();
assert_eq!(
followup_offer.sdp.matches("m=audio").count(),
1,
"the added track must appear in the offer generated after rollback + answer"
);
assert_eq!(followup_offer.sdp.matches("m=video").count(), 1);
assert!(
pc.rtp_transceivers
.iter()
.find(|t| t.kind() == RtpCodecKind::Audio)
.unwrap()
.mid()
.is_some(),
"audio transceiver must be re-associated for the follow-up offer"
);
}
}