use std::sync::Arc; use crate::Candidate; use crate::IceCreds; use crate::Rtc; use crate::RtcError; use crate::channel::ChannelId; use crate::crypto::Fingerprint; use crate::crypto::dtls::ProtocolVersion; use crate::media::{Media, MediaKind}; use crate::rtp_::MidRid; use crate::rtp_::{Mid, Rid, Ssrc}; use crate::sctp::{ChannelConfig, SctpInitData}; use crate::streams::{DEFAULT_RTX_CACHE_DURATION, DEFAULT_RTX_RATIO_CAP, StreamRx, StreamTx}; /// Direct change strategy. /// /// Makes immediate changes to the Rtc session without any SDP OFFER/ANSWER. This /// is an alternative to [`Rtc::sdp_api()`] for use cases when you don’t want to use SDP /// (or when you want to write RTP directly). /// /// To use the Direct API together with a browser client, you would need to make /// the equivalent changes on the browser side by manually generating the correct /// SDP OFFER/ANSWER to make the `RTCPeerConnection` match str0m's state. /// /// To change str0m's state through the Direct API followed by the SDP API produce /// an SDP OFFER is not a supported use case. Either pick SDP API and let str0m handle /// the OFFER/ANSWER or use Direct API and deal with SDP manually. Not both. /// ///
This is a low level API. /// /// str0m normally guarantees that user input cannot cause panics. /// However as an exception, the Direct API does allow the user to configure the /// session in a way that is internally inconsistent. Such situations can /// result in panics. ///
pub struct DirectApi<'a> { rtc: &'a mut Rtc, } impl<'a> DirectApi<'a> { /// Creates a new instance of the `DirectApi` struct with the specified `Rtc` instance. /// /// The `DirectApi` struct provides a high-level API for interacting with a WebRTC peer connection, /// and the `Rtc` instance provides low-level access to the underlying WebRTC functionality. pub fn new(rtc: &'a mut Rtc) -> Self { DirectApi { rtc } } /// Sets the ICE controlling flag for this peer connection. /// /// If `controlling` is `true`, this peer connection is set as the ICE controlling agent, /// meaning it will take the initiative to send connectivity checks and control the pace of /// connectivity checks sent between two peers during the ICE session. /// /// If `controlling` is `false`, this peer connection is set as the ICE controlled agent, /// meaning it will respond to connectivity checks sent by the controlling agent. pub fn set_ice_controlling(&mut self, controlling: bool) { self.rtc.ice.set_controlling(controlling); } /// Returns a reference to the local ICE credentials used by this peer connection. /// /// The ICE credentials consist of the username and password used by the ICE agent during /// the ICE session to authenticate and exchange connectivity checks with the remote peer. pub fn local_ice_credentials(&self) -> IceCreds { self.rtc.ice.local_credentials().clone() } /// Sets the local ICE credentials. pub fn set_local_ice_credentials(&mut self, local_ice_credentials: IceCreds) { self.rtc.ice.set_local_credentials(local_ice_credentials); } /// Sets the remote ICE credentials. pub fn set_remote_ice_credentials(&mut self, remote_ice_credentials: IceCreds) { self.rtc.ice.set_remote_credentials(remote_ice_credentials); } /// Invalidate a candidate and remove it from the connection. /// /// This is done for host candidates disappearing due to changes in the network /// interfaces like a WiFi disconnecting or changing IPs. /// /// It can also be used to invalidate _remote_ candidates, i.e. if the remote /// has signalled us that they have invalidated one of their candidates. /// /// Returns `true` if the candidate was found and invalidated. pub fn invalidate_candidate(&mut self, c: &Candidate) -> bool { self.rtc.ice.invalidate_candidate(c) } /// Returns a reference to the local DTLS fingerprint used by this peer connection. /// /// The DTLS fingerprint is a hash of the local SSL/TLS certificate used to authenticate the /// peer connection and establish a secure communication channel between the peers. pub fn local_dtls_fingerprint(&self) -> &Fingerprint { self.rtc.dtls.local_fingerprint() } /// Returns a reference to the remote DTLS fingerprint used by this peer connection. pub fn remote_dtls_fingerprint(&self) -> Option<&Fingerprint> { self.rtc.dtls.remote_fingerprint() } /// Returns the negotiated DTLS protocol version. /// /// Call this after receiving [`crate::Event::Connected`] /// to learn the negotiated DTLS version. Before the handshake completes, /// this may return `None` pub fn dtls_protocol_version(&self) -> Option { self.rtc.dtls.protocol_version() } /// Sets the remote DTLS fingerprint. pub fn set_remote_fingerprint(&mut self, dtls_fingerprint: Fingerprint) { self.rtc.remote_fingerprint = Some(dtls_fingerprint); } /// Start the DTLS subsystem. pub fn start_dtls(&mut self, active: bool) -> Result<(), RtcError> { self.rtc.init_dtls(active) } /// Start the SCTP over DTLS. /// /// When `client` is `true`, this side initiates the SCTP association as the /// connecting party. pub fn start_sctp(&mut self, client: bool) { self.rtc .try_init_sctp(client, None, None) .expect("starting SCTP should be infallible") } /// Start SCTP over DTLS using out-of-band SCTP INIT data. /// /// This is used for SNAP (SCTP Negotiation Acceleration Protocol), which /// allows skipping the 4-way SCTP handshake entirely once both peers have /// exchanged their SCTP INIT chunks out of band. /// /// The `sctp_init_data` must contain: /// /// 1. A local INIT chunk generated by calling /// [`SctpInitData::local_init_chunk()`]. /// 2. The remote INIT chunk set via [`SctpInitData::set_remote_init_chunk()`]. /// /// This method returns an error unless both the local and remote INIT /// chunks are present. /// /// This method is the SNAP-specific alternative to [`Self::start_sctp()`]. /// /// # Example /// ```ignore /// use str0m::channel::SctpInitData; /// /// let mut init_data = SctpInitData::new(); /// let local_init = init_data.local_init_chunk().unwrap(); /// // ... exchange local_init via signaling, receive remote_init ... /// init_data.set_remote_init_chunk(remote_init); /// rtc.direct_api().start_sctp_with_snap(false, init_data)?; /// ``` pub fn start_sctp_with_snap( &mut self, client: bool, sctp_init_data: SctpInitData, ) -> Result<(), RtcError> { self.rtc.try_init_sctp(client, Some(sctp_init_data), None) } /// Create a new data channel. pub fn create_data_channel(&mut self, config: ChannelConfig) -> ChannelId { let id = self.rtc.chan.new_channel(&config); self.rtc.chan.confirm(id, config); id } /// Close a data channel. pub fn close_data_channel(&mut self, channel_id: ChannelId) { self.rtc.chan.close_channel(channel_id, &mut self.rtc.sctp); } /// Set whether to enable ice-lite. pub fn set_ice_lite(&mut self, ice_lite: bool) { self.rtc.ice.set_ice_lite(ice_lite); } /// Enable twcc feedback. pub fn enable_twcc_feedback(&mut self) { self.rtc.session.enable_twcc_feedback() } /// Generate a ssrc that is not already used in session pub fn new_ssrc(&self) -> Ssrc { self.rtc.session.streams.new_ssrc() } /// Get the str0m `ChannelId` by an `sctp_stream_id`. /// /// This is useful when using out of band negotiated sctp stream id in /// [`Self::create_data_channel()`] pub fn channel_id_by_sctp_stream_id(&self, id: u16) -> Option { self.rtc.chan.channel_id_by_stream_id(id) } /// Get the `sctp_stream_id` from a str0m `ChannelId`. /// /// This is useful when using out of band negotiated sctp stream id in /// [`Self::create_data_channel()`] pub fn sctp_stream_id_by_channel_id(&self, id: ChannelId) -> Option { self.rtc.chan.stream_id_by_channel_id(id) } /// Create a new `Media`. /// /// All streams belong to a media identified by a `mid`. This creates the media without /// doing any SDP dance. pub fn declare_media(&mut self, mid: Mid, kind: MediaKind) -> &mut Media { let max_index = self.rtc.session.medias.iter().map(|m| m.index()).max(); let next_index = if let Some(max_index) = max_index { max_index + 1 } else { 0 }; let exts = self.rtc.session.exts.cloned_with_type(kind.is_audio()); let m = Media::from_direct_api(mid, next_index, kind, exts); self.rtc.session.medias.push(m); self.rtc.session.medias.last_mut().unwrap() } /// Remove `Media`. /// /// Removes media and all streams belong to a media identified by a `mid`. pub fn remove_media(&mut self, mid: Mid) { self.rtc.session.remove_media(mid); } /// Allow incoming traffic from remote peer for the given SSRC. /// /// Can be called multiple times if the `rtx` is discovered later via RTP header extensions. pub fn expect_stream_rx( &mut self, ssrc: Ssrc, rtx: Option, mid: Mid, rid: Option, ) -> &mut StreamRx { let Some(_media) = self.rtc.session.media_by_mid(mid) else { panic!("No media declared for mid: {}", mid); }; // By default we do not suppress nacks, this has to be called explicitly by the user of direct API. let suppress_nack = false; let midrid = MidRid(mid, rid); self.rtc .session .streams .expect_stream_rx(ssrc, rtx, midrid, suppress_nack) } /// Remove the receive stream for the given SSRC. /// /// Returns true if stream existed and was removed. pub fn remove_stream_rx(&mut self, ssrc: Ssrc) -> bool { self.rtc.session.streams.remove_stream_rx(ssrc) } /// Obtain a receive stream. /// /// In RTP mode, the receive stream is used to signal keyframe requests. /// /// The stream must first be declared using [`DirectApi::expect_stream_rx`]. pub fn stream_rx(&mut self, ssrc: &Ssrc) -> Option<&mut StreamRx> { self.rtc.session.streams.stream_rx(ssrc) } /// Obtain a recv stream by looking it up via mid/rid. pub fn stream_rx_by_mid(&mut self, mid: Mid, rid: Option) -> Option<&mut StreamRx> { let midrid = MidRid(mid, rid); self.rtc.session.streams.stream_rx_by_midrid(midrid, true) } /// Declare the intention to send data using the given SSRC. /// /// * The resend RTX is optional but necessary to do resends. str0m does not do /// resends without RTX. /// /// Can be called multiple times without changing any internal state. However /// the RTX value is only picked up the first ever time we see a new SSRC. pub fn declare_stream_tx( &mut self, ssrc: Ssrc, rtx: Option, mid: Mid, rid: Option, ) -> &mut StreamTx { let Some(media) = self.rtc.session.media_by_mid_mut(mid) else { panic!("No media declared for mid: {}", mid); }; let is_audio = media.kind().is_audio(); let midrid = MidRid(mid, rid); // If there is a RID tx, declare it so we an use it in Writer API if let Some(rid) = rid { media.add_to_rid_tx(rid); } let stream = self .rtc .session .streams .declare_stream_tx(ssrc, rtx, midrid); let size = if is_audio { self.rtc.session.send_buffer_audio } else { self.rtc.session.send_buffer_video }; stream.set_rtx_cache(size, DEFAULT_RTX_CACHE_DURATION, DEFAULT_RTX_RATIO_CAP); stream } /// Remove the transmit stream for the given SSRC. /// /// Returns true if stream existed and was removed. pub fn remove_stream_tx(&mut self, ssrc: Ssrc) -> bool { self.rtc.session.streams.remove_stream_tx(ssrc) } /// Obtain a send stream to write RTP data directly. /// /// The stream must first be declared using [`DirectApi::declare_stream_tx`]. pub fn stream_tx(&mut self, ssrc: &Ssrc) -> Option<&mut StreamTx> { self.rtc.session.streams.stream_tx(ssrc) } /// Obtain a send stream by looking it up via mid/rid. pub fn stream_tx_by_mid(&mut self, mid: Mid, rid: Option) -> Option<&mut StreamTx> { let midrid = MidRid(mid, rid); self.rtc.session.streams.stream_tx_by_midrid(midrid) } /// Reset a transmit stream to use a new SSRC and optionally a new RTX SSRC. /// /// This changes the SSRC of an existing stream and resets all relevant state. /// Use this when you need to change the SSRC of an existing stream without creating a new one. /// /// If the stream has an RTX SSRC, `new_rtx` must be provided. If the stream doesn't /// have an RTX SSRC, `new_rtx` is ignored. /// /// Returns a reference to the updated stream or None if: /// - No stream was found for the given mid/rid /// - The new SSRC is the same as the current one (no change needed) /// - The new RTX SSRC is the same as the current one (no change needed) pub fn reset_stream_tx( &mut self, mid: Mid, rid: Option, new_ssrc: Ssrc, new_rtx: Option, ) -> Option<&mut StreamTx> { let midrid = MidRid(mid, rid); // Find the stream by mid/rid let stream = self.rtc.session.streams.stream_tx_by_midrid(midrid)?; // Don't change to the same SSRC if stream.ssrc() == new_ssrc { return None; } // If the stream has an RTX SSRC, New RTX must be provided and differ. // But it is allowed to start or turn off RTX. if stream.rtx().is_some() && stream.rtx() == new_rtx { return None; } // Reset the stream with the new SSRC and RTX stream.reset_ssrc(new_ssrc, new_rtx); // Return a reference to the updated stream Some(stream) } /// Send an application-specific feedback message (PSFB FMT=15, PT=206). /// /// The message is sent in its own standalone SRTCP compound datagram, prefixed /// with a Receiver Report for RFC 3550 compliance. The `sender_ssrc` is used as /// the RR's sender SSRC, which allows remote RTCP demuxers that route compound /// packets by the first SSRC to deliver the feedback to the correct media session. /// /// The `payload` is opaque and application-defined. pub fn send_app_specific_feedback( &mut self, sender_ssrc: Ssrc, media_ssrc: Ssrc, payload: impl Into>, ) { self.rtc .session .send_app_specific_feedback(sender_ssrc, media_ssrc, payload); } /// Send a Picture Loss Indication (PLI, PT=206 FMT=1) for a media stream, /// choosing the `sender_ssrc` of the outgoing RTCP feedback. /// /// The PLI is queued into the regular RTCP compound packet. The `sender_ssrc` /// sets the feedback packet's sender SSRC. `media_ssrc` is the SSRC of the /// stream a keyframe is requested for. /// /// Unlike requesting a keyframe on a local receive stream, this does not require a /// receive stream for `media_ssrc` and lets the caller choose the sender SSRC, so it /// can relay a keyframe request for a stream sourced by the remote peer. pub fn send_pli_feedback(&mut self, sender_ssrc: Ssrc, media_ssrc: Ssrc) { self.rtc.session.send_pli_feedback(sender_ssrc, media_ssrc); } }