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}