Skip to main content

str0m/change/
direct.rs

1use crate::Candidate;
2use crate::IceCreds;
3use crate::Rtc;
4use crate::RtcError;
5use crate::channel::ChannelId;
6use crate::crypto::Fingerprint;
7use crate::media::{Media, MediaKind};
8use crate::rtp_::MidRid;
9use crate::rtp_::{Mid, Rid, Ssrc};
10use crate::sctp::{ChannelConfig, SctpInitData};
11use crate::streams::{DEFAULT_RTX_CACHE_DURATION, DEFAULT_RTX_RATIO_CAP, StreamRx, StreamTx};
12
13/// Direct change strategy.
14///
15/// Makes immediate changes to the Rtc session without any SDP OFFER/ANSWER. This
16/// is an alternative to [`Rtc::sdp_api()`] for use cases when you don’t want to use SDP
17/// (or when you want to write RTP directly).
18///
19/// To use the Direct API together with a browser client, you would need to make
20/// the equivalent changes on the browser side by manually generating the correct
21/// SDP OFFER/ANSWER to make the `RTCPeerConnection` match str0m's state.
22///
23/// To change str0m's state through the Direct API followed by the SDP API produce
24/// an SDP OFFER is not a supported use case. Either pick SDP API and let str0m handle
25/// the OFFER/ANSWER or use Direct API and deal with SDP manually. Not both.
26///
27/// <div class="warning"><b>This is a low level API.</b>
28///
29///  str0m normally guarantees that user input cannot cause panics.
30///  However as an exception, the Direct API does allow the user to configure the
31///  session in a way that is internally inconsistent. Such situations can
32///  result in panics.
33/// </div>
34pub struct DirectApi<'a> {
35    rtc: &'a mut Rtc,
36}
37
38impl<'a> DirectApi<'a> {
39    /// Creates a new instance of the `DirectApi` struct with the specified `Rtc` instance.
40    ///
41    /// The `DirectApi` struct provides a high-level API for interacting with a WebRTC peer connection,
42    /// and the `Rtc` instance provides low-level access to the underlying WebRTC functionality.
43    pub fn new(rtc: &'a mut Rtc) -> Self {
44        DirectApi { rtc }
45    }
46
47    /// Sets the ICE controlling flag for this peer connection.
48    ///
49    /// If `controlling` is `true`, this peer connection is set as the ICE controlling agent,
50    /// meaning it will take the initiative to send connectivity checks and control the pace of
51    /// connectivity checks sent between two peers during the ICE session.
52    ///
53    /// If `controlling` is `false`, this peer connection is set as the ICE controlled agent,
54    /// meaning it will respond to connectivity checks sent by the controlling agent.
55    pub fn set_ice_controlling(&mut self, controlling: bool) {
56        self.rtc.ice.set_controlling(controlling);
57    }
58
59    /// Returns a reference to the local ICE credentials used by this peer connection.
60    ///
61    /// The ICE credentials consist of the username and password used by the ICE agent during
62    /// the ICE session to authenticate and exchange connectivity checks with the remote peer.
63    pub fn local_ice_credentials(&self) -> IceCreds {
64        self.rtc.ice.local_credentials().clone()
65    }
66
67    /// Sets the local ICE credentials.
68    pub fn set_local_ice_credentials(&mut self, local_ice_credentials: IceCreds) {
69        self.rtc.ice.set_local_credentials(local_ice_credentials);
70    }
71
72    /// Sets the remote ICE credentials.
73    pub fn set_remote_ice_credentials(&mut self, remote_ice_credentials: IceCreds) {
74        self.rtc.ice.set_remote_credentials(remote_ice_credentials);
75    }
76
77    /// Invalidate a candidate and remove it from the connection.
78    ///
79    /// This is done for host candidates disappearing due to changes in the network
80    /// interfaces like a WiFi disconnecting or changing IPs.
81    ///
82    /// It can also be used to invalidate _remote_ candidates, i.e. if the remote
83    /// has signalled us that they have invalidated one of their candidates.
84    ///
85    /// Returns `true` if the candidate was found and invalidated.
86    pub fn invalidate_candidate(&mut self, c: &Candidate) -> bool {
87        self.rtc.ice.invalidate_candidate(c)
88    }
89
90    /// Returns a reference to the local DTLS fingerprint used by this peer connection.
91    ///
92    /// The DTLS fingerprint is a hash of the local SSL/TLS certificate used to authenticate the
93    /// peer connection and establish a secure communication channel between the peers.
94    pub fn local_dtls_fingerprint(&self) -> &Fingerprint {
95        self.rtc.dtls.local_fingerprint()
96    }
97
98    /// Returns a reference to the remote DTLS fingerprint used by this peer connection.
99    pub fn remote_dtls_fingerprint(&self) -> Option<&Fingerprint> {
100        self.rtc.dtls.remote_fingerprint()
101    }
102
103    /// Sets the remote DTLS fingerprint.
104    pub fn set_remote_fingerprint(&mut self, dtls_fingerprint: Fingerprint) {
105        self.rtc.remote_fingerprint = Some(dtls_fingerprint);
106    }
107
108    /// Start the DTLS subsystem.
109    pub fn start_dtls(&mut self, active: bool) -> Result<(), RtcError> {
110        self.rtc.init_dtls(active)
111    }
112
113    /// Start the SCTP over DTLS.
114    ///
115    /// When `client` is `true`, this side initiates the SCTP association as the
116    /// connecting party.
117    pub fn start_sctp(&mut self, client: bool) {
118        self.rtc
119            .try_init_sctp(client, None)
120            .expect("starting SCTP should be infallible")
121    }
122
123    /// Start SCTP over DTLS using out-of-band SCTP INIT data.
124    ///
125    /// This is used for SNAP (SCTP Negotiation Acceleration Protocol), which
126    /// allows skipping the 4-way SCTP handshake entirely once both peers have
127    /// exchanged their SCTP INIT chunks out of band.
128    ///
129    /// The `sctp_init_data` must contain:
130    ///
131    /// 1. A local INIT chunk generated by calling
132    ///    [`SctpInitData::local_init_chunk()`].
133    /// 2. The remote INIT chunk set via [`SctpInitData::set_remote_init_chunk()`].
134    ///
135    /// This method returns an error unless both the local and remote INIT     
136    /// chunks are present.
137    ///
138    /// This method is the SNAP-specific alternative to [`Self::start_sctp()`].
139    ///
140    /// # Example
141    /// ```ignore
142    /// use str0m::channel::SctpInitData;
143    ///
144    /// let mut init_data = SctpInitData::new();
145    /// let local_init = init_data.local_init_chunk().unwrap();
146    /// // ... exchange local_init via signaling, receive remote_init ...      
147    /// init_data.set_remote_init_chunk(remote_init);
148    /// rtc.direct_api().start_sctp_with_snap(false, init_data)?;
149    /// ```
150    pub fn start_sctp_with_snap(
151        &mut self,
152        client: bool,
153        sctp_init_data: SctpInitData,
154    ) -> Result<(), RtcError> {
155        self.rtc.try_init_sctp(client, Some(sctp_init_data))
156    }
157
158    /// Create a new data channel.
159    pub fn create_data_channel(&mut self, config: ChannelConfig) -> ChannelId {
160        let id = self.rtc.chan.new_channel(&config);
161        self.rtc.chan.confirm(id, config);
162        id
163    }
164
165    /// Close a data channel.
166    pub fn close_data_channel(&mut self, channel_id: ChannelId) {
167        self.rtc.chan.close_channel(channel_id, &mut self.rtc.sctp);
168    }
169
170    /// Set whether to enable ice-lite.
171    pub fn set_ice_lite(&mut self, ice_lite: bool) {
172        self.rtc.ice.set_ice_lite(ice_lite);
173    }
174
175    /// Enable twcc feedback.
176    pub fn enable_twcc_feedback(&mut self) {
177        self.rtc.session.enable_twcc_feedback()
178    }
179
180    /// Generate a ssrc that is not already used in session
181    pub fn new_ssrc(&self) -> Ssrc {
182        self.rtc.session.streams.new_ssrc()
183    }
184
185    /// Get the str0m `ChannelId` by an `sctp_stream_id`.
186    ///
187    /// This is useful when using out of band negotiated sctp stream id in
188    /// [`Self::create_data_channel()`]
189    pub fn channel_id_by_sctp_stream_id(&self, id: u16) -> Option<ChannelId> {
190        self.rtc.chan.channel_id_by_stream_id(id)
191    }
192
193    /// Get the `sctp_stream_id` from a str0m `ChannelId`.
194    ///
195    /// This is useful when using out of band negotiated sctp stream id in
196    /// [`Self::create_data_channel()`]
197    pub fn sctp_stream_id_by_channel_id(&self, id: ChannelId) -> Option<u16> {
198        self.rtc.chan.stream_id_by_channel_id(id)
199    }
200
201    /// Create a new `Media`.
202    ///
203    /// All streams belong to a media identified by a `mid`. This creates the media without
204    /// doing any SDP dance.
205    pub fn declare_media(&mut self, mid: Mid, kind: MediaKind) -> &mut Media {
206        let max_index = self.rtc.session.medias.iter().map(|m| m.index()).max();
207
208        let next_index = if let Some(max_index) = max_index {
209            max_index + 1
210        } else {
211            0
212        };
213
214        let exts = self.rtc.session.exts.cloned_with_type(kind.is_audio());
215        let m = Media::from_direct_api(mid, next_index, kind, exts);
216
217        self.rtc.session.medias.push(m);
218        self.rtc.session.medias.last_mut().unwrap()
219    }
220
221    /// Remove `Media`.
222    ///
223    /// Removes media and all streams belong to a media identified by a `mid`.
224    pub fn remove_media(&mut self, mid: Mid) {
225        self.rtc.session.remove_media(mid);
226    }
227
228    /// Allow incoming traffic from remote peer for the given SSRC.
229    ///
230    /// Can be called multiple times if the `rtx` is discovered later via RTP header extensions.
231    pub fn expect_stream_rx(
232        &mut self,
233        ssrc: Ssrc,
234        rtx: Option<Ssrc>,
235        mid: Mid,
236        rid: Option<Rid>,
237    ) -> &mut StreamRx {
238        let Some(_media) = self.rtc.session.media_by_mid(mid) else {
239            panic!("No media declared for mid: {}", mid);
240        };
241
242        // By default we do not suppress nacks, this has to be called explicitly by the user of direct API.
243        let suppress_nack = false;
244
245        let midrid = MidRid(mid, rid);
246
247        self.rtc
248            .session
249            .streams
250            .expect_stream_rx(ssrc, rtx, midrid, suppress_nack)
251    }
252
253    /// Remove the receive stream for the given SSRC.
254    ///
255    /// Returns true if stream existed and was removed.
256    pub fn remove_stream_rx(&mut self, ssrc: Ssrc) -> bool {
257        self.rtc.session.streams.remove_stream_rx(ssrc)
258    }
259
260    /// Obtain a receive stream.
261    ///
262    /// In RTP mode, the receive stream is used to signal keyframe requests.
263    ///
264    /// The stream must first be declared using [`DirectApi::expect_stream_rx`].
265    pub fn stream_rx(&mut self, ssrc: &Ssrc) -> Option<&mut StreamRx> {
266        self.rtc.session.streams.stream_rx(ssrc)
267    }
268
269    /// Obtain a recv stream by looking it up via mid/rid.
270    pub fn stream_rx_by_mid(&mut self, mid: Mid, rid: Option<Rid>) -> Option<&mut StreamRx> {
271        let midrid = MidRid(mid, rid);
272        self.rtc.session.streams.stream_rx_by_midrid(midrid, true)
273    }
274
275    /// Declare the intention to send data using the given SSRC.
276    ///
277    /// * The resend RTX is optional but necessary to do resends. str0m does not do
278    ///   resends without RTX.
279    ///
280    /// Can be called multiple times without changing any internal state. However
281    /// the RTX value is only picked up the first ever time we see a new SSRC.
282    pub fn declare_stream_tx(
283        &mut self,
284        ssrc: Ssrc,
285        rtx: Option<Ssrc>,
286        mid: Mid,
287        rid: Option<Rid>,
288    ) -> &mut StreamTx {
289        let Some(media) = self.rtc.session.media_by_mid_mut(mid) else {
290            panic!("No media declared for mid: {}", mid);
291        };
292
293        let is_audio = media.kind().is_audio();
294
295        let midrid = MidRid(mid, rid);
296
297        // If there is a RID tx, declare it so we an use it in Writer API
298        if let Some(rid) = rid {
299            media.add_to_rid_tx(rid);
300        }
301
302        let stream = self
303            .rtc
304            .session
305            .streams
306            .declare_stream_tx(ssrc, rtx, midrid);
307
308        let size = if is_audio {
309            self.rtc.session.send_buffer_audio
310        } else {
311            self.rtc.session.send_buffer_video
312        };
313
314        stream.set_rtx_cache(size, DEFAULT_RTX_CACHE_DURATION, DEFAULT_RTX_RATIO_CAP);
315
316        stream
317    }
318
319    /// Remove the transmit stream for the given SSRC.
320    ///
321    /// Returns true if stream existed and was removed.
322    pub fn remove_stream_tx(&mut self, ssrc: Ssrc) -> bool {
323        self.rtc.session.streams.remove_stream_tx(ssrc)
324    }
325
326    /// Obtain a send stream to write RTP data directly.
327    ///
328    /// The stream must first be declared using [`DirectApi::declare_stream_tx`].
329    pub fn stream_tx(&mut self, ssrc: &Ssrc) -> Option<&mut StreamTx> {
330        self.rtc.session.streams.stream_tx(ssrc)
331    }
332
333    /// Obtain a send stream by looking it up via mid/rid.
334    pub fn stream_tx_by_mid(&mut self, mid: Mid, rid: Option<Rid>) -> Option<&mut StreamTx> {
335        let midrid = MidRid(mid, rid);
336        self.rtc.session.streams.stream_tx_by_midrid(midrid)
337    }
338
339    /// Reset a transmit stream to use a new SSRC and optionally a new RTX SSRC.
340    ///
341    /// This changes the SSRC of an existing stream and resets all relevant state.
342    /// Use this when you need to change the SSRC of an existing stream without creating a new one.
343    ///
344    /// If the stream has an RTX SSRC, `new_rtx` must be provided. If the stream doesn't
345    /// have an RTX SSRC, `new_rtx` is ignored.
346    ///
347    /// Returns a reference to the updated stream or None if:
348    /// - No stream was found for the given mid/rid
349    /// - The new SSRC is the same as the current one (no change needed)
350    /// - The new RTX SSRC is the same as the current one (no change needed)
351    pub fn reset_stream_tx(
352        &mut self,
353        mid: Mid,
354        rid: Option<Rid>,
355        new_ssrc: Ssrc,
356        new_rtx: Option<Ssrc>,
357    ) -> Option<&mut StreamTx> {
358        let midrid = MidRid(mid, rid);
359
360        // Find the stream by mid/rid
361        let stream = self.rtc.session.streams.stream_tx_by_midrid(midrid)?;
362
363        // Don't change to the same SSRC
364        if stream.ssrc() == new_ssrc {
365            return None;
366        }
367
368        // If the stream has an RTX SSRC, New RTX must be provided and differ.
369        // But it is allowed to start or turn off RTX.
370        if stream.rtx().is_some() && stream.rtx() == new_rtx {
371            return None;
372        }
373
374        // Reset the stream with the new SSRC and RTX
375        stream.reset_ssrc(new_ssrc, new_rtx);
376
377        // Return a reference to the updated stream
378        Some(stream)
379    }
380}