rtc/peer_connection/mod.rs
1//! Peer-to-peer connections
2//!
3//! This module implements the `RTCPeerConnection` interface as defined in the
4//! [W3C WebRTC specification](https://www.w3.org/TR/webrtc/). It provides
5//! the core functionality for establishing peer-to-peer connections, negotiating
6//! media capabilities, and managing data channels.
7//!
8//! # Overview
9//!
10//! `RTCPeerConnection` is the central interface in WebRTC. It handles:
11//!
12//! - **Signaling**: Creating and exchanging SDP offers/answers
13//! - **ICE**: Gathering candidates and establishing connectivity
14//! - **Media**: Managing audio/video tracks and transceivers
15//! - **Data**: Creating and managing data channels
16//! - **Security**: DTLS encryption for all communication
17//!
18//! # Architecture
19//!
20//! This is a **sans-I/O** implementation, meaning it separates protocol logic
21//! from I/O operations. The application is responsible for:
22//!
23//! - Transmitting/receiving network packets
24//! - Managing the event loop
25//! - Handling signaling channel communication
26//!
27//! ## Sans-I/O Benefits
28//!
29//! - **Flexibility**: Works with any I/O runtime (tokio, async-std, blocking, etc.)
30//! - **Testability**: Protocol logic can be tested without network I/O
31//! - **Control**: Application has full control over threading and scheduling
32//!
33//! # Connection Establishment
34//!
35//! The typical WebRTC connection flow:
36//!
37//! ```text
38//! Peer A (Offerer) Signaling Server Peer B (Answerer)
39//! ════════════════ ════════════════ ═══════════════════
40//! │ │ │
41//! │ 1. create_offer() │ │
42//! │─────────────────┐ │ │
43//! │ │ │ │
44//! │<────────────────┘ │ │
45//! │ │ │
46//! │ 2. set_local_description() │ │
47//! │─────────────────┐ │ │
48//! │ │ │ │
49//! │<────────────────┘ │ │
50//! │ │ │
51//! │ 3. send offer (via signaling) │ │
52//! │──────────────────────────────>│──────────────────────────────>│
53//! │ │ │
54//! │ │ 4. set_remote_description() │
55//! │ │ ┌────────────┤
56//! │ │ │ │
57//! │ │ └───────────>│
58//! │ │ │
59//! │ │ 5. create_answer() │
60//! │ │ ┌────────────┤
61//! │ │ │ │
62//! │ │ └───────────>│
63//! │ │ │
64//! │ │ 6. set_local_description() │
65//! │ │ ┌────────────┤
66//! │ │ │ │
67//! │ │ └───────────>│
68//! │ │ │
69//! │ 7. receive answer │<──────────────────────────────│
70//! │<──────────────────────────────┤ │
71//! │ │ │
72//! │ 8. set_remote_description() │ │
73//! │─────────────────┐ │ │
74//! │ │ │ │
75//! │<────────────────┘ │ │
76//! │ │ │
77//! │ 9. ICE candidates exchanged │ │
78//! │<─────────────────────────────────────────────────────────────>│
79//! │ │ │
80//! │ 10. Media/data flows directly │ │
81//! │<═════════════════════════════════════════════════════════════>│
82//! ```
83//!
84//! # Examples
85//!
86//! ## Creating a Peer Connection
87//!
88//! ```
89//! # use std::time::Instant;
90//! use rtc::peer_connection::RTCPeerConnectionBuilder;
91//!
92//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
93//! // Create with default configuration
94//! let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
95//! # Ok(())
96//! # }
97//! ```
98//!
99//! ## Creating an Offer (Initiating Peer)
100//!
101//! ```no_run
102//! # use std::time::Instant;
103//! use rtc::peer_connection::RTCPeerConnectionBuilder;
104//!
105//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
106//! let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
107//!
108//! // Add media track or data channel first
109//! // pc.add_track(audio_track)?;
110//!
111//! // Create offer
112//! let offer = pc.create_offer(None)?;
113//!
114//! // Set as local description
115//! pc.set_local_description(Instant::now(), offer.clone())?;
116//!
117//! // Send offer.sdp to remote peer via signaling channel
118//! // signaling_channel.send(offer.sdp)?;
119//! # Ok(())
120//! # }
121//! ```
122//!
123//! ## Answering an Offer (Responding Peer)
124//!
125//! ```no_run
126//! # use std::time::Instant;
127//! use rtc::peer_connection::RTCPeerConnectionBuilder;
128//! use rtc::peer_connection::sdp::RTCSessionDescription;
129//!
130//! # fn example(remote_offer_sdp: String) -> Result<(), Box<dyn std::error::Error>> {
131//! let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
132//!
133//! // Receive offer from remote peer
134//! let offer = RTCSessionDescription::offer(remote_offer_sdp)?;
135//!
136//! // Set as remote description
137//! pc.set_remote_description(Instant::now(), offer)?;
138//!
139//! // Create answer
140//! let answer = pc.create_answer(None)?;
141//!
142//! // Set as local description
143//! pc.set_local_description(Instant::now(), answer.clone())?;
144//!
145//! // Send answer.sdp to remote peer via signaling channel
146//! // signaling_channel.send(answer.sdp)?;
147//! # Ok(())
148//! # }
149//! ```
150//!
151//! ## Adding Media Tracks
152//!
153//! ```no_run
154//! # use std::time::Instant;
155//! use rtc::peer_connection::RTCPeerConnectionBuilder;
156//! use rtc::media_stream::MediaStreamTrack;
157//! use rtc::rtp_transceiver::rtp_sender::RtpCodecKind;
158//!
159//! # fn example(audio_track: MediaStreamTrack) -> Result<(), Box<dyn std::error::Error>> {
160//! let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
161//!
162//! // Add an audio track
163//! let sender_id = pc.add_track(audio_track)?;
164//!
165//! // Or add a transceiver for receiving
166//! let transceiver_id = pc.add_transceiver_from_kind(RtpCodecKind::Video, None)?;
167//! # Ok(())
168//! # }
169//! ```
170//!
171//! ## Creating Data Channels
172//!
173//! ```no_run
174//! # use std::time::Instant;
175//! use rtc::peer_connection::RTCPeerConnectionBuilder;
176//! use rtc::data_channel::RTCDataChannelInit;
177//!
178//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
179//! let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
180//!
181//! // Create a reliable, ordered data channel
182//! let init = RTCDataChannelInit {
183//! ordered: true,
184//! max_retransmits: None,
185//! ..Default::default()
186//! };
187//!
188//! let channel_id = pc.create_data_channel("my-channel", Some(init))?;
189//! # Ok(())
190//! # }
191//! ```
192//!
193//! ## ICE Candidate Exchange
194//!
195//! ```no_run
196//! use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder};
197//! use rtc::peer_connection::transport::RTCIceCandidateInit;
198//!
199//! # fn example(mut pc: RTCPeerConnection) -> Result<(), Box<dyn std::error::Error>> {
200//! // When local candidates are gathered, send to remote peer
201//! // (In sans-I/O, you'd poll for events to get candidates)
202//!
203//! // When receiving remote candidate from signaling channel
204//! let remote_candidate = RTCIceCandidateInit {
205//! candidate: "candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host".to_string(),
206//! ..Default::default()
207//! };
208//!
209//! pc.add_remote_candidate(remote_candidate)?;
210//! # Ok(())
211//! # }
212//! ```
213//!
214//! # State Management
215//!
216//! The peer connection maintains several state machines:
217//!
218//! - **Signaling State**: SDP negotiation progress (stable, have-local-offer, etc.)
219//! - **ICE Connection State**: Network connectivity status
220//! - **ICE Gathering State**: Candidate gathering progress
221//! - **Connection State**: Overall connection health
222//!
223//! Monitor these states through the event system (sans-I/O polling).
224//!
225//! # Thread Safety
226//!
227//! `RTCPeerConnection` is **not** thread-safe. The application must ensure
228//! exclusive access or use appropriate synchronization primitives.
229//!
230//! # Specification
231//!
232//! - [W3C WebRTC 1.0] - Main specification
233//! - [RFC 9429] - JSEP: JavaScript Session Establishment Protocol
234//! - [RFC 8866] - SDP: Session Description Protocol
235//! - [RFC 8445] - ICE: Interactive Connectivity Establishment
236//! - [RFC 8831] - WebRTC Data Channels
237//!
238//! [W3C WebRTC 1.0]: https://www.w3.org/TR/webrtc/
239//! [RFC 9429]: https://datatracker.ietf.org/doc/html/rfc9429
240//! [RFC 8866]: https://datatracker.ietf.org/doc/html/rfc8866
241//! [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445
242//! [RFC 8831]: https://datatracker.ietf.org/doc/html/rfc8831
243
244pub mod certificate;
245pub mod configuration;
246pub mod event;
247pub(crate) mod handler;
248mod internal;
249pub mod message;
250pub mod sdp;
251pub mod state;
252pub mod transport;
253
254use crate::data_channel::init::RTCDataChannelInit;
255use crate::data_channel::parameters::DataChannelParameters;
256use crate::data_channel::registry::DataChannelRegistry;
257use crate::data_channel::state::RTCDataChannelState;
258use crate::data_channel::{RTCDataChannel, RTCDataChannelId, internal::RTCDataChannelInternal};
259use crate::media_stream::track::MediaStreamTrack;
260use crate::peer_connection::configuration::media_engine::MediaEngine;
261use crate::peer_connection::configuration::setting_engine::{SctpMaxMessageSize, SettingEngine};
262use crate::peer_connection::configuration::{
263 RTCConfiguration, RTCIceTransportPolicy,
264 offer_answer_options::{RTCAnswerOptions, RTCOfferOptions},
265};
266use crate::peer_connection::event::RTCPeerConnectionEvent;
267use crate::peer_connection::handler::PipelineContext;
268use crate::peer_connection::handler::dtls::DtlsHandlerContext;
269use crate::peer_connection::handler::ice::IceHandlerContext;
270use crate::peer_connection::handler::sctp::SctpHandlerContext;
271use crate::peer_connection::sdp::MediaDescriptionExt;
272use crate::peer_connection::sdp::session_description::RTCSessionDescription;
273use crate::peer_connection::sdp::{
274 extract_fingerprint, extract_ice_details, get_application_media,
275 get_application_media_section_max_message_size, get_application_media_section_sctp_port,
276 get_mid_value, get_peer_direction, has_ice_trickle_option, is_lite_set, sdp_type::RTCSdpType,
277 update_sdp_origin,
278};
279use crate::peer_connection::state::RTCIceGatheringState;
280use crate::peer_connection::state::ice_connection_state::RTCIceConnectionState;
281use crate::peer_connection::state::peer_connection_state::{
282 NegotiationNeededState, RTCPeerConnectionState,
283};
284use crate::peer_connection::state::signaling_state::{RTCSignalingState, StateChangeOp};
285use crate::peer_connection::transport::RTCSctpTransport;
286use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint;
287use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters;
288use crate::peer_connection::transport::dtls::role::{
289 DEFAULT_DTLS_ROLE_ANSWER, DEFAULT_DTLS_ROLE_OFFER, RTCDtlsRole,
290};
291use crate::peer_connection::transport::dtls::{DtlsTransport, RTCDtlsTransportConfig};
292use crate::peer_connection::transport::ice::IceTransport;
293use crate::peer_connection::transport::ice::candidate::RTCIceCandidateInit;
294use crate::peer_connection::transport::ice::parameters::RTCIceParameters;
295use crate::peer_connection::transport::ice::role::RTCIceRole;
296use crate::peer_connection::transport::sctp::SctpTransport;
297use crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities;
298use crate::rtp_transceiver::direction::RTCRtpTransceiverDirection;
299use crate::rtp_transceiver::rtp_receiver::RTCRtpReceiver;
300use crate::rtp_transceiver::rtp_sender::RTCRtpCodecParameters;
301use crate::rtp_transceiver::rtp_sender::RTCRtpSender;
302use crate::rtp_transceiver::rtp_sender::internal::RTCRtpSenderInternal;
303use crate::rtp_transceiver::rtp_sender::rtp_codec::{
304 CodecMatch, RtpCodecKind, codec_parameters_fuzzy_search,
305};
306use crate::rtp_transceiver::{
307 RTCRtpReceiverId, RTCRtpSenderId, RTCRtpTransceiver, RTCRtpTransceiverId,
308 RTCRtpTransceiverInit, internal::RTCRtpTransceiverInternal,
309};
310use crate::statistics::StatsSelector;
311use crate::statistics::accumulator::RTCStatsAccumulator;
312use crate::statistics::report::RTCStatsReport;
313use ::sdp::description::session::Origin;
314use ::sdp::util::ConnectionRole;
315use ice::AgentConfig;
316use ice::candidate::{Candidate, unmarshal_candidate};
317use interceptor::{Interceptor, Registry};
318use shared::error::{Error, Result};
319use shared::util::math_rand_alpha;
320use std::time::Instant;
321
322/// Builder for creating RTCPeerConnection instances.
323///
324/// This builder provides a fluent API for configuring peer connections with:
325/// - ICE servers (STUN/TURN) via [`RTCConfiguration`]
326/// - Media codecs and RTP extensions via [`MediaEngine`]
327/// - Low-level transport settings via [`SettingEngine`]
328/// - RTP/RTCP interceptors for NACK, TWCC, and RTCP reports
329///
330/// # Examples
331///
332/// ## Basic peer connection
333///
334/// ```
335/// # use std::time::Instant;
336/// use rtc::peer_connection::RTCPeerConnectionBuilder;
337///
338/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
339/// let pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
340/// # Ok(())
341/// # }
342/// ```
343///
344/// ## With ICE servers
345///
346/// ```
347/// # use std::time::Instant;
348/// use rtc::peer_connection::RTCPeerConnectionBuilder;
349/// use rtc::peer_connection::configuration::{RTCConfigurationBuilder, RTCIceServer};
350///
351/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
352/// let pc = RTCPeerConnectionBuilder::new()
353/// .with_configuration(
354/// RTCConfigurationBuilder::new()
355/// .with_ice_servers(vec![RTCIceServer {
356/// urls: vec!["stun:stun.l.google.com:19302".to_string()],
357/// ..Default::default()
358/// }])
359/// .build()
360/// )
361/// .build(Instant::now())?;
362/// # Ok(())
363/// # }
364/// ```
365///
366/// ## With custom media engine
367///
368/// ```
369/// # use std::time::Instant;
370/// use rtc::peer_connection::RTCPeerConnectionBuilder;
371/// use rtc::peer_connection::configuration::media_engine::MediaEngine;
372///
373/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
374/// let mut media_engine = MediaEngine::default();
375/// media_engine.register_default_codecs()?;
376///
377/// let pc = RTCPeerConnectionBuilder::new()
378/// .with_media_engine(media_engine)
379/// .build(Instant::now())?;
380/// # Ok(())
381/// # }
382/// ```
383///
384/// ## With interceptors
385///
386/// ```
387/// # use std::time::Instant;
388/// use rtc::peer_connection::RTCPeerConnectionBuilder;
389/// use rtc::peer_connection::configuration::media_engine::MediaEngine;
390/// use rtc::peer_connection::configuration::interceptor_registry::{
391/// Registry, register_default_interceptors,
392/// };
393///
394/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
395/// let mut media_engine = MediaEngine::default();
396/// let registry = Registry::new();
397/// let registry = register_default_interceptors(registry, &mut media_engine)?;
398///
399/// let pc = RTCPeerConnectionBuilder::new()
400/// .with_media_engine(media_engine)
401/// .with_interceptor_registry(registry)
402/// .build(Instant::now())?;
403/// # Ok(())
404/// # }
405/// ```
406#[derive(Default)]
407pub struct RTCPeerConnectionBuilder {
408 configuration: RTCConfiguration,
409 media_engine: MediaEngine,
410 setting_engine: SettingEngine,
411 interceptor_registry: Registry,
412}
413
414impl RTCPeerConnectionBuilder {
415 /// Creates a new RTCPeerConnectionBuilder with default configuration.
416 ///
417 /// The default builder includes:
418 /// - Empty ICE server list
419 /// - Default MediaEngine (no codecs registered)
420 /// - Default SettingEngine (standard timeouts and limits)
421 /// - NoopInterceptor (no RTP/RTCP processing)
422 ///
423 /// Use `with_*` methods to customize configuration before calling `build()`.
424 ///
425 /// # Examples
426 ///
427 /// ```
428 /// # use std::time::Instant;
429 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
430 ///
431 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
432 /// let pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
433 /// # Ok(())
434 /// # }
435 /// ```
436 pub fn new() -> Self {
437 Self::default()
438 }
439}
440
441impl RTCPeerConnectionBuilder {
442 /// Sets the RTCConfiguration for the peer connection.
443 ///
444 /// The configuration includes ICE servers, transport policies, bundle policies,
445 /// RTCP mux policies, and certificates.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// # use std::time::Instant;
451 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
452 /// use rtc::peer_connection::configuration::{RTCConfigurationBuilder, RTCIceServer};
453 ///
454 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
455 /// let config = RTCConfigurationBuilder::new()
456 /// .with_ice_servers(vec![RTCIceServer {
457 /// urls: vec!["stun:stun.l.google.com:19302".to_string()],
458 /// ..Default::default()
459 /// }])
460 /// .build();
461 ///
462 /// let pc = RTCPeerConnectionBuilder::new()
463 /// .with_configuration(config)
464 /// .build(Instant::now())?;
465 /// # Ok(())
466 /// # }
467 /// ```
468 pub fn with_configuration(mut self, configuration: RTCConfiguration) -> Self {
469 self.configuration = configuration;
470 self
471 }
472
473 /// Sets the MediaEngine for the peer connection.
474 ///
475 /// The media engine configures codecs and RTP header extensions.
476 ///
477 /// # Examples
478 ///
479 /// ```
480 /// # use std::time::Instant;
481 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
482 /// use rtc::peer_connection::configuration::media_engine::MediaEngine;
483 ///
484 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
485 /// let mut media_engine = MediaEngine::default();
486 /// media_engine.register_default_codecs()?;
487 ///
488 /// let pc = RTCPeerConnectionBuilder::new()
489 /// .with_media_engine(media_engine)
490 /// .build(Instant::now())?;
491 /// # Ok(())
492 /// # }
493 /// ```
494 pub fn with_media_engine(mut self, media_engine: MediaEngine) -> Self {
495 self.media_engine = media_engine;
496 self
497 }
498
499 /// Sets the SettingEngine for the peer connection.
500 ///
501 /// The setting engine configures low-level transport parameters including
502 /// timeouts, buffer sizes, ICE settings, and SCTP parameters.
503 ///
504 /// # Examples
505 ///
506 /// ```
507 /// # use std::time::Instant;
508 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
509 /// use rtc::peer_connection::configuration::setting_engine::SettingEngineBuilder;
510 /// use std::time::Duration;
511 ///
512 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
513 /// let setting_engine = SettingEngineBuilder::new()
514 /// .with_ice_timeouts(
515 /// Some(Duration::from_secs(30)),
516 /// Some(Duration::from_secs(60)),
517 /// Some(Duration::from_millis(100)),
518 /// )
519 /// .build();
520 ///
521 /// let pc = RTCPeerConnectionBuilder::new()
522 /// .with_setting_engine(setting_engine)
523 /// .build(Instant::now())?;
524 /// # Ok(())
525 /// # }
526 /// ```
527 pub fn with_setting_engine(mut self, setting_engine: SettingEngine) -> Self {
528 self.setting_engine = setting_engine;
529 self
530 }
531
532 /// Configures the peer connection with an interceptor registry.
533 ///
534 /// Interceptors process RTP/RTCP packets as they flow through the pipeline,
535 /// enabling features like:
536 /// - NACK (Negative Acknowledgment) for packet loss recovery
537 /// - TWCC (Transport-Wide Congestion Control) for bandwidth estimation
538 /// - RTCP Reports for quality statistics
539 ///
540 /// This method replaces the builder's interceptor type — `NoopInterceptor` by default —
541 /// with the registry's, so it returns a `RTCPeerConnectionBuilder<P>` rather than
542 /// `Self`. Every other builder setting is carried over, and the remaining setters are
543 /// available on the returned builder, so this does not have to be the last call before
544 /// `build()`; it is simply the only one that changes the builder's type.
545 ///
546 /// # Type Parameters
547 ///
548 /// * `P` - The interceptor type produced by the registry
549 ///
550 /// # Examples
551 ///
552 /// ```
553 /// # use std::time::Instant;
554 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
555 /// use rtc::peer_connection::configuration::media_engine::MediaEngine;
556 /// use rtc::peer_connection::configuration::interceptor_registry::{
557 /// Registry, register_default_interceptors,
558 /// };
559 ///
560 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
561 /// let mut media_engine = MediaEngine::default();
562 /// let registry = Registry::new();
563 /// let registry = register_default_interceptors(registry, &mut media_engine)?;
564 ///
565 /// let pc = RTCPeerConnectionBuilder::new()
566 /// .with_media_engine(media_engine)
567 /// .with_interceptor_registry(registry)
568 /// .build(Instant::now())?;
569 /// # Ok(())
570 /// # }
571 /// ```
572 ///
573 /// # One connection type, whatever the chain
574 ///
575 /// A chain is a flat list of interceptors, so the connection has one concrete type whatever
576 /// the list contains. That is what lets a non-generic struct own one, or two connections with
577 /// *different* chains share a collection:
578 ///
579 /// ```
580 /// # use std::time::Instant;
581 /// use rtc::peer_connection::configuration::interceptor_registry::{
582 /// Registry, register_default_interceptors,
583 /// };
584 /// use rtc::peer_connection::configuration::media_engine::MediaEngine;
585 /// use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder};
586 ///
587 /// struct Session {
588 /// peer_connection: RTCPeerConnection, // no type parameter
589 /// }
590 ///
591 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
592 /// let mut media_engine = MediaEngine::default();
593 /// let registry =
594 /// register_default_interceptors(Registry::new(), &mut media_engine)?;
595 ///
596 /// let session = Session {
597 /// peer_connection: RTCPeerConnectionBuilder::new()
598 /// .with_media_engine(media_engine)
599 /// .with_interceptor_registry(registry)
600 /// .build(Instant::now())?,
601 /// };
602 /// # Ok(())
603 /// # }
604 /// ```
605 ///
606 /// The registry is assembled into the chain by [`build`](RTCPeerConnectionBuilder::build),
607 /// so it stays a list — inspectable, extendable — right up until the connection is made.
608 pub fn with_interceptor_registry(mut self, interceptor_registry: Registry) -> Self {
609 self.interceptor_registry = interceptor_registry;
610 self
611 }
612
613 /// Builds the RTCPeerConnection with the configured settings.
614 ///
615 /// This method validates the configuration and creates a new peer connection.
616 /// If validation fails (e.g., expired certificates, invalid ICE servers),
617 /// an error is returned.
618 ///
619 /// # Errors
620 ///
621 /// Returns an error if:
622 /// - Certificates have expired
623 /// - ICE server URLs are invalid
624 /// - Other validation checks fail
625 ///
626 /// # Examples
627 ///
628 /// ```
629 /// # use std::time::Instant;
630 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
631 ///
632 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
633 /// let pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
634 /// # Ok(())
635 /// # }
636 /// ```
637 pub fn build(self, now: Instant) -> Result<RTCPeerConnection> {
638 RTCPeerConnection::new(
639 now,
640 self.configuration,
641 self.media_engine,
642 self.setting_engine,
643 // The registry is a list until here; `build` freezes it into the chain.
644 Box::new(self.interceptor_registry.build()),
645 )
646 }
647}
648
649/// The `RTCPeerConnection` interface represents a WebRTC connection between the local computer
650/// and a remote peer. It provides methods to connect to a remote peer, maintain and monitor
651/// the connection, and close the connection once it's no longer needed.
652///
653/// This is a sans-I/O implementation following the [W3C WebRTC specification](https://www.w3.org/TR/webrtc/).
654///
655/// # Driving the connection
656///
657/// This type performs **no I/O and reads no clock**. It never opens a socket, never sleeps, and
658/// never calls `Instant::now()` on your behalf: every method that needs the time takes it as an
659/// argument. Progress happens only when you feed it something. That is what makes a whole
660/// session reproducible in a test with no socket and no sleep.
661///
662/// The driving methods come from the [`sansio::Protocol`] trait, so **the trait must be in scope**
663/// (`use rtc::sansio::Protocol;`) or none of them will resolve:
664///
665/// | Method | Direction | What it does |
666/// | --- | --- | --- |
667/// | [`handle_read`] | in | Feed one received datagram, tagged with its 5-tuple and arrival instant |
668/// | [`poll_write`] | out | Take the next packet to put on the wire; drain until `None` |
669/// | [`poll_read`] | out | Take the next inbound RTP/RTCP/data-channel message for the application |
670/// | [`poll_event`] | out | Take the next state change or notification |
671/// | [`poll_timeout`] | out | Next deadline for retransmissions, keepalives and ICE checks |
672/// | [`handle_timeout`] | in | Report that a deadline has passed |
673/// | [`handle_write`] | in | Queue an outbound RTP/RTCP/data-channel message |
674/// | [`close`] | in | Shut the connection down |
675///
676/// Nothing is sent eagerly: `handle_read`, `handle_timeout`, `handle_write` and the negotiation
677/// methods all *queue* packets, so drain `poll_write` after any of them.
678///
679/// [`handle_event`] also exists on the trait, but [`RTCEvent`] is uninhabited — no value of it can
680/// be constructed, so there is nothing to call it with. It reserves the signature for the first
681/// inbound event variant.
682///
683/// # Back-pressure
684///
685/// [`poll_read`] is the throttle. Undrained data-channel messages leave bytes in SCTP's reassembly
686/// queue, which lowers the receiver-window credit advertised in every SACK, which tells the peer to
687/// slow down. **Declining to call `poll_read` is how back-pressure is applied**; resume when the
688/// application catches up. Media is never throttled this way: RTP arrives over SRTP and is subject
689/// to none of SCTP's flow control, and `poll_read` interleaves the two by the instant each packet
690/// was observed, so a stalled data channel cannot starve video.
691///
692/// # ICE candidates
693///
694/// Because there is no I/O here, there is no candidate gathering either: the application owns the
695/// sockets, so it owns gathering. Hand every local candidate to [`Self::add_local_candidate`], and
696/// finish with an empty candidate string to signal end-of-gathering — see that method for the
697/// details.
698///
699/// # Examples
700///
701/// ```
702/// # use std::time::Instant;
703/// use rtc::peer_connection::RTCPeerConnectionBuilder;
704///
705/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
706/// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
707/// # Ok(())
708/// # }
709/// ```
710///
711/// A complete event loop is shown in the [crate-level documentation](crate).
712///
713/// [`sansio::Protocol`]: sansio::Protocol
714/// [`handle_read`]: sansio::Protocol::handle_read
715/// [`poll_write`]: sansio::Protocol::poll_write
716/// [`poll_read`]: sansio::Protocol::poll_read
717/// [`poll_event`]: sansio::Protocol::poll_event
718/// [`poll_timeout`]: sansio::Protocol::poll_timeout
719/// [`handle_timeout`]: sansio::Protocol::handle_timeout
720/// [`handle_write`]: sansio::Protocol::handle_write
721/// [`handle_event`]: sansio::Protocol::handle_event
722/// [`close`]: sansio::Protocol::close
723/// [`RTCEvent`]: crate::peer_connection::event::RTCEvent
724pub struct RTCPeerConnection {
725 //////////////////////////////////////////////////
726 // PeerConnection WebRTC Spec Interface Definition
727 //////////////////////////////////////////////////
728 pub(crate) configuration: RTCConfiguration,
729 pub(crate) media_engine: MediaEngine,
730 pub(crate) setting_engine: SettingEngine,
731 pub(crate) interceptor: Box<dyn Interceptor>,
732
733 local_description: Option<RTCSessionDescription>,
734 current_local_description: Option<RTCSessionDescription>,
735 pending_local_description: Option<RTCSessionDescription>,
736 remote_description: Option<RTCSessionDescription>,
737 current_remote_description: Option<RTCSessionDescription>,
738 pending_remote_description: Option<RTCSessionDescription>,
739
740 pub(crate) signaling_state: RTCSignalingState,
741 pub(crate) peer_connection_state: RTCPeerConnectionState,
742 can_trickle_ice_candidates: Option<bool>,
743
744 //////////////////////////////////////////////////
745 // PeerConnection Internal State Machine
746 //////////////////////////////////////////////////
747 pub(crate) pipeline_context: PipelineContext,
748 pub(crate) data_channels: DataChannelRegistry,
749 pub(super) rtp_transceivers: Vec<RTCRtpTransceiverInternal>,
750
751 greater_mid: isize,
752 sdp_origin: Origin,
753 last_offer: String,
754 last_answer: String,
755
756 ice_restart_requested: Option<RTCOfferOptions>,
757 negotiation_needed_state: NegotiationNeededState,
758 is_negotiation_ongoing: bool,
759}
760
761impl RTCPeerConnection {
762 /// Creates an SDP offer to start a new WebRTC connection to a remote peer.
763 ///
764 /// The offer includes information about the attached media tracks, codecs and options supported
765 /// by the browser, and ICE candidates gathered by the ICE agent. This offer can be sent to a
766 /// remote peer over a signaling channel to establish a connection.
767 ///
768 /// # Parameters
769 ///
770 /// * `options` - Optional configuration for the offer, such as whether to restart ICE.
771 ///
772 /// # Returns
773 ///
774 /// Returns an `RTCSessionDescription` containing the SDP offer.
775 ///
776 /// # Errors
777 ///
778 /// Returns an error if:
779 /// - The peer connection is closed
780 /// - There's an error generating the SDP
781 ///
782 /// # Specification
783 ///
784 /// See [createOffer](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createoffer)
785 pub fn create_offer(
786 &mut self,
787 mut options: Option<RTCOfferOptions>,
788 ) -> Result<RTCSessionDescription> {
789 if self.peer_connection_state == RTCPeerConnectionState::Closed {
790 return Err(Error::ErrConnectionClosed);
791 }
792
793 // Staging, not restarting: the offer must advertise fresh ICE credentials, but JSEP
794 // requires `createOffer` to be free of side effects, and inbound STUN is still being
795 // validated against the current ufrag/pwd. `set_local_description` applies the restart.
796 let is_ice_restart_requested = self
797 .ice_restart_requested
798 .take()
799 .is_some_and(|options| options.ice_restart)
800 || options.take().is_some_and(|options| options.ice_restart);
801
802 if is_ice_restart_requested {
803 self.stage_ice_restart()?;
804 }
805
806 // include unmatched local transceivers
807 // update the greater mid if the remote description provides a greater one
808 if let Some(d) = self.current_remote_description.as_ref()
809 && let Some(parsed) = &d.parsed
810 {
811 for media in &parsed.media_descriptions {
812 if let Some(mid) = get_mid_value(media) {
813 if mid.is_empty() {
814 continue;
815 }
816 let numeric_mid = match mid.parse::<isize>() {
817 Ok(n) => n,
818 Err(_) => continue,
819 };
820 if numeric_mid > self.greater_mid {
821 self.greater_mid = numeric_mid;
822 }
823 }
824 }
825 }
826 for transceiver in &mut self.rtp_transceivers {
827 if let Some(mid) = transceiver.mid()
828 && !mid.is_empty()
829 {
830 if let Ok(numeric_mid) = mid.parse::<isize>()
831 && numeric_mid > self.greater_mid
832 {
833 self.greater_mid = numeric_mid;
834 }
835 } else {
836 self.greater_mid += 1;
837 transceiver.set_mid(format!("{}", self.greater_mid))?;
838 }
839 }
840
841 let mut d = if self.current_remote_description.is_none() {
842 self.generate_unmatched_sdp()?
843 } else {
844 self.generate_matched_sdp(
845 true, /*includeUnmatched */
846 DEFAULT_DTLS_ROLE_OFFER.to_connection_role(),
847 false,
848 )?
849 };
850
851 update_sdp_origin(&mut self.sdp_origin, &mut d);
852
853 let sdp = d.marshal();
854
855 let offer = RTCSessionDescription {
856 sdp_type: RTCSdpType::Offer,
857 sdp,
858 parsed: Some(d),
859 };
860
861 self.last_offer.clone_from(&offer.sdp);
862
863 Ok(offer)
864 }
865
866 /// Creates an SDP answer in response to an offer from a remote peer.
867 ///
868 /// This method must be called after `set_remote_description()` has been called
869 /// with an offer. The answer describes which media formats and codecs this peer
870 /// will accept and how the connection will be established.
871 ///
872 /// # Parameters
873 ///
874 /// - `options`: Optional answer configuration. Currently not used but reserved
875 /// for future extensions.
876 ///
877 /// # Returns
878 ///
879 /// Returns an `RTCSessionDescription` containing the SDP answer that should be
880 /// set as the local description and sent to the remote peer.
881 ///
882 /// # Errors
883 ///
884 /// Returns an error if:
885 /// - No remote description has been set (`ErrNoRemoteDescription`)
886 /// - The peer connection is closed (`ErrConnectionClosed`)
887 /// - The signaling state is incorrect (`ErrIncorrectSignalingState`)
888 /// - SDP generation fails
889 ///
890 /// # Signaling State Requirements
891 ///
892 /// This method can only be called when the signaling state is:
893 /// - `HaveRemoteOffer` - After receiving an initial offer
894 /// - `HaveLocalPranswer` - After sending a provisional answer
895 ///
896 /// # Examples
897 ///
898 /// ## Basic Answer Flow
899 ///
900 /// ```no_run
901 /// # use std::time::Instant;
902 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
903 /// use rtc::peer_connection::sdp::RTCSessionDescription;
904 ///
905 /// # fn example(remote_offer_sdp: String) -> Result<(), Box<dyn std::error::Error>> {
906 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
907 ///
908 /// // 1. Receive and set remote offer
909 /// let offer = RTCSessionDescription::offer(remote_offer_sdp)?;
910 /// pc.set_remote_description(Instant::now(), offer)?;
911 ///
912 /// // 2. Create answer
913 /// let answer = pc.create_answer(None)?;
914 ///
915 /// // 3. Set as local description
916 /// pc.set_local_description(Instant::now(), answer.clone())?;
917 ///
918 /// // 4. Send answer to remote peer
919 /// // signaling_channel.send(answer.sdp)?;
920 /// # Ok(())
921 /// # }
922 /// ```
923 ///
924 /// ## With Media Tracks
925 ///
926 /// ```no_run
927 /// # use std::time::Instant;
928 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
929 /// use rtc::peer_connection::sdp::RTCSessionDescription;
930 /// use rtc::media_stream::MediaStreamTrack;
931 ///
932 /// # fn example(
933 /// # remote_offer_sdp: String,
934 /// # audio_track: MediaStreamTrack,
935 /// # ) -> Result<(), Box<dyn std::error::Error>> {
936 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
937 ///
938 /// // Set remote offer
939 /// let offer = RTCSessionDescription::offer(remote_offer_sdp)?;
940 /// pc.set_remote_description(Instant::now(), offer)?;
941 ///
942 /// // Add local track before creating answer
943 /// pc.add_track(audio_track)?;
944 ///
945 /// // Create answer (will include the track)
946 /// let answer = pc.create_answer(None)?;
947 /// pc.set_local_description(Instant::now(), answer)?;
948 /// # Ok(())
949 /// # }
950 /// ```
951 ///
952 /// # DTLS Role Selection
953 ///
954 /// The answer automatically determines the appropriate DTLS role:
955 /// - Uses `answering_dtls_role` from settings if configured
956 /// - Defaults to `Client` (active) for lower latency
957 /// - Uses `Server` (passive) if remote is ICE-Lite
958 ///
959 /// # Specification
960 ///
961 /// - [W3C RTCPeerConnection.createAnswer]
962 /// - [RFC 9429 Section 5.3] - Generating an Answer
963 ///
964 /// [W3C RTCPeerConnection.createAnswer]: https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createanswer
965 /// [RFC 9429 Section 5.3]: https://datatracker.ietf.org/doc/html/rfc9429#section-5.3
966 pub fn create_answer(
967 &mut self,
968 _options: Option<RTCAnswerOptions>,
969 ) -> Result<RTCSessionDescription> {
970 if self.remote_description().is_none() {
971 return Err(Error::ErrNoRemoteDescription);
972 }
973
974 if self.peer_connection_state == RTCPeerConnectionState::Closed {
975 return Err(Error::ErrConnectionClosed);
976 }
977
978 if self.signaling_state != RTCSignalingState::HaveRemoteOffer
979 && self.signaling_state != RTCSignalingState::HaveLocalPranswer
980 {
981 return Err(Error::ErrIncorrectSignalingState);
982 }
983
984 let mut connection_role = self.setting_engine.answering_dtls_role.to_connection_role();
985 // RFC 5763 §5: "The answerer MUST use either a setup attribute value of setup:active or
986 // setup:passive." `actpass` is an offer-only value, so an answering role of
987 // `RTCDtlsRole::Auto` — which maps to it — means "no preference" and has to be resolved
988 // to a concrete role here, exactly like `Unspecified`.
989 //
990 // Letting `actpass` through leaves the role genuinely unnegotiated: the answerer falls
991 // back to its own `answering_dtls_role`, and the offerer, seeing no explicit role in the
992 // answer, falls back to *its* configured role. When those agree — e.g. an offerer pinned
993 // to `Client` answered by a peer configured `Auto` — both endpoints become the DTLS
994 // client, both wait for a ClientHello, and the handshake never completes.
995 if matches!(
996 connection_role,
997 ConnectionRole::Unspecified | ConnectionRole::Actpass
998 ) {
999 connection_role = DEFAULT_DTLS_ROLE_ANSWER.to_connection_role();
1000
1001 if let Some(remote_description) = self.remote_description()
1002 && let Some(parsed) = remote_description.parsed.as_ref()
1003 && is_lite_set(parsed)
1004 && !self.setting_engine.candidates.ice_lite
1005 {
1006 connection_role = RTCDtlsRole::Server.to_connection_role();
1007 }
1008 }
1009
1010 let mut d = self.generate_matched_sdp(
1011 false, /*includeUnmatched */
1012 connection_role,
1013 self.setting_engine.ignore_rid_pause_for_recv,
1014 )?;
1015
1016 update_sdp_origin(&mut self.sdp_origin, &mut d);
1017
1018 let sdp = d.marshal();
1019
1020 let answer = RTCSessionDescription {
1021 sdp_type: RTCSdpType::Answer,
1022 sdp,
1023 parsed: Some(d),
1024 };
1025
1026 self.last_answer.clone_from(&answer.sdp);
1027
1028 Ok(answer)
1029 }
1030
1031 /// Sets the local description for this peer connection.
1032 ///
1033 /// This method applies a local SDP description (offer or answer) to the peer
1034 /// connection, updating the local media and transport configuration. It must be
1035 /// called after creating an offer or answer.
1036 ///
1037 /// # Parameters
1038 ///
1039 /// - `local_description`: The session description to set as the local description.
1040 /// This should be an offer or answer created by `create_offer()` or `create_answer()`.
1041 ///
1042 /// # Returns
1043 ///
1044 /// Returns `Ok(())` on success.
1045 ///
1046 /// # Errors
1047 ///
1048 /// Returns an error if:
1049 /// - The peer connection is closed (`ErrConnectionClosed`)
1050 /// - The SDP type is invalid for the current signaling state
1051 /// - SDP parsing fails
1052 /// - Transport configuration fails
1053 ///
1054 /// # Signaling State Transitions
1055 ///
1056 /// Setting the local description causes signaling state transitions:
1057 ///
1058 /// - **Offer**: `Stable` → `HaveLocalOffer`
1059 /// - **Answer**: `HaveRemoteOffer` → `Stable`
1060 /// - **Pranswer**: `HaveRemoteOffer` → `HaveLocalPranswer`
1061 ///
1062 /// # Examples
1063 ///
1064 /// ## Setting Local Offer
1065 ///
1066 /// ```no_run
1067 /// # use std::time::Instant;
1068 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
1069 ///
1070 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1071 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
1072 ///
1073 /// // Create offer
1074 /// let offer = pc.create_offer(None)?;
1075 ///
1076 /// // Set as local description
1077 /// pc.set_local_description(Instant::now(), offer.clone())?;
1078 ///
1079 /// // Now send offer.sdp to remote peer via signaling
1080 /// // signaling_channel.send(offer.sdp)?;
1081 /// # Ok(())
1082 /// # }
1083 /// ```
1084 ///
1085 /// ## Setting Local Answer
1086 ///
1087 /// ```no_run
1088 /// # use std::time::Instant;
1089 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
1090 /// use rtc::peer_connection::sdp::RTCSessionDescription;
1091 ///
1092 /// # fn example(remote_offer_sdp: String) -> Result<(), Box<dyn std::error::Error>> {
1093 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
1094 ///
1095 /// // Set remote offer first
1096 /// let offer = RTCSessionDescription::offer(remote_offer_sdp)?;
1097 /// pc.set_remote_description(Instant::now(), offer)?;
1098 ///
1099 /// // Create and set local answer
1100 /// let answer = pc.create_answer(None)?;
1101 /// pc.set_local_description(Instant::now(), answer.clone())?;
1102 ///
1103 /// // Send answer to remote peer
1104 /// // signaling_channel.send(answer.sdp)?;
1105 /// # Ok(())
1106 /// # }
1107 /// ```
1108 ///
1109 /// # Empty SDP Handling (JSEP 5.4)
1110 ///
1111 /// If the SDP string is empty, the last offer or answer is reused:
1112 /// - For offers: Uses the last generated offer
1113 /// - For answers: Uses the last generated answer
1114 ///
1115 /// This allows re-applying descriptions without regenerating SDP.
1116 ///
1117 /// # Media and Transport Activation
1118 ///
1119 /// When setting a local answer:
1120 /// - RTP transceivers are activated
1121 /// - SCTP transport is started for data channels
1122 /// - Media can begin flowing
1123 ///
1124 /// # Specification
1125 ///
1126 /// - [W3C RTCPeerConnection.setLocalDescription]
1127 /// - [RFC 9429 Section 5.9] - Applying a Local Description
1128 ///
1129 /// [W3C RTCPeerConnection.setLocalDescription]: https://www.w3.org/TR/webrtc/#dom-peerconnection-setlocaldescription
1130 /// [RFC 9429 Section 5.9]: https://datatracker.ietf.org/doc/html/rfc9429#section-5.9
1131 pub fn set_local_description(
1132 &mut self,
1133 now: Instant,
1134 mut local_description: RTCSessionDescription,
1135 ) -> Result<()> {
1136 if self.peer_connection_state == RTCPeerConnectionState::Closed {
1137 return Err(Error::ErrConnectionClosed);
1138 }
1139
1140 // Apply an ICE restart staged by `create_offer`. A no-op if none was staged, so an offer
1141 // that was created and then discarded leaves the existing session untouched.
1142 if self.ice_transport().has_pending_restart() {
1143 self.apply_ice_restart(now)?;
1144 }
1145
1146 // JSEP 5.4
1147 if local_description.sdp.is_empty() {
1148 match local_description.sdp_type {
1149 RTCSdpType::Answer | RTCSdpType::Pranswer => {
1150 local_description.sdp.clone_from(&self.last_answer);
1151 }
1152 RTCSdpType::Offer => {
1153 local_description.sdp.clone_from(&self.last_offer);
1154 }
1155 RTCSdpType::Rollback => {
1156 // WebRTC spec: rollback SDP is ignored, empty is allowed
1157 }
1158 _ => return Err(Error::ErrPeerConnSDPTypeInvalidValueSetLocalDescription),
1159 }
1160 }
1161
1162 // Parse SDP (skip for rollback as content is ignored per spec)
1163 if local_description.sdp_type != RTCSdpType::Rollback {
1164 local_description.parsed = Some(local_description.unmarshal()?);
1165 }
1166 self.set_description(&local_description, StateChangeOp::SetLocal)?;
1167
1168 let we_answer = local_description.sdp_type == RTCSdpType::Answer;
1169 if we_answer && let Some(parsed_local_description) = &local_description.parsed {
1170 // WebRTC Spec 1.0 https://www.w3.org/TR/webrtc/
1171 // Section 4.4.1.5
1172 for media in &parsed_local_description.media_descriptions {
1173 let mid_value = match get_mid_value(media) {
1174 Some(mid) if !mid.is_empty() => mid,
1175 _ => return Err(Error::ErrPeerConnLocalDescriptionWithoutMidValue),
1176 };
1177
1178 if media.is_webrtc_datachannel() {
1179 continue;
1180 }
1181
1182 let i = match RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers) {
1183 Some(i) => i,
1184 None => return Err(Error::ErrPeerConnTransceiverMidNil),
1185 };
1186
1187 let kind = RtpCodecKind::from(media.media_name.media.as_str());
1188 let mut direction = get_peer_direction(media);
1189 if kind == RtpCodecKind::Unspecified
1190 || direction == RTCRtpTransceiverDirection::Unspecified
1191 {
1192 continue;
1193 }
1194
1195 // If a transceiver is created by applying a remote description that has recvonly transceiver,
1196 // it will have no sender. In this case, the transceiver's current direction is set to inactive so
1197 // that the transceiver can be reused by next AddTrack.
1198 if direction == RTCRtpTransceiverDirection::Sendonly
1199 && self.rtp_transceivers[i].sender().is_none()
1200 {
1201 direction = RTCRtpTransceiverDirection::Inactive;
1202 }
1203
1204 self.rtp_transceivers[i].set_current_direction(direction);
1205 }
1206
1207 if let Some(remote_description) = self.remote_description().cloned()
1208 && let Some(parsed_remote_description) = remote_description.parsed.as_ref()
1209 {
1210 // only start sctp transport if application media has been negotiated
1211 if let (Some(local_application_media), Some(remote_application_media)) = (
1212 get_application_media(parsed_local_description),
1213 get_application_media(parsed_remote_description),
1214 ) {
1215 let (dtls_role, remote_caps, local_sctp_port, remote_sctp_port) = (
1216 self.dtls_transport().role(),
1217 SCTPTransportCapabilities {
1218 max_message_size: get_application_media_section_max_message_size(
1219 remote_application_media,
1220 )
1221 .unwrap_or(SctpMaxMessageSize::DEFAULT_MESSAGE_SIZE),
1222 },
1223 get_application_media_section_sctp_port(local_application_media)
1224 .unwrap_or(5000),
1225 get_application_media_section_sctp_port(remote_application_media)
1226 .unwrap_or(5000),
1227 );
1228
1229 // we_answer: we first call set_remote_description,
1230 // then, we create_answer() and set_local_description() here
1231 // Now we should have done SDP negotiation.
1232 // Therefore, it is ready to start sctp and rtp.
1233 self.sctp_transport_mut().start(
1234 dtls_role,
1235 remote_caps,
1236 local_sctp_port,
1237 remote_sctp_port,
1238 )?;
1239 }
1240 self.start_rtp(remote_description)?;
1241 }
1242 }
1243
1244 self.ice_transport_mut().ice_gathering_state = RTCIceGatheringState::Gathering;
1245
1246 Ok(())
1247 }
1248
1249 /// Returns the local session description.
1250 ///
1251 /// Returns `pending_local_description` if it is not null, otherwise returns
1252 /// `current_local_description`. This property is used to determine if
1253 /// `set_local_description` has already been called.
1254 ///
1255 /// # Specification
1256 ///
1257 /// See [localDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-localdescription)
1258 pub fn local_description(&self) -> Option<RTCSessionDescription> {
1259 if let Some(pending_local_description) = self.pending_local_description() {
1260 return Some(pending_local_description);
1261 }
1262 self.current_local_description()
1263 }
1264
1265 /// Returns the current local description as last successfully negotiated since
1266 /// the last negotiation completed.
1267 ///
1268 /// This represents the local description from the last offer/answer exchange that was
1269 /// successfully applied, not including any offers currently being negotiated.
1270 ///
1271 /// Returns `None` if there is no current local description (e.g., before initial negotiation).
1272 ///
1273 /// # Specification
1274 ///
1275 /// See [currentLocalDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-currentlocaldesc)
1276 pub fn current_local_description(&self) -> Option<RTCSessionDescription> {
1277 self.populate_local_candidates(self.current_local_description.as_ref())
1278 }
1279
1280 /// Returns the pending local description if it exists.
1281 ///
1282 /// This represents the local description from a call to `set_local_description()` whose
1283 /// corresponding remote description has not yet been applied. This is `None` if negotiation
1284 /// is not in progress or if a rollback has been performed.
1285 ///
1286 /// Returns `None` if there is no pending local description.
1287 ///
1288 /// # Specification
1289 ///
1290 /// See [pendingLocalDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-pendinglocaldesc)
1291 pub fn pending_local_description(&self) -> Option<RTCSessionDescription> {
1292 self.populate_local_candidates(self.pending_local_description.as_ref())
1293 }
1294
1295 /// Returns whether the remote peer supports trickle ICE.
1296 ///
1297 /// This value is determined from the remote SDP description after `set_remote_description()`
1298 /// is called. It checks for "trickle" in the "ice-options" attribute per
1299 /// RFC 8838 and RFC 9429 section 4.1.17.
1300 ///
1301 /// Returns:
1302 /// - `None` if no remote description has been set yet (unknown)
1303 /// - `Some(true)` if the remote peer indicated trickle ICE support
1304 /// - `Some(false)` if the remote peer did not indicate support
1305 ///
1306 /// # Specification
1307 ///
1308 /// See [canTrickleIceCandidates](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-cantrickleicecandidates)
1309 pub fn can_trickle_ice_candidates(&self) -> Option<bool> {
1310 self.can_trickle_ice_candidates
1311 }
1312
1313 /// Sets the remote description as part of the offer/answer negotiation.
1314 ///
1315 /// This changes the remote description associated with the connection. This description
1316 /// specifies the properties of the remote end of the connection, including the media format.
1317 ///
1318 /// # Parameters
1319 ///
1320 /// * `remote_description` - The remote session description to set.
1321 ///
1322 /// # Errors
1323 ///
1324 /// Returns an error if:
1325 /// - The peer connection is closed
1326 /// - The SDP cannot be parsed
1327 /// - The media engine fails to update from the remote description
1328 ///
1329 /// # Specification
1330 ///
1331 /// See [setRemoteDescription](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setremotedescription!overload-1)
1332 pub fn set_remote_description(
1333 &mut self,
1334 now: Instant,
1335 mut remote_description: RTCSessionDescription,
1336 ) -> Result<()> {
1337 if self.peer_connection_state == RTCPeerConnectionState::Closed {
1338 return Err(Error::ErrConnectionClosed);
1339 }
1340
1341 let is_renegotiation = self.current_remote_description.is_some();
1342
1343 // Parse SDP (skip for rollback as content is ignored per spec)
1344 if remote_description.sdp_type != RTCSdpType::Rollback {
1345 remote_description.parsed = Some(remote_description.unmarshal()?);
1346 }
1347 self.set_description(&remote_description, StateChangeOp::SetRemote)?;
1348
1349 if let Some(parsed_remote_description) = &remote_description.parsed {
1350 self.media_engine
1351 .update_from_remote_description(parsed_remote_description)?;
1352
1353 // Detect trickle ICE support from remote SDP (RFC 8838/RFC 9429 section 4.1.17)
1354 // Check for "trickle" in space-separated "ice-options" attribute values
1355 let has_trickle_ice = has_ice_trickle_option(parsed_remote_description);
1356
1357 match remote_description.sdp_type {
1358 RTCSdpType::Offer | RTCSdpType::Answer | RTCSdpType::Pranswer => {
1359 self.can_trickle_ice_candidates = Some(has_trickle_ice);
1360 }
1361 _ => {
1362 // Rollback or other types: reset to unknown
1363 self.can_trickle_ice_candidates = None;
1364 }
1365 }
1366
1367 // Disable RTX/FEC on RTPSenders if the remote didn't support it
1368 for transceiver in &mut self.rtp_transceivers {
1369 if let Some(sender) = transceiver.sender_mut() {
1370 let (is_rtx_enabled, is_fec_enabled) = (
1371 self.media_engine
1372 .is_rtx_enabled(sender.kind(), RTCRtpTransceiverDirection::Sendonly),
1373 self.media_engine
1374 .is_fec_enabled(sender.kind(), RTCRtpTransceiverDirection::Sendonly),
1375 );
1376 sender.configure_rtx_and_fec(is_rtx_enabled, is_fec_enabled);
1377 }
1378 }
1379
1380 let we_offer = remote_description.sdp_type == RTCSdpType::Answer;
1381
1382 // Extract media descriptions to avoid borrowing conflicts
1383 let media_descriptions = self
1384 .remote_description()
1385 .as_ref()
1386 .and_then(|r| r.parsed.as_ref())
1387 .map(|parsed| parsed.media_descriptions.clone());
1388
1389 if let Some(media_descriptions) = media_descriptions {
1390 if !we_offer {
1391 for media in &media_descriptions {
1392 let mid_value = match get_mid_value(media) {
1393 Some(mid) if !mid.is_empty() => mid,
1394 _ => return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue),
1395 };
1396
1397 if media.is_webrtc_datachannel() {
1398 continue;
1399 }
1400
1401 let kind = RtpCodecKind::from(media.media_name.media.as_str());
1402 let direction = get_peer_direction(media);
1403 if kind == RtpCodecKind::Unspecified
1404 || direction == RTCRtpTransceiverDirection::Unspecified
1405 {
1406 continue;
1407 }
1408
1409 let transceiver = if let Some(i) =
1410 RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers)
1411 {
1412 if direction == RTCRtpTransceiverDirection::Inactive {
1413 self.rtp_transceivers[i]
1414 .stop(&self.media_engine, &mut self.interceptor)?;
1415 }
1416 Some(&mut self.rtp_transceivers[i])
1417 } else {
1418 RTCPeerConnection::satisfy_type_and_direction(
1419 kind,
1420 direction,
1421 &mut self.rtp_transceivers,
1422 )
1423 };
1424
1425 if let Some(transceiver) = transceiver {
1426 if direction == RTCRtpTransceiverDirection::Recvonly {
1427 if transceiver.direction() == RTCRtpTransceiverDirection::Sendrecv {
1428 transceiver.set_direction(RTCRtpTransceiverDirection::Sendonly);
1429 } else if transceiver.direction()
1430 == RTCRtpTransceiverDirection::Recvonly
1431 {
1432 transceiver.set_direction(RTCRtpTransceiverDirection::Inactive);
1433 }
1434 } else if direction == RTCRtpTransceiverDirection::Sendrecv {
1435 if transceiver.direction() == RTCRtpTransceiverDirection::Sendonly {
1436 transceiver.set_direction(RTCRtpTransceiverDirection::Sendrecv);
1437 } else if transceiver.direction()
1438 == RTCRtpTransceiverDirection::Inactive
1439 {
1440 transceiver.set_direction(RTCRtpTransceiverDirection::Recvonly);
1441 }
1442 } else if direction == RTCRtpTransceiverDirection::Sendonly
1443 && transceiver.direction() == RTCRtpTransceiverDirection::Inactive
1444 {
1445 transceiver.set_direction(RTCRtpTransceiverDirection::Recvonly);
1446 }
1447
1448 transceiver.set_codec_preferences_from_remote_description(
1449 media,
1450 &self.media_engine,
1451 )?;
1452
1453 if transceiver.mid().is_none() {
1454 transceiver.set_mid(mid_value.to_string())?;
1455 }
1456 } else {
1457 let local_direction =
1458 if direction == RTCRtpTransceiverDirection::Recvonly {
1459 RTCRtpTransceiverDirection::Sendonly
1460 } else {
1461 RTCRtpTransceiverDirection::Recvonly
1462 };
1463
1464 let mut transceiver = RTCRtpTransceiverInternal::new(
1465 kind,
1466 None,
1467 RTCRtpTransceiverInit {
1468 direction: local_direction,
1469 streams: vec![],
1470 send_encodings: vec![],
1471 },
1472 );
1473
1474 transceiver.set_codec_preferences_from_remote_description(
1475 media,
1476 &self.media_engine,
1477 )?;
1478
1479 if transceiver.mid().is_none() {
1480 transceiver.set_mid(mid_value.to_string())?;
1481 }
1482
1483 // Mark as implicitly created by a remote offer so that, if this offer
1484 // is later rolled back, the transceiver is stopped and removed
1485 // (RFC 9429, Section 5.7) — unless a track is attached via add_track.
1486 transceiver.set_created_by_remote_description(true);
1487
1488 self.add_rtp_transceiver(transceiver);
1489 }
1490 }
1491 } else {
1492 // we_offer
1493 // WebRTC Spec 1.0 https://www.w3.org/TR/webrtc/
1494 // 4.5.9.2
1495 // This is an answer from the remote.
1496 for media in &media_descriptions {
1497 let mid_value = match get_mid_value(media) {
1498 Some(mid) if !mid.is_empty() => mid,
1499 _ => return Err(Error::ErrPeerConnRemoteDescriptionWithoutMidValue),
1500 };
1501
1502 if media.is_webrtc_datachannel() {
1503 continue;
1504 }
1505
1506 let kind = RtpCodecKind::from(media.media_name.media.as_str());
1507 let mut direction = get_peer_direction(media);
1508 if kind == RtpCodecKind::Unspecified
1509 || direction == RTCRtpTransceiverDirection::Unspecified
1510 {
1511 continue;
1512 }
1513
1514 let transceiver = if let Some(i) =
1515 RTCPeerConnection::find_by_mid(mid_value, &self.rtp_transceivers)
1516 {
1517 &mut self.rtp_transceivers[i]
1518 } else {
1519 return Err(Error::ErrPeerConnTransceiverMidNil);
1520 };
1521
1522 // reverse direction if it was a remote answer
1523 if direction == RTCRtpTransceiverDirection::Sendonly {
1524 direction = RTCRtpTransceiverDirection::Recvonly;
1525 } else if direction == RTCRtpTransceiverDirection::Recvonly {
1526 direction = RTCRtpTransceiverDirection::Sendonly;
1527 }
1528
1529 transceiver.set_current_direction(direction);
1530
1531 transceiver.set_codec_preferences_from_remote_description(
1532 media,
1533 &self.media_engine,
1534 )?;
1535 }
1536 }
1537 }
1538
1539 let (remote_ufrag, remote_pwd, candidates) =
1540 extract_ice_details(parsed_remote_description)?;
1541
1542 if is_renegotiation
1543 && self
1544 .ice_transport()
1545 .have_remote_credentials_change(&remote_ufrag, &remote_pwd)
1546 {
1547 // An ICE Restart only happens implicitly for a set_remote_description of type offer
1548
1549 if !we_offer {
1550 // The answerer restarts in one step: it has `now`, and the answer it generates
1551 // next must already carry the new local credentials.
1552 self.stage_ice_restart()?;
1553 self.apply_ice_restart(now)?;
1554 }
1555
1556 self.ice_transport_mut()
1557 .set_remote_credentials(remote_ufrag.clone(), remote_pwd.clone())?;
1558 }
1559
1560 for candidate in candidates {
1561 self.ice_transport_mut().add_remote_candidate(candidate)?;
1562 }
1563
1564 if !is_renegotiation {
1565 let remote_is_lite = is_lite_set(parsed_remote_description);
1566
1567 let (remote_fingerprint, remote_fingerprint_hash) =
1568 extract_fingerprint(parsed_remote_description)?;
1569
1570 // If one of the agents is lite and the other one is not, the lite agent must be the controlling agent.
1571 // If both or neither agents are lite the offering agent is controlling.
1572 // RFC 8445 S6.1.1
1573 let local_ice_role = if (we_offer
1574 && remote_is_lite == self.setting_engine.candidates.ice_lite)
1575 || (remote_is_lite && !self.setting_engine.candidates.ice_lite)
1576 {
1577 RTCIceRole::Controlling
1578 } else {
1579 RTCIceRole::Controlled
1580 };
1581
1582 let remote_dtls_role = RTCDtlsRole::from(parsed_remote_description);
1583 log::trace!(
1584 "start_transports: local_ice_role={local_ice_role}, remote_dtls_role={remote_dtls_role}"
1585 );
1586
1587 self.start_transports(
1588 now,
1589 local_ice_role,
1590 RTCIceParameters {
1591 username_fragment: remote_ufrag,
1592 password: remote_pwd,
1593 ice_lite: remote_is_lite,
1594 },
1595 RTCDtlsParameters {
1596 role: remote_dtls_role,
1597 fingerprints: vec![RTCDtlsFingerprint {
1598 algorithm: remote_fingerprint_hash,
1599 value: remote_fingerprint,
1600 }],
1601 },
1602 )?;
1603 }
1604
1605 if we_offer
1606 && let Some(parsed_local_description) = self
1607 .current_local_description
1608 .as_ref()
1609 .and_then(|desc| desc.parsed.as_ref())
1610 {
1611 // only start sctp transport if application media has been negotiated
1612 if let (Some(local_application_media), Some(remote_application_media)) = (
1613 get_application_media(parsed_local_description),
1614 get_application_media(parsed_remote_description),
1615 ) {
1616 let (dtls_role, remote_caps, local_sctp_port, remote_sctp_port) = (
1617 self.dtls_transport().role(),
1618 SCTPTransportCapabilities {
1619 max_message_size: get_application_media_section_max_message_size(
1620 remote_application_media,
1621 )
1622 .unwrap_or(SctpMaxMessageSize::DEFAULT_MESSAGE_SIZE),
1623 },
1624 get_application_media_section_sctp_port(local_application_media)
1625 .unwrap_or(5000),
1626 get_application_media_section_sctp_port(remote_application_media)
1627 .unwrap_or(5000),
1628 );
1629
1630 // we_offer: we create_offer() and set_local_description() first
1631 // then, after call set_remote_description here,
1632 // Now we should have done SDP negotiation.
1633 // Therefore, it is ready to start sctp and rtp.
1634 self.sctp_transport_mut().start(
1635 dtls_role,
1636 remote_caps,
1637 local_sctp_port,
1638 remote_sctp_port,
1639 )?;
1640 }
1641 self.start_rtp(remote_description)?;
1642 }
1643 }
1644
1645 Ok(())
1646 }
1647
1648 /// Returns the remote session description.
1649 ///
1650 /// Returns `pending_remote_description` if it is not null, otherwise returns
1651 /// `current_remote_description`. This property is used to determine if
1652 /// `set_remote_description` has already been called.
1653 ///
1654 /// # Specification
1655 ///
1656 /// See [remoteDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-remotedescription)
1657 pub fn remote_description(&self) -> Option<&RTCSessionDescription> {
1658 if self.pending_remote_description.is_some() {
1659 self.pending_remote_description.as_ref()
1660 } else {
1661 self.current_remote_description.as_ref()
1662 }
1663 }
1664
1665 /// Returns the current remote description as last successfully negotiated since
1666 /// the last negotiation completed.
1667 ///
1668 /// This represents the remote description from the last offer/answer exchange that was
1669 /// successfully applied, not including any offers currently being negotiated.
1670 ///
1671 /// Returns `None` if there is no current remote description (e.g., before initial negotiation).
1672 ///
1673 /// # Specification
1674 ///
1675 /// See [currentRemoteDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-currentremotedesc)
1676 pub fn current_remote_description(&self) -> Option<&RTCSessionDescription> {
1677 self.current_remote_description.as_ref()
1678 }
1679
1680 /// Returns the pending remote description if it exists.
1681 ///
1682 /// This represents the remote description from a call to `set_remote_description()` whose
1683 /// corresponding local description has not yet been applied. This is `None` if negotiation
1684 /// is not in progress or if a rollback has been performed.
1685 ///
1686 /// Returns `None` if there is no pending remote description.
1687 ///
1688 /// # Specification
1689 ///
1690 /// See [pendingRemoteDescription](https://www.w3.org/TR/webrtc/#dom-peerconnection-pendingremotedesc)
1691 pub fn pending_remote_description(&self) -> Option<&RTCSessionDescription> {
1692 self.pending_remote_description.as_ref()
1693 }
1694
1695 /// Adds a remote ICE candidate to the peer connection.
1696 ///
1697 /// This method provides a remote candidate to the ICE agent. When the remote peer
1698 /// gathers ICE candidates and sends them over the signaling channel, this method
1699 /// should be called to add each candidate.
1700 ///
1701 /// # Parameters
1702 ///
1703 /// * `remote_candidate` - The ICE candidate initialization data.
1704 ///
1705 /// # Errors
1706 ///
1707 /// Returns an error if:
1708 /// - No remote description has been set
1709 /// - The candidate string is invalid
1710 ///
1711 /// # Specification
1712 ///
1713 /// See [addIceCandidate](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-addicecandidate!overload-1)
1714 pub fn add_remote_candidate(&mut self, remote_candidate: RTCIceCandidateInit) -> Result<()> {
1715 if self.remote_description().is_none() {
1716 return Err(Error::ErrNoRemoteDescription);
1717 }
1718
1719 let candidate_value = match remote_candidate.candidate.strip_prefix("candidate:") {
1720 Some(s) => s,
1721 None => remote_candidate.candidate.as_str(),
1722 };
1723
1724 if !candidate_value.is_empty() {
1725 self.add_ice_remote_candidate(candidate_value)?;
1726 }
1727
1728 Ok(())
1729 }
1730
1731 /// Adds a local ICE candidate to the peer connection.
1732 ///
1733 /// This has no W3C counterpart: a browser's ICE agent gathers its own candidates, but this
1734 /// crate performs no I/O, so the application owns the sockets and therefore owns gathering.
1735 /// Every local candidate the connection is to advertise must be handed to this method.
1736 ///
1737 /// # Signalling the end of gathering
1738 ///
1739 /// An **empty candidate string** is the end-of-gathering sentinel. It advertises no
1740 /// candidate; it moves the ICE gathering state to [`RTCIceGatheringState::Complete`] and
1741 /// emits [`RTCPeerConnectionEvent::OnIceGatheringStateChangeEvent`] with that state. No
1742 /// other call reaches `Complete`, so a caller that never sends it leaves the connection
1743 /// reporting `Gathering` forever. Send it once, after the last real candidate:
1744 ///
1745 /// ```no_run
1746 /// # use std::time::Instant;
1747 /// # use rtc::peer_connection::RTCPeerConnectionBuilder;
1748 /// use rtc::peer_connection::transport::RTCIceCandidateInit;
1749 ///
1750 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1751 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
1752 /// // ... add every gathered candidate first ...
1753 /// pc.add_local_candidate(RTCIceCandidateInit::default())?;
1754 /// # Ok(())
1755 /// # }
1756 /// ```
1757 ///
1758 /// # Parameters
1759 ///
1760 /// - `local_candidate`: The ICE candidate initialization data, or a value whose `candidate`
1761 /// field is empty to signal end-of-gathering. For candidates of type `srflx` (server
1762 /// reflexive) or `relay`, set `url` to the STUN/TURN server the candidate was gathered
1763 /// from, so that `getStats` attributes it correctly.
1764 ///
1765 /// # Errors
1766 ///
1767 /// Returns an error if a non-empty candidate string cannot be parsed.
1768 ///
1769 /// [`RTCIceGatheringState::Complete`]: crate::peer_connection::state::RTCIceGatheringState::Complete
1770 /// [`RTCPeerConnectionEvent::OnIceGatheringStateChangeEvent`]: crate::peer_connection::event::RTCPeerConnectionEvent::OnIceGatheringStateChangeEvent
1771 pub fn add_local_candidate(&mut self, local_candidate: RTCIceCandidateInit) -> Result<()> {
1772 let candidate_value = match local_candidate.candidate.strip_prefix("candidate:") {
1773 Some(s) => s,
1774 None => local_candidate.candidate.as_str(),
1775 };
1776
1777 if !candidate_value.is_empty() {
1778 self.add_ice_local_candidate(candidate_value, local_candidate.url.as_deref())?;
1779 } else {
1780 self.ice_transport_mut().ice_gathering_state = RTCIceGatheringState::Complete;
1781 // Emit OnIceGatheringStateChangeEvent
1782 self.pipeline_context.event_outs.push_back(
1783 RTCPeerConnectionEvent::OnIceGatheringStateChangeEvent(
1784 RTCIceGatheringState::Complete,
1785 ),
1786 );
1787 }
1788
1789 Ok(())
1790 }
1791
1792 /// Tells the peer connection that ICE should be restarted.
1793 ///
1794 /// This method causes the next call to `create_offer` to generate an offer that
1795 /// will restart ICE. This is useful when network conditions change or the connection
1796 /// fails.
1797 ///
1798 /// # Specification
1799 ///
1800 /// See [restartIce](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-restartice)
1801 pub fn restart_ice(&mut self) {
1802 self.ice_restart_requested = Some(RTCOfferOptions { ice_restart: true });
1803 }
1804
1805 /// Returns the current configuration of this peer connection.
1806 ///
1807 /// The returned reference is to the current configuration. To modify the configuration,
1808 /// use `set_configuration`.
1809 ///
1810 /// # Specification
1811 ///
1812 /// See [getConfiguration](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration)
1813 pub fn get_configuration(&self) -> &RTCConfiguration {
1814 &self.configuration
1815 }
1816
1817 /// Updates the configuration of this peer connection.
1818 ///
1819 /// Only the fields the W3C algorithm permits changing after construction are applied;
1820 /// attempting to change the peer identity or the certificate set is an error.
1821 ///
1822 /// # Parameters
1823 ///
1824 /// - `configuration`: The configuration to apply.
1825 ///
1826 /// # Errors
1827 ///
1828 /// Returns an error if:
1829 /// - The peer connection is closed (`ErrConnectionClosed`)
1830 /// - The peer identity differs from the one already set (`ErrModifyingPeerIdentity`)
1831 /// - The number of certificates differs from the one already set (`ErrModifyingCertificates`)
1832 ///
1833 /// # Specification
1834 ///
1835 /// See [setConfiguration](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration)
1836 pub fn set_configuration(&mut self, configuration: RTCConfiguration) -> Result<()> {
1837 // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2)
1838 if self.peer_connection_state == RTCPeerConnectionState::Closed {
1839 return Err(Error::ErrConnectionClosed);
1840 }
1841
1842 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #3)
1843 if !configuration.peer_identity.is_empty() {
1844 if configuration.peer_identity != self.configuration.peer_identity {
1845 return Err(Error::ErrModifyingPeerIdentity);
1846 }
1847 self.configuration.peer_identity = configuration.peer_identity;
1848 }
1849
1850 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #4)
1851 if !configuration.certificates.is_empty() {
1852 if configuration.certificates.len() != self.configuration.certificates.len() {
1853 return Err(Error::ErrModifyingCertificates);
1854 }
1855
1856 self.configuration.certificates = configuration.certificates;
1857 }
1858
1859 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #5)
1860
1861 if configuration.bundle_policy != self.configuration.bundle_policy {
1862 return Err(Error::ErrModifyingBundlePolicy);
1863 }
1864 self.configuration.bundle_policy = configuration.bundle_policy;
1865
1866 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #6)
1867 if configuration.rtcp_mux_policy != self.configuration.rtcp_mux_policy {
1868 return Err(Error::ErrModifyingRTCPMuxPolicy);
1869 }
1870 self.configuration.rtcp_mux_policy = configuration.rtcp_mux_policy;
1871
1872 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #7)
1873 if configuration.ice_candidate_pool_size != 0 {
1874 if self.configuration.ice_candidate_pool_size != configuration.ice_candidate_pool_size
1875 && self.local_description().is_some()
1876 {
1877 return Err(Error::ErrModifyingICECandidatePoolSize);
1878 }
1879 self.configuration.ice_candidate_pool_size = configuration.ice_candidate_pool_size;
1880 }
1881
1882 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #8)
1883
1884 self.configuration.ice_transport_policy = configuration.ice_transport_policy;
1885
1886 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11)
1887 if !configuration.ice_servers.is_empty() {
1888 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3)
1889 for server in &configuration.ice_servers {
1890 server.validate()?;
1891 }
1892 self.configuration.ice_servers = configuration.ice_servers
1893 }
1894
1895 Ok(())
1896 }
1897
1898 /// Creates a new data channel with the given label.
1899 ///
1900 /// The returned handle borrows the peer connection. To reach the channel again later,
1901 /// keep its [`RTCDataChannelId`] (from [`RTCDataChannel::id`]) and pass it to
1902 /// [`Self::data_channel`].
1903 ///
1904 /// A channel created here is not immediately usable: unless it was negotiated
1905 /// out-of-band, its SCTP stream is established by the in-band DCEP handshake, and sending
1906 /// before then fails. Wait for [`RTCDataChannelEvent::OnOpen`].
1907 ///
1908 /// # Parameters
1909 ///
1910 /// - `label`: The channel label. Labels need not be unique.
1911 /// - `options`: Optional configuration such as ordering, reliability and out-of-band
1912 /// negotiation. Defaults are used when `None`.
1913 ///
1914 /// # Errors
1915 ///
1916 /// Returns an error if:
1917 /// - The peer connection is closed (`ErrConnectionClosed`)
1918 /// - The label or sub-protocol exceeds the length the DCEP OPEN message can carry
1919 ///
1920 /// # Specification
1921 ///
1922 /// See [createDataChannel](https://www.w3.org/TR/webrtc/#dom-peerconnection-createdatachannel)
1923 ///
1924 /// [`RTCDataChannelId`]: crate::data_channel::RTCDataChannelId
1925 /// [`RTCDataChannel::id`]: crate::data_channel::RTCDataChannel::id
1926 /// [`RTCDataChannelEvent::OnOpen`]: crate::peer_connection::event::RTCDataChannelEvent::OnOpen
1927 pub fn create_data_channel(
1928 &mut self,
1929 label: &str,
1930 options: Option<RTCDataChannelInit>,
1931 ) -> Result<RTCDataChannel<'_>> {
1932 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #2)
1933 if self.peer_connection_state == RTCPeerConnectionState::Closed {
1934 return Err(Error::ErrConnectionClosed);
1935 }
1936
1937 let mut params = DataChannelParameters {
1938 label: label.to_owned(),
1939 ..Default::default()
1940 };
1941
1942 // `None` means "the dictionary defaults", which is what `RTCDataChannelInit::default()`
1943 // spells out. Taking that route rather than leaving `params` on its derived default
1944 // keeps a single definition of those defaults — notably `ordered`, which is `true`.
1945 let options = options.unwrap_or_default();
1946
1947 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #16)
1948 if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() {
1949 return Err(Error::ErrRetransmitsOrPacketLifeTime);
1950 }
1951
1952 // Ordered indicates if data is allowed to be delivered out of order. The
1953 // default value of true, guarantees that data will be delivered in order.
1954 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #9)
1955 params.ordered = options.ordered;
1956
1957 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #7)
1958 params.max_packet_life_time = options.max_packet_life_time;
1959
1960 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #8)
1961 params.max_retransmits = options.max_retransmits;
1962
1963 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #10)
1964 params.protocol = options.protocol;
1965
1966 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #11)
1967 if params.protocol.len() > 65535 {
1968 return Err(Error::ErrProtocolTooLarge);
1969 }
1970
1971 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #12)
1972 //
1973 // `negotiated` doubles as the out-of-band stream id. When it is set the id is fixed by
1974 // the application and known now; when it is not, the channel is announced in-band and
1975 // its stream id has to wait for the DTLS role, per RFC 8832 §6. Nothing is guessed
1976 // here — see `assign_stream_ids`.
1977 params.negotiated = options.negotiated;
1978
1979 // The registry assigns the handle. Note this happens before any dial: the channel is
1980 // addressable from the moment it exists, whether or not it has a stream id yet.
1981 let id = self
1982 .data_channels
1983 .insert(RTCDataChannelInternal::new(params));
1984
1985 // https://www.w3.org/TR/webrtc/#peer-to-peer-data-api (Step #23)
1986 // Open the channel's data transport immediately when an SCTP association already
1987 // exists. The DTLS role is necessarily resolved by then, so a stream id can be
1988 // assigned right here rather than waiting for a connected procedure that has passed.
1989 //
1990 // The role guard is not redundant with the association check. In practice an
1991 // association only exists once a description has been applied, which resolves the
1992 // role — but if it somehow is not resolved, there is no correct parity to pick, and
1993 // "wait for the connected procedure" is the right answer rather than an error. That is
1994 // the ordinary path for every channel created before negotiation.
1995 let dtls_role = self.dtls_transport().role();
1996 if let Some(handle) = self
1997 .sctp_transport()
1998 .sctp_associations
1999 .keys()
2000 .next()
2001 .copied()
2002 && matches!(dtls_role, RTCDtlsRole::Client | RTCDtlsRole::Server)
2003 {
2004 let max_channels = self.sctp_transport().max_channels();
2005 self.data_channels
2006 .assign_stream_ids(dtls_role, max_channels)?;
2007
2008 let data_channel = self
2009 .data_channels
2010 .get_mut(&id)
2011 .ok_or(Error::ErrDataChannelNotExisted)?;
2012 if data_channel.ready_state == RTCDataChannelState::Connecting
2013 && data_channel.data_channel.is_none()
2014 {
2015 data_channel.dial(handle.0)?;
2016 }
2017 }
2018
2019 self.trigger_negotiation_needed();
2020
2021 Ok(RTCDataChannel {
2022 id,
2023 peer_connection: self,
2024 })
2025 }
2026
2027 /// Returns an iterator over the `RTCRtpSender` objects.
2028 ///
2029 /// The `RTCRtpSender` objects represent the media streams that are being sent
2030 /// to the remote peer.
2031 ///
2032 /// # Specification
2033 ///
2034 /// See [getSenders](https://www.w3.org/TR/webrtc/#dom-peerconnection-getsenders)
2035 pub fn get_senders(&self) -> impl Iterator<Item = RTCRtpSenderId> + use<'_> {
2036 self.rtp_transceivers
2037 .iter()
2038 .enumerate()
2039 .filter(|(_, transceiver)| transceiver.direction().has_send())
2040 .map(|(id, _)| RTCRtpSenderId(id))
2041 }
2042
2043 /// Returns an iterator over the `RTCRtpReceiver` objects.
2044 ///
2045 /// The `RTCRtpReceiver` objects represent the media streams that are being received
2046 /// from the remote peer.
2047 ///
2048 /// # Specification
2049 ///
2050 /// See [getReceivers](https://www.w3.org/TR/webrtc/#dom-peerconnection-getreceivers)
2051 pub fn get_receivers(&self) -> impl Iterator<Item = RTCRtpReceiverId> + use<'_> {
2052 self.rtp_transceivers
2053 .iter()
2054 .enumerate()
2055 .filter(|(_, transceiver)| transceiver.direction().has_recv())
2056 .map(|(id, _)| RTCRtpReceiverId(id))
2057 }
2058
2059 /// Returns an iterator over the `RTCRtpTransceiver` objects.
2060 ///
2061 /// The `RTCRtpTransceiver` objects represent the combination of an `RTCRtpSender`
2062 /// and an `RTCRtpReceiver` that share a common mid.
2063 ///
2064 /// # Specification
2065 ///
2066 /// See [getTransceivers](https://www.w3.org/TR/webrtc/#dom-peerconnection-gettranseceivers)
2067 pub fn get_transceivers(&self) -> impl Iterator<Item = RTCRtpTransceiverId> {
2068 0..self.rtp_transceivers.len()
2069 }
2070
2071 /// Adds a media track to the peer connection.
2072 ///
2073 /// This method adds a track to the connection, either by finding an existing transceiver
2074 /// that can be reused, or by creating a new transceiver. The track represents media
2075 /// (audio or video) that will be sent to the remote peer.
2076 ///
2077 /// # Parameters
2078 ///
2079 /// * `track` - The media stream track to add.
2080 ///
2081 /// # Returns
2082 ///
2083 /// Returns the ID of the `RTCRtpSender` that will send this track.
2084 ///
2085 /// # Errors
2086 ///
2087 /// Returns an error if the peer connection is closed.
2088 ///
2089 /// # Specification
2090 ///
2091 /// See [addTrack](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-addtrack)
2092 pub fn add_track(&mut self, track: MediaStreamTrack) -> Result<RTCRtpSenderId> {
2093 if self.peer_connection_state == RTCPeerConnectionState::Closed {
2094 return Err(Error::ErrConnectionClosed);
2095 }
2096
2097 let send_encodings = self.send_encodings_from_track(&track);
2098 let (track, send_encodings, codec_preferences) =
2099 self.normalize_sender_track(track, send_encodings)?;
2100 for (id, transceiver) in self.rtp_transceivers.iter_mut().enumerate() {
2101 if !transceiver.stopped()
2102 && transceiver.kind() == track.kind()
2103 && transceiver.sender().is_none()
2104 {
2105 let mut sender =
2106 RTCRtpSenderInternal::new(track.kind(), track, vec![], send_encodings);
2107
2108 if transceiver.get_codec_preferences().is_empty() && !codec_preferences.is_empty() {
2109 transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
2110 }
2111
2112 sender.set_codec_preferences(transceiver.get_codec_preferences().to_vec());
2113
2114 transceiver.sender_mut().replace(sender);
2115
2116 transceiver.set_direction(RTCRtpTransceiverDirection::from_send_recv(
2117 true,
2118 transceiver.direction().has_recv(),
2119 ));
2120
2121 self.trigger_negotiation_needed();
2122 return Ok(RTCRtpSenderId(id));
2123 }
2124 }
2125
2126 let mut transceiver = self.new_transceiver_from_track(
2127 track,
2128 RTCRtpTransceiverInit {
2129 direction: RTCRtpTransceiverDirection::Sendrecv,
2130 streams: vec![],
2131 send_encodings,
2132 },
2133 )?;
2134 if !codec_preferences.is_empty() {
2135 transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
2136 }
2137 Ok(RTCRtpSenderId(self.add_rtp_transceiver(transceiver)))
2138 }
2139
2140 /// Removes a track from the peer connection.
2141 ///
2142 /// This method stops an `RTCRtpSender` from sending media and marks its transceiver
2143 /// as no longer sending. This will trigger renegotiation.
2144 ///
2145 /// # Parameters
2146 ///
2147 /// * `sender_id` - The ID of the `RTCRtpSender` to remove.
2148 ///
2149 /// # Errors
2150 ///
2151 /// Returns an error if:
2152 /// - The peer connection is closed
2153 /// - The sender ID is invalid
2154 ///
2155 /// # Specification
2156 ///
2157 /// See [removeTrack](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-removetrack)
2158 pub fn remove_track(&mut self, sender_id: RTCRtpSenderId) -> Result<()> {
2159 if self.peer_connection_state == RTCPeerConnectionState::Closed {
2160 return Err(Error::ErrConnectionClosed);
2161 }
2162
2163 if sender_id.0 >= self.rtp_transceivers.len() {
2164 return Err(Error::ErrRTPSenderNotExisted);
2165 }
2166
2167 // This also happens in `set_sending_track` but we need to make sure we do this
2168 // before we call sender.stop to avoid a race condition when removing tracks and
2169 // generating offers.
2170 let has_recv = self.rtp_transceivers[sender_id.0].direction().has_recv();
2171 self.rtp_transceivers[sender_id.0]
2172 .set_direction(RTCRtpTransceiverDirection::from_send_recv(false, has_recv));
2173
2174 if let Some(sender) = self.rtp_transceivers[sender_id.0].sender_mut()
2175 && sender
2176 .stop(&self.media_engine, &mut self.interceptor)
2177 .is_ok()
2178 {
2179 self.trigger_negotiation_needed();
2180 }
2181
2182 self.rtp_transceivers[sender_id.0].sender_mut().take();
2183
2184 Ok(())
2185 }
2186
2187 /// Creates a new `RTCRtpTransceiver` and adds it to the set of transceivers.
2188 ///
2189 /// This method creates a transceiver associated with the given track, which can be
2190 /// configured to send, receive, or both.
2191 ///
2192 /// # Parameters
2193 ///
2194 /// * `track` - The media stream track to associate with the transceiver.
2195 /// * `init` - Optional initialization parameters for the transceiver.
2196 ///
2197 /// # Returns
2198 ///
2199 /// Returns the ID of the created transceiver.
2200 ///
2201 /// # Errors
2202 ///
2203 /// Returns an error if the peer connection is closed.
2204 ///
2205 /// # Specification
2206 ///
2207 /// See [addTransceiver](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-addtransceiver)
2208 pub fn add_transceiver_from_track(
2209 &mut self,
2210 track: MediaStreamTrack,
2211 init: Option<RTCRtpTransceiverInit>,
2212 ) -> Result<RTCRtpTransceiverId> {
2213 if self.peer_connection_state == RTCPeerConnectionState::Closed {
2214 return Err(Error::ErrConnectionClosed);
2215 }
2216
2217 if let Some(init) = init.as_ref()
2218 && !init.direction.has_send()
2219 {
2220 return Err(Error::ErrInvalidDirection);
2221 }
2222
2223 let mut init = if let Some(init) = init {
2224 init
2225 } else {
2226 RTCRtpTransceiverInit {
2227 direction: RTCRtpTransceiverDirection::Sendrecv,
2228 streams: vec![],
2229 send_encodings: vec![],
2230 }
2231 };
2232
2233 let send_encodings = if init.send_encodings.is_empty() {
2234 self.send_encodings_from_track(&track)
2235 } else {
2236 init.send_encodings.clone()
2237 };
2238 let (track, send_encodings, codec_preferences) =
2239 self.normalize_sender_track(track, send_encodings)?;
2240 init.send_encodings = send_encodings;
2241
2242 let mut transceiver = self.new_transceiver_from_track(track, init)?;
2243 if !codec_preferences.is_empty() {
2244 transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
2245 }
2246
2247 Ok(self.add_rtp_transceiver(transceiver))
2248 }
2249
2250 /// Creates a new transceiver for the given media kind and adds it to the set of
2251 /// transceivers.
2252 ///
2253 /// # Parameters
2254 ///
2255 /// - `kind`: Audio or video.
2256 /// - `init`: Optional direction and encoding parameters; defaults are used when `None`.
2257 ///
2258 /// # Errors
2259 ///
2260 /// Returns an error if:
2261 /// - The peer connection is closed (`ErrConnectionClosed`)
2262 /// - The requested direction is not supported for a transceiver created this way
2263 /// - No codec is registered in the media engine for `kind`
2264 ///
2265 /// # Specification
2266 ///
2267 /// See [addTransceiver](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-addtransceiver)
2268 pub fn add_transceiver_from_kind(
2269 &mut self,
2270 kind: RtpCodecKind,
2271 init: Option<RTCRtpTransceiverInit>,
2272 ) -> Result<RTCRtpTransceiverId> {
2273 if self.peer_connection_state == RTCPeerConnectionState::Closed {
2274 return Err(Error::ErrConnectionClosed);
2275 }
2276
2277 let init = if let Some(init) = init {
2278 if init.direction.has_send() && init.send_encodings.is_empty() {
2279 return Err(Error::ErrInvalidDirection);
2280 }
2281
2282 init
2283 } else {
2284 RTCRtpTransceiverInit {
2285 direction: RTCRtpTransceiverDirection::Recvonly,
2286 streams: vec![],
2287 send_encodings: vec![],
2288 }
2289 };
2290
2291 let transceiver = match init.direction {
2292 RTCRtpTransceiverDirection::Sendonly | RTCRtpTransceiverDirection::Sendrecv => {
2293 let mut init = init;
2294 let track = MediaStreamTrack::new(
2295 math_rand_alpha(16), // MediaStreamId
2296 math_rand_alpha(16), // MediaStreamTrackId
2297 math_rand_alpha(16), // Label
2298 kind,
2299 init.send_encodings.clone(),
2300 );
2301 let (track, send_encodings, codec_preferences) =
2302 self.normalize_sender_track(track, init.send_encodings)?;
2303 init.send_encodings = send_encodings;
2304
2305 let mut transceiver = self.new_transceiver_from_track(track, init)?;
2306 if !codec_preferences.is_empty() {
2307 transceiver.set_codec_preferences(codec_preferences, &self.media_engine)?;
2308 }
2309 transceiver
2310 }
2311 RTCRtpTransceiverDirection::Recvonly => {
2312 RTCRtpTransceiverInternal::new(kind, None, init)
2313 }
2314 _ => return Err(Error::ErrPeerConnAddTransceiverFromKindSupport),
2315 };
2316
2317 Ok(self.add_rtp_transceiver(transceiver))
2318 }
2319
2320 /// Returns a handle to the [`RTCDataChannel`] with the given id, or `None` if no such
2321 /// channel exists on this peer connection.
2322 ///
2323 /// [`RTCDataChannel`]: crate::data_channel::RTCDataChannel
2324 pub fn data_channel(&mut self, id: RTCDataChannelId) -> Option<RTCDataChannel<'_>> {
2325 if self.data_channels.contains(&id) {
2326 Some(RTCDataChannel {
2327 id,
2328 peer_connection: self,
2329 })
2330 } else {
2331 None
2332 }
2333 }
2334
2335 /// The SCTP transport over which data channels are carried.
2336 ///
2337 /// `None` until SCTP has been negotiated — that is, until a description establishing an
2338 /// SCTP association has been applied by both sides. A connection carrying only media never
2339 /// has one.
2340 ///
2341 /// This is the **only** transport accessor on `RTCPeerConnection`, matching the W3C
2342 /// interface, which exposes `sctp` and nothing else. The DTLS and ICE transports are
2343 /// reached by walking from here (or from a sender or receiver):
2344 ///
2345 /// ```text
2346 /// pc.sctp()?.transport().ice_transport()
2347 /// ```
2348 ///
2349 /// ## Specifications
2350 ///
2351 /// * [W3C](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-sctp)
2352 pub fn sctp(&self) -> Option<RTCSctpTransport<'_>> {
2353 if self.sctp_transport().is_started {
2354 Some(RTCSctpTransport {
2355 peer_connection: self,
2356 })
2357 } else {
2358 None
2359 }
2360 }
2361
2362 /// Returns a handle to the [`RTCRtpSender`] with the given id, or `None` if no such
2363 /// sender exists on this peer connection.
2364 ///
2365 /// [`RTCRtpSender`]: crate::rtp_transceiver::rtp_sender::RTCRtpSender
2366 pub fn rtp_sender(&mut self, id: RTCRtpSenderId) -> Option<RTCRtpSender<'_>> {
2367 if id.0 < self.rtp_transceivers.len()
2368 && self.rtp_transceivers[id.0].direction().has_send()
2369 && self.rtp_transceivers[id.0].sender().is_some()
2370 {
2371 Some(RTCRtpSender {
2372 id,
2373 peer_connection: self,
2374 })
2375 } else {
2376 None
2377 }
2378 }
2379
2380 /// Returns a handle to the [`RTCRtpReceiver`] with the given id, or `None` if no such
2381 /// receiver exists on this peer connection.
2382 ///
2383 /// [`RTCRtpReceiver`]: crate::rtp_transceiver::rtp_receiver::RTCRtpReceiver
2384 pub fn rtp_receiver(&mut self, id: RTCRtpReceiverId) -> Option<RTCRtpReceiver<'_>> {
2385 if id.0 < self.rtp_transceivers.len()
2386 && self.rtp_transceivers[id.0].direction().has_recv()
2387 && self.rtp_transceivers[id.0].receiver().is_some()
2388 {
2389 Some(RTCRtpReceiver {
2390 id,
2391 peer_connection: self,
2392 })
2393 } else {
2394 None
2395 }
2396 }
2397
2398 /// Returns a handle to the [`RTCRtpTransceiver`] with the given id, or `None` if no such
2399 /// transceiver exists on this peer connection.
2400 ///
2401 /// [`RTCRtpTransceiver`]: crate::rtp_transceiver::RTCRtpTransceiver
2402 pub fn rtp_transceiver(&mut self, id: RTCRtpTransceiverId) -> Option<RTCRtpTransceiver<'_>> {
2403 if id < self.rtp_transceivers.len() {
2404 Some(RTCRtpTransceiver {
2405 id,
2406 peer_connection: self,
2407 })
2408 } else {
2409 None
2410 }
2411 }
2412
2413 /// Returns a snapshot of accumulated statistics.
2414 ///
2415 /// This method creates an immutable snapshot of WebRTC statistics
2416 /// at the given timestamp. When `selector` is `StatsSelector::None`,
2417 /// the returned `RTCStatsReport` contains statistics for all aspects
2418 /// of the peer connection. When a sender or receiver is specified,
2419 /// only statistics relevant to that sender/receiver are included.
2420 ///
2421 /// # Statistics included by selector
2422 ///
2423 /// - `StatsSelector::None` - All statistics for the entire connection
2424 /// - `StatsSelector::Sender(id)` - Outbound RTP streams for the sender
2425 /// and all referenced stats (transport, codec, remote inbound, etc.)
2426 /// - `StatsSelector::Receiver(id)` - Inbound RTP streams for the receiver
2427 /// and all referenced stats (transport, codec, remote outbound, etc.)
2428 ///
2429 /// # Parameters
2430 ///
2431 /// * `now` - The timestamp to use for all stats in the report. This is
2432 /// passed explicitly to support deterministic testing.
2433 /// * `selector` - Controls which statistics are included in the report.
2434 ///
2435 /// # Returns
2436 ///
2437 /// An `RTCStatsReport` containing snapshots of the selected statistics.
2438 ///
2439 /// # Examples
2440 ///
2441 /// ```no_run
2442 /// use std::time::Instant;
2443 /// use rtc::peer_connection::RTCPeerConnectionBuilder;
2444 /// use rtc::statistics::StatsSelector;
2445 ///
2446 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
2447 /// let mut pc = RTCPeerConnectionBuilder::new().build(Instant::now())?;
2448 ///
2449 /// // Get all stats
2450 /// let report = pc.get_stats(Instant::now(), StatsSelector::None);
2451 ///
2452 /// // Access peer connection stats
2453 /// if let Some(pc_stats) = report.peer_connection() {
2454 /// println!("Data channels opened: {}", pc_stats.data_channels_opened);
2455 /// }
2456 ///
2457 /// // Iterate over inbound RTP streams
2458 /// for stream in report.inbound_rtp_streams() {
2459 /// println!("SSRC {}: {} packets received", stream.received_rtp_stream_stats.rtp_stream_stats.ssrc, stream.received_rtp_stream_stats.packets_received);
2460 /// }
2461 /// # Ok(())
2462 /// # }
2463 /// ```
2464 ///
2465 /// # Specification
2466 ///
2467 /// See [getStats](https://www.w3.org/TR/webrtc/#widl-RTCPeerConnection-getStats-Promise-RTCStatsReport--MediaStreamTrack-selector) and
2468 /// [The stats selection algorithm](https://www.w3.org/TR/webrtc/#the-stats-selection-algorithm)
2469 pub fn get_stats(&mut self, now: Instant, selector: StatsSelector) -> RTCStatsReport {
2470 // Update ICE agent stats before taking snapshot
2471 self.update_ice_agent_stats(now);
2472 // Update codec stats from transceivers before taking snapshot
2473 self.update_codec_stats();
2474 self.pipeline_context
2475 .stats
2476 .snapshot_with_selector(now, selector)
2477 }
2478}
2479
2480#[cfg(test)]
2481mod tests {
2482 use super::*;
2483 use crate::data_channel::state::RTCDataChannelState;
2484 use crate::peer_connection::configuration::setting_engine::SctpMaxMessageSize;
2485 use crate::peer_connection::configuration::setting_engine::SettingEngineBuilder;
2486 use crate::peer_connection::transport::RTCIceComponent;
2487 use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState;
2488 use sctp::AssociationHandle;
2489
2490 #[test]
2491 fn with_sctp_receive_buffer_size_sets_and_clamps() {
2492 let setting_engine = SettingEngineBuilder::new()
2493 .with_sctp_max_receive_buffer_size(200_000)
2494 .build();
2495
2496 let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine);
2497 assert_eq!(
2498 builder.setting_engine.sctp_max_receive_buffer_size,
2499 Some(200_000)
2500 );
2501
2502 // Values below the RFC 9260 §3.3.2 floor (1500 bytes), including 0, are clamped up so
2503 // they cannot break the SCTP handshake.
2504 for input in [0u32, 500, 1499] {
2505 let setting_engine = SettingEngineBuilder::new()
2506 .with_sctp_max_receive_buffer_size(input)
2507 .build();
2508 let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine);
2509 assert_eq!(
2510 builder.setting_engine.sctp_max_receive_buffer_size,
2511 Some(1500),
2512 "input {input} should clamp up to the 1500-byte floor"
2513 );
2514 }
2515 }
2516
2517 // The graph the spec exposes: `pc.sctp` is the only way in, and the rest is walked.
2518 #[test]
2519 fn sctp_is_none_until_sctp_is_negotiated() {
2520 let pc = RTCPeerConnectionBuilder::new()
2521 .build(Instant::now())
2522 .unwrap();
2523 assert!(
2524 pc.sctp().is_none(),
2525 "nothing has been negotiated, so there is no SCTP transport to expose"
2526 );
2527 }
2528
2529 #[test]
2530 fn the_transport_graph_is_walkable_and_ids_identify() {
2531 let mut pc = RTCPeerConnectionBuilder::new()
2532 .build(Instant::now())
2533 .unwrap();
2534 // `start()` is what negotiation calls once both descriptions carry an m=application
2535 // section; it is the predicate `sctp()` keys off.
2536 pc.sctp_transport_mut()
2537 .start(
2538 RTCDtlsRole::Client,
2539 crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
2540 max_message_size: 0,
2541 },
2542 5000,
2543 5000,
2544 )
2545 .expect("start");
2546
2547 let sctp = pc.sctp().expect("SCTP is negotiated");
2548 let dtls = sctp.transport();
2549 let ice = dtls.ice_transport();
2550
2551 // Three transports, three identities.
2552 assert_ne!(sctp.id(), dtls.id());
2553 assert_ne!(dtls.id(), ice.id());
2554 assert_ne!(sctp.id(), ice.id());
2555
2556 // Ids are stored, not minted per call: walking twice yields the same identity.
2557 let dtls_again = pc.sctp().unwrap().transport();
2558 assert_eq!(dtls.id(), dtls_again.id());
2559 assert_eq!(ice.id(), dtls_again.ice_transport().id());
2560
2561 // The default configuration caps messages at 64 KiB and the peer advertised no limit,
2562 // so the negotiated value is this endpoint's cap.
2563 assert_eq!(Some(65536), sctp.max_message_size());
2564 // No association yet, so no negotiated stream count.
2565 assert_eq!(None, sctp.max_channels());
2566 assert_eq!(RTCIceComponent::Rtp, ice.component());
2567 }
2568
2569 // W3C types `maxMessageSize` `unrestricted double` so that an implementation with no limit
2570 // can report +Infinity. This one always has a limit — the working buffer is a real
2571 // allocation — so a configuration naming no limit resolves to the implementation ceiling,
2572 // and the value reported is the value enforced.
2573 #[test]
2574 fn max_message_size_with_no_configured_limit_reports_the_ceiling() {
2575 let setting_engine = SettingEngineBuilder::new()
2576 .with_sctp_max_message_size(SctpMaxMessageSize::Bounded(0))
2577 .build();
2578 let mut pc = RTCPeerConnectionBuilder::new()
2579 .with_setting_engine(setting_engine)
2580 .build(Instant::now())
2581 .unwrap();
2582 pc.sctp_transport_mut()
2583 .start(
2584 RTCDtlsRole::Client,
2585 crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
2586 max_message_size: 0,
2587 },
2588 5000,
2589 5000,
2590 )
2591 .expect("start");
2592
2593 assert_eq!(
2594 Some(SctpMaxMessageSize::MAX_MESSAGE_SIZE),
2595 pc.sctp().expect("negotiated").max_message_size()
2596 );
2597 }
2598
2599 // `RTCRtpSender.transport` / `RTCRtpReceiver.transport` come from the per-object
2600 // `[[SenderTransport]]` / `[[ReceiverTransport]]` slots, filled when the transceiver is
2601 // associated by negotiation. Before that the spec reports null.
2602 #[test]
2603 fn sender_and_receiver_transport_are_none_until_the_transceiver_is_associated() {
2604 let mut pc = media_pc();
2605 // `add_track` creates a sendrecv transceiver, so this one has both a sender and a
2606 // receiver while still being unassociated.
2607 let track = MediaStreamTrack::new(
2608 "stream".to_owned(),
2609 "track".to_owned(),
2610 "label".to_owned(),
2611 RtpCodecKind::Audio,
2612 vec![],
2613 );
2614 let sender_id = pc.add_track(track).expect("add track");
2615 let receiver_id = RTCRtpReceiverId::from(sender_id.0);
2616
2617 assert!(pc.rtp_transceivers[sender_id.0].mid().is_none());
2618 assert!(
2619 pc.rtp_sender(sender_id)
2620 .expect("sender")
2621 .transport()
2622 .is_none(),
2623 "an unassociated sender has a null transport"
2624 );
2625 assert!(
2626 pc.rtp_receiver(receiver_id)
2627 .expect("receiver")
2628 .transport()
2629 .is_none(),
2630 "an unassociated receiver has a null transport"
2631 );
2632
2633 // Applying a local offer associates the transceiver. This is the offerer's window: a mid
2634 // exists, but no answer has arrived so DTLS has not started — and a browser reports a
2635 // transport here, which is why the predicate is association rather than "DTLS is up".
2636 let offer = pc.create_offer(None).expect("create offer");
2637 pc.set_local_description(Instant::now(), offer)
2638 .expect("set local description");
2639 assert!(pc.rtp_transceivers[sender_id.0].mid().is_some());
2640 assert!(
2641 !pc.dtls_transport().is_started(),
2642 "no answer yet, so DTLS has not been brought up"
2643 );
2644
2645 let sender_transport_id = pc
2646 .rtp_sender(sender_id)
2647 .expect("sender")
2648 .transport()
2649 .expect("an associated sender has a transport")
2650 .id();
2651 let receiver_transport_id = pc
2652 .rtp_receiver(receiver_id)
2653 .expect("receiver")
2654 .transport()
2655 .expect("an associated receiver has a transport")
2656 .id();
2657
2658 // Under bundling both directions share one DTLS transport, as the spec says...
2659 assert_eq!(sender_transport_id, receiver_transport_id);
2660 // ...and it is the same transport the rest of the graph names.
2661 assert_eq!(sender_transport_id, pc.dtls_transport().id);
2662 // Its state is `New` until the handshake begins: a transport that exists but has not
2663 // connected, exactly as a browser reports in this window.
2664 assert_eq!(RTCDtlsTransportState::New, pc.dtls_transport().state());
2665 }
2666
2667 // `with_discard_local_candidates_during_ice_restart` has to reach `apply_restart`, not just
2668 // land in the struct. Applying a restart with it set must empty the local candidates the
2669 // agent had gathered; with it unset they survive.
2670 //
2671 // This is the setting that makes a socket-rebinding ICE restart work (webrtc#868): retained
2672 // candidates name addresses nothing is bound to any more, so checks written for them go
2673 // nowhere and the restarted generation never leaves `Checking`.
2674 #[test]
2675 fn discard_local_candidates_during_ice_restart_reaches_apply_restart() {
2676 fn restart_with(discard: bool) -> usize {
2677 let setting_engine = SettingEngineBuilder::new()
2678 .with_discard_local_candidates_during_ice_restart(discard)
2679 .build();
2680 let mut pc = RTCPeerConnectionBuilder::new()
2681 .with_setting_engine(setting_engine)
2682 .build(Instant::now())
2683 .unwrap();
2684
2685 // Gather one host candidate so there is something to keep or drop.
2686 pc.add_local_candidate(RTCIceCandidateInit {
2687 candidate: "candidate:1 1 udp 2130706431 127.0.0.1 5000 typ host".to_owned(),
2688 ..Default::default()
2689 })
2690 .expect("add local candidate");
2691 assert_eq!(
2692 1,
2693 pc.ice_transport().get_local_candidates().unwrap().len(),
2694 "precondition: the agent holds the gathered candidate"
2695 );
2696
2697 pc.ice_transport_mut()
2698 .generate_restart_credentials(
2699 "newufrag".to_owned(),
2700 "newpasswordlongenough".to_owned(),
2701 )
2702 .expect("stage restart");
2703 pc.apply_ice_restart(Instant::now())
2704 .expect("apply ice restart");
2705
2706 pc.ice_transport().get_local_candidates().unwrap().len()
2707 }
2708
2709 assert_eq!(
2710 0,
2711 restart_with(true),
2712 "with discard enabled the stale generation's candidates are dropped"
2713 );
2714 assert_eq!(
2715 1,
2716 restart_with(false),
2717 "the default keeps them, which is the pre-existing behaviour"
2718 );
2719 }
2720
2721 // Two connections must never report the same transport as each other's. This is the case a
2722 // per-connection counter or a small-integer scheme gets wrong.
2723 #[test]
2724 fn transports_of_two_peer_connections_are_never_equal() {
2725 let mut ids = vec![];
2726 for _ in 0..2 {
2727 let mut pc = RTCPeerConnectionBuilder::new()
2728 .build(Instant::now())
2729 .unwrap();
2730 pc.sctp_transport_mut()
2731 .start(
2732 RTCDtlsRole::Client,
2733 crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities {
2734 max_message_size: 0,
2735 },
2736 5000,
2737 5000,
2738 )
2739 .expect("start");
2740 let sctp = pc.sctp().expect("SCTP is negotiated");
2741 ids.push((
2742 sctp.id(),
2743 sctp.transport().id(),
2744 sctp.transport().ice_transport().id(),
2745 ));
2746 }
2747
2748 let (a_sctp, a_dtls, a_ice) = ids[0];
2749 let (b_sctp, b_dtls, b_ice) = ids[1];
2750 assert_ne!(a_sctp, b_sctp);
2751 assert_ne!(a_dtls, b_dtls);
2752 assert_ne!(a_ice, b_ice);
2753 }
2754
2755 #[test]
2756 fn create_data_channel_dials_immediately_when_sctp_association_present() {
2757 let mut pc = RTCPeerConnectionBuilder::new()
2758 .build(Instant::now())
2759 .unwrap();
2760
2761 // Simulate an SCTP association so create_data_channel sees a transport
2762 // that is ready to open streams, and the resolved DTLS role that always accompanies
2763 // one in practice — the association is built by `SCTPTransport::start`, which runs
2764 // after `prepare_transport` has settled the role. Without it there is no correct
2765 // stream-id parity to pick and the channel would rightly defer (RFC 8832 §6).
2766 pc.dtls_transport_mut().dtls_role = RTCDtlsRole::Client;
2767 pc.sctp_transport_mut()
2768 .sctp_associations
2769 .insert(AssociationHandle(0), sctp::Association::default());
2770
2771 let _dc = pc.create_data_channel("test", None).unwrap();
2772
2773 let internal = pc
2774 .data_channels
2775 .values()
2776 .next()
2777 .expect("data channel must be stored internally");
2778 assert!(internal.data_channel.is_some());
2779 assert_eq!(
2780 internal.ready_state,
2781 RTCDataChannelState::Connecting,
2782 "a dialed in-band channel stays Connecting until its DATA_CHANNEL_ACK arrives"
2783 );
2784 assert_eq!(
2785 internal.stream_id,
2786 Some(0),
2787 "the DTLS client must take an even stream id (RFC 8832 §6)"
2788 );
2789 }
2790
2791 /// The mirror of the case above: with no resolved DTLS role there is no correct parity, so
2792 /// the channel is created without a stream id and waits for the connected procedure rather
2793 /// than being dialed or rejected.
2794 #[test]
2795 fn create_data_channel_defers_stream_id_when_dtls_role_unresolved() {
2796 let mut pc = RTCPeerConnectionBuilder::new()
2797 .build(Instant::now())
2798 .unwrap();
2799 pc.sctp_transport_mut()
2800 .sctp_associations
2801 .insert(AssociationHandle(0), sctp::Association::default());
2802
2803 let dc = pc.create_data_channel("test", None).unwrap();
2804 let id = dc.id();
2805
2806 let internal = pc.data_channels.get(&id).unwrap();
2807 assert_eq!(internal.stream_id, None, "no parity is knowable yet");
2808 assert!(
2809 internal.data_channel.is_none(),
2810 "a channel with no stream id cannot be dialed"
2811 );
2812 }
2813
2814 // ---- Rollback (RFC 9429, Section 5.7) ----
2815
2816 use crate::peer_connection::configuration::media_engine::MediaEngine;
2817 use crate::rtp_transceiver::RTCRtpTransceiverInit;
2818
2819 fn media_pc() -> RTCPeerConnection {
2820 let mut me = MediaEngine::default();
2821 me.register_default_codecs().unwrap();
2822 RTCPeerConnectionBuilder::new()
2823 .with_media_engine(me)
2824 .build(Instant::now())
2825 .unwrap()
2826 }
2827
2828 /// Builds an audio+video offer from a freshly-created peer connection.
2829 fn audio_video_offer() -> RTCSessionDescription {
2830 let mut offerer = media_pc();
2831 offerer
2832 .add_transceiver_from_kind(
2833 RtpCodecKind::Audio,
2834 Some(RTCRtpTransceiverInit {
2835 direction: RTCRtpTransceiverDirection::Recvonly,
2836 streams: vec![],
2837 send_encodings: vec![],
2838 }),
2839 )
2840 .unwrap();
2841 offerer
2842 .add_transceiver_from_kind(
2843 RtpCodecKind::Video,
2844 Some(RTCRtpTransceiverInit {
2845 direction: RTCRtpTransceiverDirection::Recvonly,
2846 streams: vec![],
2847 send_encodings: vec![],
2848 }),
2849 )
2850 .unwrap();
2851 offerer.create_offer(None).unwrap()
2852 }
2853
2854 fn rollback() -> RTCSessionDescription {
2855 RTCSessionDescription {
2856 sdp_type: RTCSdpType::Rollback,
2857 sdp: String::new(),
2858 parsed: None,
2859 }
2860 }
2861
2862 #[test]
2863 fn set_remote_rollback_removes_transceivers_created_by_remote_offer() {
2864 let offer = audio_video_offer();
2865
2866 let mut pc = media_pc();
2867 pc.set_remote_description(Instant::now(), offer).unwrap();
2868
2869 // Applying the remote offer implicitly creates two transceivers, each associated
2870 // with an "m=" section.
2871 assert_eq!(pc.rtp_transceivers.len(), 2);
2872 assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
2873 assert!(pc.rtp_transceivers.iter().all(|t| t.mid().is_some()));
2874
2875 // Rolling back the offer must stop and remove those transceivers and return to stable.
2876 pc.set_remote_description(Instant::now(), rollback())
2877 .unwrap();
2878
2879 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
2880 assert!(
2881 pc.rtp_transceivers.is_empty(),
2882 "transceivers created by a rolled-back remote offer must be removed"
2883 );
2884 }
2885
2886 #[test]
2887 fn set_local_rollback_disassociates_but_keeps_app_created_transceivers() {
2888 // A locally-initiated offer: the transceivers are created by the application, so
2889 // rolling back the local offer via set_local_description must disassociate them
2890 // (clear their mids) but must NOT remove them. This exercises the same rollback
2891 // cleanup path as set_remote_description, per RFC 9429 Section 5.7.
2892 let mut pc = media_pc();
2893 pc.add_transceiver_from_kind(
2894 RtpCodecKind::Audio,
2895 Some(RTCRtpTransceiverInit {
2896 direction: RTCRtpTransceiverDirection::Recvonly,
2897 streams: vec![],
2898 send_encodings: vec![],
2899 }),
2900 )
2901 .unwrap();
2902
2903 let offer = pc.create_offer(None).unwrap();
2904 pc.set_local_description(Instant::now(), offer).unwrap();
2905 assert_eq!(pc.signaling_state, RTCSignalingState::HaveLocalOffer);
2906 assert_eq!(pc.rtp_transceivers.len(), 1);
2907 assert!(pc.rtp_transceivers[0].mid().is_some());
2908
2909 pc.set_local_description(Instant::now(), rollback())
2910 .unwrap();
2911
2912 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
2913 assert_eq!(
2914 pc.rtp_transceivers.len(),
2915 1,
2916 "application-created transceivers must not be removed by rollback"
2917 );
2918 assert!(
2919 pc.rtp_transceivers[0].mid().is_none(),
2920 "rolled-back transceiver must be disassociated from its m= section"
2921 );
2922 }
2923
2924 #[test]
2925 fn rollback_keeps_transceiver_with_track_attached_via_add_track() {
2926 let offer = audio_video_offer();
2927
2928 let mut pc = media_pc();
2929 pc.set_remote_description(Instant::now(), offer).unwrap();
2930 assert_eq!(pc.rtp_transceivers.len(), 2);
2931
2932 // Attach a local track. add_track reuses the sender-less audio transceiver that was
2933 // created by the remote offer, giving it a sender.
2934 let track = MediaStreamTrack::new(
2935 "stream".to_owned(),
2936 "track".to_owned(),
2937 "label".to_owned(),
2938 RtpCodecKind::Audio,
2939 vec![],
2940 );
2941 pc.add_track(track).unwrap();
2942
2943 pc.set_remote_description(Instant::now(), rollback())
2944 .unwrap();
2945
2946 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
2947 // The video transceiver (no track) is removed; the audio transceiver with the attached
2948 // track is kept but disassociated (mid cleared) so a future offer can re-add it.
2949 assert_eq!(
2950 pc.rtp_transceivers.len(),
2951 1,
2952 "transceiver with a track attached via add_track must not be removed"
2953 );
2954 let kept = &pc.rtp_transceivers[0];
2955 assert_eq!(kept.kind(), RtpCodecKind::Audio);
2956 assert!(kept.sender().is_some());
2957 assert!(
2958 kept.mid().is_none(),
2959 "kept transceiver must be disassociated from its m= section on rollback"
2960 );
2961 }
2962
2963 #[test]
2964 fn rollback_keeps_transceiver_negotiated_by_a_previous_exchange() {
2965 // First, complete a full offer/answer so an incoming transceiver becomes part of the
2966 // stable state (its mid is recorded in the current remote description).
2967 let offer = audio_video_offer();
2968 let mut pc = media_pc();
2969 pc.set_remote_description(Instant::now(), offer).unwrap();
2970 assert_eq!(pc.rtp_transceivers.len(), 2);
2971 let answer = pc.create_answer(None).unwrap();
2972 pc.set_local_description(Instant::now(), answer).unwrap();
2973 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
2974 let negotiated_mids: Vec<_> = pc
2975 .rtp_transceivers
2976 .iter()
2977 .map(|t| t.mid().clone())
2978 .collect();
2979 assert!(negotiated_mids.iter().all(|m| m.is_some()));
2980
2981 // Now a renegotiation arrives and is rolled back. The transceivers negotiated by the
2982 // FIRST exchange must survive with their mids intact — rollback only undoes the pending
2983 // (second) transaction, not committed state.
2984 let reoffer = audio_video_offer();
2985 pc.set_remote_description(Instant::now(), reoffer).unwrap();
2986 assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
2987
2988 pc.set_remote_description(Instant::now(), rollback())
2989 .unwrap();
2990
2991 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
2992 assert_eq!(
2993 pc.rtp_transceivers.len(),
2994 2,
2995 "previously-negotiated transceivers must not be removed by a renegotiation rollback"
2996 );
2997 let mids_after: Vec<_> = pc
2998 .rtp_transceivers
2999 .iter()
3000 .map(|t| t.mid().clone())
3001 .collect();
3002 assert_eq!(
3003 mids_after, negotiated_mids,
3004 "previously-negotiated transceivers must keep their mid across rollback"
3005 );
3006 }
3007
3008 #[test]
3009 fn add_track_then_rollback_remote_offer_then_create_offer_includes_track() {
3010 // RFC 9429, Section 5.7: "an application may call addTrack, then call
3011 // setRemoteDescription with an offer, then roll back that offer, then call createOffer
3012 // and have an "m=" section for the added track appear in the generated offer."
3013 let mut pc = media_pc();
3014
3015 // 1. addTrack — creates a local sendrecv audio transceiver with a sender.
3016 let track = MediaStreamTrack::new(
3017 "stream".to_owned(),
3018 "track".to_owned(),
3019 "label".to_owned(),
3020 RtpCodecKind::Audio,
3021 vec![],
3022 );
3023 pc.add_track(track).unwrap();
3024 assert_eq!(pc.rtp_transceivers.len(), 1);
3025
3026 // 2. setRemoteDescription with a remote (video-only) offer. This reuses no existing
3027 // transceiver (different kind), so it creates a second, remote-originated one.
3028 let mut video_offerer = media_pc();
3029 video_offerer
3030 .add_transceiver_from_kind(
3031 RtpCodecKind::Video,
3032 Some(RTCRtpTransceiverInit {
3033 direction: RTCRtpTransceiverDirection::Recvonly,
3034 streams: vec![],
3035 send_encodings: vec![],
3036 }),
3037 )
3038 .unwrap();
3039 let remote_offer = video_offerer.create_offer(None).unwrap();
3040 pc.set_remote_description(Instant::now(), remote_offer)
3041 .unwrap();
3042 assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
3043 // The audio transceiver (with our track) got associated with the offer's m= section.
3044 assert!(
3045 pc.rtp_transceivers
3046 .iter()
3047 .any(|t| t.kind() == RtpCodecKind::Audio && t.sender().is_some())
3048 );
3049
3050 // 3. Roll back that offer.
3051 pc.set_remote_description(Instant::now(), rollback())
3052 .unwrap();
3053 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
3054
3055 // The audio transceiver with the attached track must survive (disassociated), while the
3056 // remote-created video transceiver must be gone.
3057 assert_eq!(
3058 pc.rtp_transceivers.len(),
3059 1,
3060 "the add_track transceiver must survive rollback; the remote one must be removed"
3061 );
3062 let kept = &pc.rtp_transceivers[0];
3063 assert_eq!(kept.kind(), RtpCodecKind::Audio);
3064 assert!(kept.sender().is_some());
3065 assert!(kept.mid().is_none(), "must be disassociated after rollback");
3066
3067 // 4. createOffer — an m= section for the added (audio) track must appear.
3068 let offer = pc.create_offer(None).unwrap();
3069 assert_eq!(
3070 offer.sdp.matches("m=audio").count(),
3071 1,
3072 "createOffer after rollback must emit an m=audio section for the added track"
3073 );
3074 // The transceiver has been re-associated (given a mid) for the new offer.
3075 assert!(pc.rtp_transceivers[0].mid().is_some());
3076 }
3077
3078 #[test]
3079 fn add_track_then_rollback_local_offer_then_answer_remote_still_renegotiates_track() {
3080 // Polite-peer glare scenario (RFC 9429, Section 5.7): the application calls addTrack and
3081 // sends its own offer, then a remote offer arrives (collision). The polite peer rolls
3082 // back its local offer, applies the remote offer, and answers it. The locally-added
3083 // track was never negotiated, so it must still be pending and appear in a subsequent
3084 // offer — mirroring the RFC's addTrack/rollback/createOffer guarantee for the local case.
3085 let mut pc = media_pc();
3086
3087 // 1. addTrack (audio) — local sendrecv audio transceiver with a sender.
3088 let track = MediaStreamTrack::new(
3089 "stream".to_owned(),
3090 "track".to_owned(),
3091 "label".to_owned(),
3092 RtpCodecKind::Audio,
3093 vec![],
3094 );
3095 pc.add_track(track).unwrap();
3096
3097 // 2. set_local_description(our offer) — enters have-local-offer, audio gets a mid.
3098 let local_offer = pc.create_offer(None).unwrap();
3099 pc.set_local_description(Instant::now(), local_offer)
3100 .unwrap();
3101 assert_eq!(pc.signaling_state, RTCSignalingState::HaveLocalOffer);
3102 assert!(pc.rtp_transceivers[0].mid().is_some());
3103
3104 // 3. A remote (video) offer arrives — glare.
3105 let mut video_offerer = media_pc();
3106 video_offerer
3107 .add_transceiver_from_kind(
3108 RtpCodecKind::Video,
3109 Some(RTCRtpTransceiverInit {
3110 direction: RTCRtpTransceiverDirection::Recvonly,
3111 streams: vec![],
3112 send_encodings: vec![],
3113 }),
3114 )
3115 .unwrap();
3116 let remote_offer = video_offerer.create_offer(None).unwrap();
3117
3118 // 4. Roll back our local offer (via set_local_description, the polite-peer path).
3119 pc.set_local_description(Instant::now(), rollback())
3120 .unwrap();
3121 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
3122 // Our audio transceiver survives (has a track/sender) but is disassociated.
3123 assert_eq!(pc.rtp_transceivers.len(), 1);
3124 assert_eq!(pc.rtp_transceivers[0].kind(), RtpCodecKind::Audio);
3125 assert!(pc.rtp_transceivers[0].sender().is_some());
3126 assert!(pc.rtp_transceivers[0].mid().is_none());
3127
3128 // 5. Apply the remote offer, then answer it.
3129 pc.set_remote_description(Instant::now(), remote_offer)
3130 .unwrap();
3131 assert_eq!(pc.signaling_state, RTCSignalingState::HaveRemoteOffer);
3132 let answer = pc.create_answer(None).unwrap();
3133 // The answer only covers the remote's video m= section; our audio track is not yet
3134 // negotiated, so it must NOT appear in the answer.
3135 assert_eq!(answer.sdp.matches("m=video").count(), 1);
3136 assert_eq!(answer.sdp.matches("m=audio").count(), 0);
3137 pc.set_local_description(Instant::now(), answer).unwrap();
3138 assert_eq!(pc.signaling_state, RTCSignalingState::Stable);
3139
3140 // 6. The locally-added track was never negotiated, so a follow-up offer must include an
3141 // m=audio section for it (alongside the now-negotiated video section).
3142 let followup_offer = pc.create_offer(None).unwrap();
3143 assert_eq!(
3144 followup_offer.sdp.matches("m=audio").count(),
3145 1,
3146 "the added track must appear in the offer generated after rollback + answer"
3147 );
3148 assert_eq!(followup_offer.sdp.matches("m=video").count(), 1);
3149 assert!(
3150 pc.rtp_transceivers
3151 .iter()
3152 .find(|t| t.kind() == RtpCodecKind::Audio)
3153 .unwrap()
3154 .mid()
3155 .is_some(),
3156 "audio transceiver must be re-associated for the follow-up offer"
3157 );
3158 }
3159}