reactor_webrtc/peer_connection.rs
1//! The peer connection and its associated signaling/data types.
2
3use std::ffi::{c_void, CStr, CString};
4use std::os::raw::{c_char, c_int};
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::mpsc::{sync_channel, SyncSender};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use reactor_webrtc_sys::ReactorStatEntry;
11
12use crate::chunking_times::{ChunkingTimes, FrameSteps};
13use crate::dc_chunking::ApplyError;
14use crate::encoded::{FrameTransform, VideoCodec};
15use crate::media::{MediaKind, Track};
16use crate::observer::ObserverState;
17use crate::{Error, FactoryHandle, Result};
18
19/// How long to wait for an async native op (create offer/answer, set
20/// description, add ICE candidate) to complete before giving up.
21const OP_TIMEOUT: Duration = Duration::from_secs(10);
22
23/// SDP description kind.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SdpType {
26 Offer,
27 PrAnswer,
28 Answer,
29 Rollback,
30}
31
32impl SdpType {
33 fn as_str(self) -> &'static str {
34 match self {
35 SdpType::Offer => "offer",
36 SdpType::PrAnswer => "pranswer",
37 SdpType::Answer => "answer",
38 SdpType::Rollback => "rollback",
39 }
40 }
41 fn from_str(s: &str) -> Option<Self> {
42 match s {
43 "offer" => Some(SdpType::Offer),
44 "pranswer" => Some(SdpType::PrAnswer),
45 "answer" => Some(SdpType::Answer),
46 "rollback" => Some(SdpType::Rollback),
47 _ => None,
48 }
49 }
50}
51
52/// A session description (offer/answer).
53#[derive(Debug, Clone)]
54pub struct SessionDescription {
55 pub kind: SdpType,
56 pub sdp: String,
57}
58
59/// RFC 8445 §5.3: an ice-ufrag is 4..=256 characters.
60const ICE_UFRAG_LEN: std::ops::RangeInclusive<usize> = 4..=256;
61/// RFC 8445 §5.3: an ice-pwd is 22..=256 characters.
62const ICE_PWD_LEN: std::ops::RangeInclusive<usize> = 22..=256;
63
64/// RFC 8445 `ice-char = ALPHA / DIGIT / "+" / "/"`.
65fn is_ice_char(c: char) -> bool {
66 c.is_ascii_alphanumeric() || c == '+' || c == '/'
67}
68
69fn check_ice_value(
70 what: &str,
71 value: &str,
72 len: std::ops::RangeInclusive<usize>,
73) -> crate::Result<()> {
74 if !len.contains(&value.len()) {
75 return Err(crate::Error::Webrtc(format!(
76 "{what} must be {}..={} characters, got {}",
77 len.start(),
78 len.end(),
79 value.len()
80 )));
81 }
82 if let Some(bad) = value.chars().find(|c| !is_ice_char(*c)) {
83 return Err(crate::Error::Webrtc(format!(
84 "{what} contains {bad:?}, which is not an RFC 8445 ice-char"
85 )));
86 }
87 Ok(())
88}
89
90impl SessionDescription {
91 /// The `ice-ufrag` values this description carries, in document order.
92 ///
93 /// One per m-section. Bundled sections repeat the same value; a non-BUNDLE
94 /// description has a distinct ufrag per transport.
95 pub fn ice_ufrags(&self) -> Vec<&str> {
96 self.sdp
97 .lines()
98 .filter_map(|l| l.strip_prefix("a=ice-ufrag:").map(str::trim_end))
99 .collect()
100 }
101
102 /// Return a copy with every `ice-ufrag` and `ice-pwd` replaced.
103 ///
104 /// # Why this exists
105 ///
106 /// libwebrtc generates ICE credentials itself and exposes no setter — the
107 /// `SetIceParameters` entry point lives on `IceTransportInternal`, below the
108 /// public API, and calling it out of band would desync the transport from the
109 /// description that was signalled. An application that needs to *choose* its
110 /// ufrag — routing through an edge relay that demultiplexes on it, for
111 /// instance — has to do it here instead.
112 ///
113 /// It works because the local description is the source of truth:
114 /// `JsepTransport::SetLocalJsepTransportDescription` reads `IceParameters`
115 /// straight out of the description's transport description, and the only guard
116 /// libwebrtc applies to a local description checks that credentials are
117 /// *present*, not that it generated them. `tests/ice_credentials.rs` verifies
118 /// this by observation rather than by reading: a loopback whose offerer has
119 /// substituted credentials connects, which it could not if the transport had
120 /// kept its own.
121 ///
122 /// # Ordering
123 ///
124 /// Call this on the description returned by `create_offer`/`create_answer` and
125 /// before [`PeerConnection::set_local_description`]. Setting the local
126 /// description is what creates the transport and starts gathering, so
127 /// substituting afterwards has nothing to act on.
128 ///
129 /// ```no_run
130 /// # use reactor_webrtc::PeerConnection;
131 /// # fn f(pc: &PeerConnection) -> reactor_webrtc::Result<()> {
132 /// let answer = pc.create_answer()?;
133 /// let answer = answer.with_ice_credentials("MyRelayIssuedUfrag00", "aPasswordOfAtLeast22Chars")?;
134 /// pc.set_local_description(&answer)?;
135 /// # Ok(()) }
136 /// ```
137 ///
138 /// # Renegotiation
139 ///
140 /// Changing `ice-ufrag`/`ice-pwd` between generations *is* an ICE restart
141 /// (RFC 8445 §9): the transport discards its checklist and revalidates. So on a
142 /// renegotiation that is not meant to restart ICE, re-apply the **same** values
143 /// the session already uses. Substituting a fresh pair out of habit — rotating
144 /// a routing token, say — restarts connectivity checks and can interrupt media.
145 ///
146 /// A renegotiation-time description also differs from a first one in carrying
147 /// the candidates gathered so far. Those are left untouched, which is correct
148 /// for this build: it emits `a=candidate` lines without the optional trailing
149 /// `ufrag` token, so no candidate-level value can fall out of step with the
150 /// substituted media-level one. `tests/ice_credentials.rs` pins that, since it
151 /// is an upstream behaviour rather than a guarantee.
152 ///
153 /// # Errors
154 ///
155 /// If either value is outside RFC 8445's length range or contains a character
156 /// outside `ice-char` (`ALPHA / DIGIT / "+" / "/"`). Rejecting here rather
157 /// than at `set_local_description` keeps the failure attributable: libwebrtc
158 /// reports the same problem as a generic invalid-parameters error much later.
159 ///
160 /// Note that RFC 8839 §5.4 asks a *sender* to keep the ufrag to 32 characters
161 /// even though a receiver must accept 256. That is not enforced here, because
162 /// the range libwebrtc itself accepts is the one that governs interoperation,
163 /// but staying inside 32 is the safer choice.
164 pub fn with_ice_credentials(&self, ufrag: &str, pwd: &str) -> crate::Result<Self> {
165 check_ice_value("ice-ufrag", ufrag, ICE_UFRAG_LEN)?;
166 check_ice_value("ice-pwd", pwd, ICE_PWD_LEN)?;
167
168 if self.ice_ufrags().is_empty() {
169 return Err(crate::Error::Webrtc(
170 "session description carries no ice-ufrag to replace".into(),
171 ));
172 }
173
174 // Every occurrence, not the first: bundled m-sections each carry the
175 // attribute and they must agree, so replacing one would leave an SDP that
176 // is inconsistent rather than substituted.
177 let mut out = String::with_capacity(self.sdp.len() + 64);
178 for line in self.sdp.lines() {
179 if line.starts_with("a=ice-ufrag:") {
180 out.push_str("a=ice-ufrag:");
181 out.push_str(ufrag);
182 } else if line.starts_with("a=ice-pwd:") {
183 out.push_str("a=ice-pwd:");
184 out.push_str(pwd);
185 } else {
186 out.push_str(line);
187 }
188 // SDP lines are CRLF-terminated (RFC 4566 §5); `lines` has already
189 // stripped whatever the input used.
190 out.push_str("\r\n");
191 }
192
193 Ok(Self {
194 kind: self.kind,
195 sdp: out,
196 })
197 }
198
199 /// Whether this description declares frame-metadata support.
200 ///
201 /// True when it carries a session-level
202 /// `a=x-reactor-frame-metadata:<version>` whose version this build understands
203 /// ([`FRAME_METADATA_VERSION`](crate::metadata::FRAME_METADATA_VERSION)). A peer
204 /// speaking a different trailer format therefore reads as unsupported rather
205 /// than as a partial match.
206 ///
207 /// Read off the SDP string, not from libwebrtc: it drops `a=` lines it does not
208 /// recognise when parsing, so the parsed description never carries this.
209 ///
210 /// This is what [`PeerConnection::set_remote_description`] arms the connection's
211 /// [`FrameMetadataGate`](crate::FrameMetadataGate) from.
212 pub fn declares_frame_metadata(&self) -> bool {
213 let prefix = format!("a={}:", crate::metadata::FRAME_METADATA_ATTRIBUTE);
214 self.sdp.lines().any(|line| {
215 line.strip_prefix(prefix.as_str())
216 .and_then(|v| v.trim_end().parse::<u32>().ok())
217 .is_some_and(|v| v == crate::metadata::FRAME_METADATA_VERSION)
218 })
219 }
220
221 /// Return a copy declaring frame-metadata support, as a session-level attribute.
222 ///
223 /// [`create_offer`](PeerConnection::create_offer) already applies this to every
224 /// offer, and [`create_answer`](PeerConnection::create_answer) mirrors the offer
225 /// — so a caller using this crate's signalling path never needs it. It is public
226 /// for callers that assemble or rewrite descriptions themselves.
227 ///
228 /// Idempotent: a description that already declares the capability comes back
229 /// unchanged, as does one with no lines at all.
230 ///
231 /// # Why a bespoke attribute
232 ///
233 /// The declaration says only "this peer understands the trailer". An
234 /// `a=extmap` would have been the recognisable spelling, but it means "I will
235 /// send this RTP header extension", which is not true — no header extension is
236 /// ever emitted — and it would drag in a shared id namespace that the *peer*
237 /// validates (RFC 8843 requires one id to mean one URI across a BUNDLE group),
238 /// so a collision would surface as the far side's `set_remote_description`
239 /// failing. An unregistered `x-` attribute claims nothing false and has no id
240 /// to collide.
241 ///
242 /// The cost is that libwebrtc discards it while parsing, so it is only ever
243 /// readable from the SDP string. Nothing here depends on the parsed form.
244 ///
245 /// # Placement
246 ///
247 /// Inserted immediately before the first `m=` line, which is the end of the
248 /// session section: RFC 8866 §5 puts session-level attributes after `t=`/`z=`/`k=`
249 /// and before the first media description, and everything preceding the first
250 /// `m=` is by definition session level.
251 /// Whether this description declares data-channel chunking at a version
252 /// this build speaks (`a=x-reactor-dc-chunking:1 …`). Read off the SDP
253 /// string, like [`declares_frame_metadata`](Self::declares_frame_metadata).
254 pub fn declares_dc_chunking(&self) -> bool {
255 reactor_webrtc_dc_chunking::sdp::parse(&self.sdp).is_some()
256 }
257
258 /// Return a copy declaring data-channel chunking with `params`, at session
259 /// level. Idempotent. [`PeerConnection::create_offer`] and
260 /// [`create_answer`](PeerConnection::create_answer) already apply it when
261 /// the connection takes part, so callers using this crate's signalling
262 /// never need it.
263 pub fn with_dc_chunking(&self, params: &crate::DcChunkingParams) -> Self {
264 Self {
265 kind: self.kind,
266 sdp: reactor_webrtc_dc_chunking::sdp::declare(&self.sdp, params),
267 }
268 }
269
270 pub fn with_frame_metadata(&self) -> Self {
271 if self.declares_frame_metadata() || self.sdp.lines().next().is_none() {
272 return self.clone();
273 }
274 let declaration = format!(
275 "a={}:{}\r\n",
276 crate::metadata::FRAME_METADATA_ATTRIBUTE,
277 crate::metadata::FRAME_METADATA_VERSION
278 );
279 let mut out = String::with_capacity(self.sdp.len() + declaration.len());
280 let mut inserted = false;
281 for line in self.sdp.lines() {
282 if !inserted && line.starts_with("m=") {
283 out.push_str(&declaration);
284 inserted = true;
285 }
286 out.push_str(line);
287 out.push_str("\r\n");
288 }
289 // A description with no media section at all: session level is still session
290 // level, so it goes at the end.
291 if !inserted {
292 out.push_str(&declaration);
293 }
294 Self {
295 kind: self.kind,
296 sdp: out,
297 }
298 }
299}
300
301/// A trickled ICE candidate.
302///
303/// An empty [`IceCandidate::candidate`] string is the end-of-candidates
304/// marker (RFC 8838), not a candidate to parse; `sdp_mid` and
305/// `sdp_mline_index` still identify the m-line it ends.
306#[derive(Debug, Clone)]
307pub struct IceCandidate {
308 pub candidate: String,
309 pub sdp_mid: Option<String>,
310 pub sdp_mline_index: Option<u16>,
311}
312
313/// Direction of a transceiver / track.
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum TransceiverDirection {
316 SendRecv,
317 SendOnly,
318 RecvOnly,
319 Inactive,
320}
321
322impl TransceiverDirection {
323 fn to_raw(self) -> c_int {
324 match self {
325 TransceiverDirection::SendRecv => 0,
326 TransceiverDirection::SendOnly => 1,
327 TransceiverDirection::RecvOnly => 2,
328 TransceiverDirection::Inactive => 3,
329 }
330 }
331}
332
333/// ICE gathering state (delivered to
334/// [`PeerConnectionObserver::on_ice_gathering_change`](crate::PeerConnectionObserver)).
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum IceGatheringState {
337 New,
338 Gathering,
339 Complete,
340}
341
342impl IceGatheringState {
343 pub(crate) fn from_raw(state: c_int) -> Self {
344 match state {
345 1 => IceGatheringState::Gathering,
346 2 => IceGatheringState::Complete,
347 _ => IceGatheringState::New,
348 }
349 }
350}
351
352/// A transceiver: one bidirectional media "slot" in the peer connection. Its
353/// `mid` (available after `set_local_description`) maps it to an SDP m-section.
354pub struct Transceiver {
355 raw: *mut reactor_webrtc_sys::RtpTransceiver,
356 pc_id: usize,
357 // Shared with the owning PeerConnection: what negotiation concluded about
358 // frame metadata. Consulted by set_track when replacing a disallowed
359 // track with an allowed one.
360 frame_metadata_gate: crate::FrameMetadataGate,
361}
362
363// SAFETY: the native transceiver is internally thread-safe.
364unsafe impl Send for Transceiver {}
365unsafe impl Sync for Transceiver {}
366
367impl Transceiver {
368 pub(crate) fn from_raw(
369 raw: *mut reactor_webrtc_sys::RtpTransceiver,
370 pc_id: usize,
371 gate: crate::FrameMetadataGate,
372 ) -> Self {
373 Self {
374 raw,
375 pc_id,
376 frame_metadata_gate: gate,
377 }
378 }
379
380 /// The transceiver's media kind (audio/video).
381 /// Identity of the transceiver itself, as an opaque value.
382 ///
383 /// Stable for the transceiver's life, unlike the handle pointer — `transceivers()`
384 /// allocates a fresh handle each call. Usable as a key before a track is attached
385 /// and before a mid is assigned.
386 pub(crate) fn transceiver_id(&self) -> usize {
387 unsafe { reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_id(self.raw) }
388 }
389
390 /// Identity of the track on this transceiver's sender, as an opaque value.
391 ///
392 /// Only ever compared — it is how the crate recognises which of its own
393 /// [`Track`](crate::Track)s a transceiver is sending, so that state living in
394 /// that track can be found from here. 0 when the sender has no track.
395 pub(crate) fn sender_track_id(&self) -> usize {
396 unsafe { reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_sender_track_id(self.raw) }
397 }
398
399 /// Identity of the track this transceiver's receiver delivers, on the same
400 /// terms as [`sender_track_id`](Self::sender_track_id). Non-zero once the
401 /// remote description has been applied.
402 pub(crate) fn receiver_track_id(&self) -> usize {
403 unsafe { reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_receiver_track_id(self.raw) }
404 }
405
406 pub fn kind(&self) -> MediaKind {
407 let k = unsafe { reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_media_kind(self.raw) };
408 MediaKind::from_raw(k)
409 }
410
411 /// The transceiver's mid, once assigned (after `set_local_description`).
412 pub fn mid(&self) -> Option<String> {
413 let mut buf = [0u8; 256];
414 let n = unsafe {
415 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_mid(
416 self.raw,
417 buf.as_mut_ptr() as *mut c_char,
418 buf.len() as c_int,
419 )
420 };
421 if n < 0 {
422 None
423 } else {
424 Some(
425 unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
426 .to_string_lossy()
427 .into_owned(),
428 )
429 }
430 }
431
432 /// Attach a local track to this transceiver's sender (for sendonly/sendrecv).
433 ///
434 /// The track is published in the same MediaStream as every other track this
435 /// peer sends, the way [`PeerConnection::add_track`] publishes one. The
436 /// remote groups the streams it receives by that id, so an audio track and a
437 /// video track published here play out in sync with each other.
438 pub fn set_track(&self, track: &Track) -> Result<()> {
439 let ok = unsafe {
440 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_track(self.raw, track.raw())
441 };
442 if ok == 1 {
443 let id = self.transceiver_id();
444 let sender_id = self.sender_track_id();
445 // Re-wire the embed source when the gate is already open (replaceTrack
446 // post-negotiation). Without this the old track's source stays in the
447 // slot and pushes to the new track are silently dropped until the next
448 // renegotiation re-runs install_frame_metadata_transforms.
449 if let Some(source) = crate::sender_meta::lookup(sender_id) {
450 crate::sender_meta::update_embed_source(self.pc_id, id, source);
451 }
452 // Fresh embed only when both the negotiated gate is open and the new
453 // track is allowed to carry metadata — the mirror for, e.g., replacing
454 // a track created with frame_metadata off by one with it on. If the slot
455 // already runs an embed step this is a no-op rather than a second
456 // transformer.
457 if !self.frame_metadata_gate.is_open() || !crate::sender_meta::allowed(sender_id) {
458 } else if let Some(source) = crate::sender_meta::lookup(sender_id) {
459 if let Some(native) = crate::sender_meta::attach_embed(
460 self.pc_id,
461 id,
462 source,
463 self.frame_metadata_gate.clone(),
464 ) {
465 if self
466 .attach_native_transform(crate::sender_meta::Side::Send, &native)
467 .is_err()
468 {
469 crate::sender_meta::release_install(
470 self.pc_id,
471 id,
472 crate::sender_meta::Side::Send,
473 );
474 }
475 }
476 }
477 Ok(())
478 } else {
479 Err(Error::Webrtc("transceiver set_track failed".into()))
480 }
481 }
482
483 /// Set the transceiver's direction — controls what appears in the next
484 /// `create_answer()` / `create_offer()` for this m-section.
485 pub fn set_direction(&self, direction: TransceiverDirection) -> Result<()> {
486 let ok = unsafe {
487 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_direction(
488 self.raw,
489 direction.to_raw() as c_int,
490 )
491 };
492 if ok == 1 {
493 Ok(())
494 } else {
495 Err(Error::Webrtc("transceiver set_direction failed".into()))
496 }
497 }
498
499 /// Hold this transceiver's received media in the jitter buffer for at least
500 /// `delay` (`RTCRtpReceiver.jitterBufferTarget`); `None` restores the
501 /// default. The buffer still adds whatever the network's jitter calls for
502 /// on top — this is a floor, so it can only add latency, never remove it.
503 /// To cut playout latency instead, see
504 /// [`with_receive_playout_delay`](crate::PeerConnectionFactoryBuilder::with_receive_playout_delay).
505 ///
506 /// Takes effect immediately on a running stream, and is remembered when set
507 /// before one exists — on a transceiver of your own before you offer, or in
508 /// `on_track` — so it
509 /// applies from the first frame. Read the result back as
510 /// [`InboundRtpStats::jitter_buffer_target_delay_s`]. Fails when `delay`
511 /// is above libwebrtc's 10 s limit.
512 pub fn set_jitter_buffer_minimum_delay(
513 &self,
514 delay: Option<std::time::Duration>,
515 ) -> Result<()> {
516 const LIMIT: std::time::Duration = std::time::Duration::from_secs(10);
517 if delay.is_some_and(|d| d > LIMIT) {
518 return Err(Error::Webrtc(format!(
519 "jitter buffer minimum delay {delay:?} is above libwebrtc's {LIMIT:?}"
520 )));
521 }
522 let ok = unsafe {
523 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_jitter_buffer_minimum_delay(
524 self.raw,
525 delay.is_some() as c_int,
526 delay.map_or(0.0, |d| d.as_secs_f64()),
527 )
528 };
529 if ok == 1 {
530 Ok(())
531 } else {
532 Err(Error::Webrtc(
533 "transceiver set_jitter_buffer_minimum_delay failed".into(),
534 ))
535 }
536 }
537
538 /// Reorder this video transceiver's codec preferences: `codecs`, most
539 /// preferred first, sort ahead of every other codec the endpoint
540 /// supports. Mirrors [`RTCRtpTransceiver.setCodecPreferences`](
541 /// https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiver-setcodecpreferences),
542 /// plus one behavior the browser API does not need: once negotiation
543 /// completes, [`PeerConnection::set_local_description`] and
544 /// [`PeerConnection::set_remote_description`] also make this
545 /// transceiver's own sender actually *encode* with whichever preferred
546 /// codec was negotiated, not just list it first in the SDP. Without
547 /// that, a fresh sender follows the remote offer's own codec order
548 /// regardless of what got negotiated — libwebrtc's SDP negotiation and
549 /// its sender codec selection are two separate mechanisms, and only the
550 /// first one is driven by preference order. See
551 /// [`try_lock_negotiated_send_codec`](Self::try_lock_negotiated_send_codec).
552 ///
553 /// Nothing is dropped: a codec left out of `codecs`, and every
554 /// retransmission/RED/FEC entry, keeps its original relative order after
555 /// the preferred ones — retransmission stays associated with its codec,
556 /// and the peer that doesn't support a preferred codec still gets an
557 /// offer/answer it can negotiate against. A codec named in `codecs` that
558 /// this endpoint does not actually support is silently ignored rather
559 /// than treated as an error.
560 ///
561 /// Takes effect on the next [`PeerConnection::create_offer`] or
562 /// [`PeerConnection::create_answer`] for this transceiver's m-section —
563 /// call it before negotiating. Returns an error if this transceiver
564 /// carries audio, not video.
565 pub fn set_codec_preferences(&self, codecs: &[VideoCodec]) -> Result<()> {
566 let names: Vec<CString> = codecs
567 .iter()
568 .map(|c| CString::new(c.name()).expect("codec name is a static ASCII string"))
569 .collect();
570 let ptrs: Vec<*const c_char> = names.iter().map(|n| n.as_ptr()).collect();
571 let ok = unsafe {
572 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_video_codec_preferences(
573 self.raw,
574 ptrs.as_ptr(),
575 ptrs.len() as c_int,
576 )
577 };
578 if ok == 1 {
579 Ok(())
580 } else {
581 Err(Error::Webrtc(
582 "transceiver set_codec_preferences failed (not a video transceiver?)".into(),
583 ))
584 }
585 }
586
587 /// Best-effort counterpart to [`set_codec_preferences`](Self::set_codec_preferences):
588 /// make this transceiver's sender actually encode with the codec
589 /// `set_codec_preferences` put first, instead of whatever it would
590 /// otherwise pick (e.g. the remote offer's own codec order).
591 /// `set_codec_preferences` only controls SDP negotiation; it does not by
592 /// itself change which negotiated codec an existing sender encodes
593 /// with — that is libwebrtc's separate "codec switching" mechanism.
594 ///
595 /// Not public: [`PeerConnection::set_local_description`] and
596 /// [`PeerConnection::set_remote_description`] call this on every video
597 /// transceiver after applying the description, so callers only ever
598 /// need `set_codec_preferences`. Returns `false` rather than erroring
599 /// when there is nothing to do yet — no preference was set, there is no
600 /// sender, or negotiation has not completed on this side yet — since
601 /// whichever of the two description calls comes second on either role
602 /// (offerer or answerer) is the one that finds a completed negotiation.
603 pub(crate) fn try_lock_negotiated_send_codec(&self) -> bool {
604 let ok = unsafe {
605 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_lock_negotiated_send_codec(self.raw)
606 };
607 ok == 1
608 }
609
610 /// Set this transceiver's **per-sender** bitrate bounds — the ceiling that
611 /// actually caps the video encoder.
612 ///
613 /// This is a different knob from [`PeerConnection::set_bitrate`], and the
614 /// two are conjunctive: the lower one wins.
615 ///
616 /// - [`PeerConnection::set_bitrate`] bounds the *aggregate* congestion-control
617 /// estimate for the whole connection — how much bandwidth the GCC algorithm
618 /// believes it may allocate.
619 /// - This method bounds *this one stream's* share of that allocation.
620 ///
621 /// Without this call, the stream's ceiling is libwebrtc's resolution-keyed
622 /// default: 600 kbps up to 320x240, 1700 up to 640x480, 2000 up to 960x540,
623 /// and **2500 kbps for everything above that**. So 720p, 1080p and 4K all cap
624 /// at 2.5 Mbps no matter how high the congestion-control ceiling is raised —
625 /// setting `max_bps` here is the only way to lift it.
626 ///
627 /// Both bounds are optional; pass `None` to leave one at the libwebrtc
628 /// default. Values are in bits per second, and apply to the first encoding
629 /// (the single-stream case — simulcast layers are not addressed individually).
630 ///
631 /// **When the sender has encodings to write depends on where the transceiver
632 /// came from**, and it is the one sharp edge here:
633 ///
634 /// | Transceiver from | audio | video |
635 /// |---|---|---|
636 /// | [`PeerConnection::add_transceiver`] | has encodings | has encodings |
637 /// | applying a remote description | **none until the local description is applied** | has encodings |
638 ///
639 /// `add_transceiver` seeds a default encoding; one materialised from a
640 /// remote offer does not get one for audio until the answer is set locally.
641 /// Called before that, this returns "sender has no encodings".
642 ///
643 /// Only the *default* being lifted is video-specific — it is keyed on frame
644 /// size. The bounds themselves apply to an audio sender too, capping its
645 /// allocation. So an answerer that only wants to clear that default can
646 /// bound its video senders while building the answer, and one that also
647 /// wants an audio bound applies it after
648 /// [`PeerConnection::set_local_description`].
649 ///
650 /// Can be called again at any point to change the bounds mid-call.
651 ///
652 /// # Errors
653 ///
654 /// Returns [`Error::Webrtc`] when either bound is negative — `None`, not a
655 /// negative number, is how a bound is left unset — when `min_bps` exceeds
656 /// `max_bps`, when the transceiver has no sender or no encodings, or when
657 /// libwebrtc rejects the parameters.
658 pub fn set_send_bitrate(&self, min_bps: Option<i32>, max_bps: Option<i32>) -> Result<()> {
659 // `None` is how a caller says "leave this at the libwebrtc default", and
660 // it crosses the ABI as -1. A negative `Some` is therefore ambiguous with
661 // that sentinel, and resolving it as "unset" would take a typo — or an
662 // arithmetic slip like `budget - overhead` going below zero — and quietly
663 // remove a cap the caller had set, reporting success. Refuse it here, so
664 // it never reaches the boundary where the two become indistinguishable.
665 for (label, value) in [("min_bps", min_bps), ("max_bps", max_bps)] {
666 if let Some(v) = value {
667 if v < 0 {
668 return Err(Error::Webrtc(format!(
669 "{label} must be >= 0; pass None to leave it at the libwebrtc default \
670 (got {v})"
671 )));
672 }
673 }
674 }
675
676 let mut err = [0 as std::os::raw::c_char; 256];
677 let rc = unsafe {
678 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_send_bitrate(
679 self.raw,
680 min_bps.unwrap_or(-1),
681 max_bps.unwrap_or(-1),
682 err.as_mut_ptr(),
683 err.len() as std::os::raw::c_int,
684 )
685 };
686 if rc != 0 {
687 let reason = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
688 .to_string_lossy()
689 .into_owned();
690 return Err(Error::Webrtc(if reason.is_empty() {
691 "transceiver set_send_bitrate failed".into()
692 } else {
693 reason
694 }));
695 }
696 Ok(())
697 }
698
699 /// Attach an encoded-frame transform to this transceiver's **sender**
700 /// (encoder → packetizer): observe/replace/drop each encoded frame before
701 /// it is sent. See [`crate::FrameTransform`].
702 ///
703 /// Composes rather than replaces. The crate owns libwebrtc's single
704 /// `SetFrameTransformer` slot per sender and runs both this callback and the
705 /// frame-metadata step under it, so encoded-frame access and per-frame metadata
706 /// work on the same transceiver. The callback runs first, before any trailer is
707 /// appended, so it sees exactly the bytes the encoder produced.
708 ///
709 /// Calling this again replaces the callback. The `FrameTransform` may be dropped
710 /// afterwards — the registration holds its own reference.
711 pub fn set_sender_transform(&self, transform: &FrameTransform) -> Result<()> {
712 self.attach_caller_transform(crate::sender_meta::Side::Send, transform)
713 }
714
715 /// Attach an encoded-frame transform to this transceiver's **receiver**
716 /// (depacketizer → decoder): observe each encoded frame before decode, and
717 /// [`FrameAction::Drop`](crate::FrameAction) to bypass the decoder. See
718 /// [`crate::FrameTransform`].
719 ///
720 /// Composes rather than replaces, as on the sender. The callback runs before the
721 /// metadata trailer is stripped, so it sees exactly the bytes that arrived; call
722 /// [`decode_and_strip_trailer`](crate::metadata::decode_and_strip_trailer)
723 /// yourself if you want the payload without the framing.
724 pub fn set_receiver_transform(&self, transform: &FrameTransform) -> Result<()> {
725 self.attach_caller_transform(crate::sender_meta::Side::Receive, transform)
726 }
727
728 fn attach_caller_transform(
729 &self,
730 side: crate::sender_meta::Side,
731 transform: &FrameTransform,
732 ) -> Result<()> {
733 let id = self.transceiver_id();
734 let Some(native) =
735 crate::sender_meta::attach_caller(self.pc_id, id, side, transform.callback())
736 else {
737 // Either the transformer is already attached — the registration above is
738 // all that was needed — or this transceiver has no native identity, in
739 // which case there is nothing to attach it to.
740 return if id == 0 {
741 Err(Error::Webrtc(
742 "transceiver has no native identity to attach a transform to".into(),
743 ))
744 } else {
745 Ok(())
746 };
747 };
748 let result = self.attach_native_transform(side, &native);
749 if result.is_err() {
750 // The slot was claimed but the native attach failed: un-claim it so
751 // a retry can install a new transformer rather than seeing installed=true
752 // and silently doing nothing.
753 crate::sender_meta::release_install(self.pc_id, id, side);
754 }
755 result
756 }
757
758 /// Attach the crate-owned composed transformer to one side.
759 ///
760 /// Dropping `native` afterwards is safe and is what the callers do: the native
761 /// transformer owns its callback state and the sender/receiver holds a reference
762 /// to it (see the [`crate::FrameTransform`] docs).
763 pub(crate) fn attach_native_transform(
764 &self,
765 side: crate::sender_meta::Side,
766 native: &crate::encoded::NativeTransform,
767 ) -> Result<()> {
768 let ok = unsafe {
769 match side {
770 crate::sender_meta::Side::Send => {
771 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_sender_transform(
772 self.raw,
773 native.raw(),
774 )
775 }
776 crate::sender_meta::Side::Receive => {
777 reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_set_receiver_transform(
778 self.raw,
779 native.raw(),
780 )
781 }
782 }
783 };
784 if ok == 1 {
785 Ok(())
786 } else {
787 Err(Error::Webrtc(format!(
788 "transceiver set_{}_transform failed",
789 match side {
790 crate::sender_meta::Side::Send => "sender",
791 crate::sender_meta::Side::Receive => "receiver",
792 }
793 )))
794 }
795 }
796}
797
798impl Drop for Transceiver {
799 fn drop(&mut self) {
800 // Deliberately *not* forgetting this transceiver's composed slots: handles
801 // are recreated per `transceivers()` call, so dropping one says nothing
802 // about the underlying transceiver going away. The slots are keyed by native
803 // identity and released with the peer connection instead.
804 unsafe { reactor_webrtc_sys::reactor_webrtc_rtp_transceiver_destroy(self.raw) }
805 }
806}
807
808/// Aggregate connection state.
809#[derive(Debug, Clone, Copy, PartialEq, Eq)]
810pub enum PeerConnectionState {
811 New,
812 Connecting,
813 Connected,
814 Disconnected,
815 Failed,
816 Closed,
817}
818
819impl PeerConnectionState {
820 pub(crate) fn from_raw(state: c_int) -> Self {
821 match state {
822 0 => PeerConnectionState::New,
823 1 => PeerConnectionState::Connecting,
824 2 => PeerConnectionState::Connected,
825 3 => PeerConnectionState::Disconnected,
826 4 => PeerConnectionState::Failed,
827 _ => PeerConnectionState::Closed,
828 }
829 }
830}
831
832/// Data channel readiness state.
833#[derive(Debug, Clone, Copy, PartialEq, Eq)]
834pub enum DataChannelState {
835 Connecting,
836 Open,
837 Closing,
838 Closed,
839}
840
841impl DataChannelState {
842 fn from_raw(v: c_int) -> Self {
843 match v {
844 0 => DataChannelState::Connecting,
845 1 => DataChannelState::Open,
846 2 => DataChannelState::Closing,
847 _ => DataChannelState::Closed,
848 }
849 }
850}
851
852// ── Stats types ──────────────────────────────────────────────────────────────
853
854/// State of an ICE candidate pair (`RTCIceCandidatePairStats::state`).
855#[derive(Debug, Clone, Copy, PartialEq, Eq)]
856pub enum IceCandidatePairState {
857 Waiting,
858 InProgress,
859 Failed,
860 Succeeded,
861 Cancelled,
862}
863
864impl IceCandidatePairState {
865 fn from_raw(v: c_int) -> Self {
866 match v {
867 1 => IceCandidatePairState::InProgress,
868 2 => IceCandidatePairState::Failed,
869 3 => IceCandidatePairState::Succeeded,
870 4 => IceCandidatePairState::Cancelled,
871 _ => IceCandidatePairState::Waiting,
872 }
873 }
874}
875
876/// Media kind of an RTP stream (`RTCRtpStreamStats::kind`).
877///
878/// The field that lets a reader tell which of several receive streams is the
879/// video one. Without it there is only the SSRC, and "the video stream's jitter"
880/// becomes "the worst jitter of anything arriving" — a different statistic.
881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
882pub enum StreamKind {
883 /// The engine did not report a kind.
884 Unknown,
885 Audio,
886 Video,
887}
888
889impl StreamKind {
890 fn from_raw(v: c_int) -> Self {
891 match v {
892 0 => StreamKind::Audio,
893 1 => StreamKind::Video,
894 _ => StreamKind::Unknown,
895 }
896 }
897}
898
899/// Type of an ICE candidate (`RTCIceCandidateStats::candidate_type`).
900///
901/// [`IceCandidateType::Relay`] means the media is going through a TURN server,
902/// which is the first thing worth knowing when latency is bad.
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904pub enum IceCandidateType {
905 /// The engine did not report one, or the pair named no local candidate.
906 Unknown,
907 /// A local interface address.
908 Host,
909 /// Server-reflexive: discovered through STUN.
910 Srflx,
911 /// Peer-reflexive: learned from an incoming connectivity check.
912 Prflx,
913 /// Relayed through TURN.
914 Relay,
915}
916
917impl IceCandidateType {
918 fn from_raw(v: c_int) -> Self {
919 match v {
920 0 => IceCandidateType::Host,
921 1 => IceCandidateType::Srflx,
922 2 => IceCandidateType::Prflx,
923 3 => IceCandidateType::Relay,
924 _ => IceCandidateType::Unknown,
925 }
926 }
927}
928
929/// Transport a relayed candidate uses to reach its TURN server
930/// (`RTCIceCandidateStats::relay_protocol`).
931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
932pub enum RelayProtocol {
933 /// Not relayed, or not reported. Distinct from a protocol: a `Host`
934 /// candidate has no relay to reach.
935 ///
936 /// Spelled out rather than `None` because the Python binding mirrors these
937 /// variant names, and `RelayProtocol.None` is not addressable from Python.
938 NotRelayed,
939 Udp,
940 Tcp,
941 Tls,
942}
943
944impl RelayProtocol {
945 fn from_raw(v: c_int) -> Self {
946 match v {
947 0 => RelayProtocol::Udp,
948 1 => RelayProtocol::Tcp,
949 2 => RelayProtocol::Tls,
950 _ => RelayProtocol::NotRelayed,
951 }
952 }
953}
954
955/// `RTCInboundRtpStreamStats` subset.
956#[derive(Debug, Clone)]
957pub struct InboundRtpStats {
958 pub ssrc: u32,
959 /// Audio or video. See [`StreamKind`].
960 pub kind: StreamKind,
961 /// The transceiver this stream belongs to (`RTCInboundRtpStreamStats::mid`).
962 /// Several tracks of one kind are told apart by this, not by
963 /// [`StreamKind`]: match it against [`Transceiver::mid`]. `None` before the
964 /// stream is negotiated.
965 pub mid: Option<String>,
966 /// The codec this stream carries, as its mime type (`"video/VP9"`,
967 /// `"audio/opus"`), from the `RTCCodecStats` its `codec_id` names. `None`
968 /// until the stream has a codec.
969 pub codec_mime_type: Option<String>,
970 pub packets_received: u32,
971 pub bytes_received: u64,
972 /// Jitter in seconds.
973 pub jitter_s: f64,
974 pub packets_lost: i32,
975 /// Retransmissions this endpoint asked the sender for.
976 ///
977 /// Repair traffic moves before loss does — a retransmission that arrives in
978 /// time hides the loss that prompted it — so this climbs while the stream
979 /// still plays. Read it as the earliest sign a receive path is going bad.
980 pub nack_count: u32,
981 /// Keyframe requests this endpoint sent because its decoder could not
982 /// continue. The step past [`InboundRtpStats::nack_count`]: a NACK asks for
983 /// one packet again, a Picture Loss Indication says repair has been outrun
984 /// and the stream has to restart from a fresh keyframe.
985 pub pli_count: u32,
986 /// Full Intra Refresh requests this endpoint sent. Serves the same purpose
987 /// as [`InboundRtpStats::pli_count`] and which one a decoder sends depends
988 /// on the codec, so a reader after "the stream had to be restarted" wants
989 /// the two together.
990 pub fir_count: u32,
991 /// Cumulative decode time in seconds.
992 pub total_decode_time_s: f64,
993 /// Decoded frames per second; `0.0` if not measured. Video only.
994 pub frames_per_second: f64,
995 pub frames_decoded: u32,
996 pub frames_dropped: u32,
997 /// Decoded frame size; `0` for audio, and before the first frame.
998 pub frame_width: u32,
999 pub frame_height: u32,
1000 /// Cumulative time, in seconds, that the frames counted by
1001 /// [`jitter_buffer_emitted_count`](Self::jitter_buffer_emitted_count) spent
1002 /// in the jitter buffer (`RTCInboundRtpStreamStats::jitterBufferDelay`). For
1003 /// video, from a frame's first packet arriving to the frame leaving for the
1004 /// decoder; divide by the count for the per-frame average.
1005 pub jitter_buffer_delay_s: f64,
1006 /// Cumulative target delay in seconds, over the same frames: what the
1007 /// jitter buffer was aiming for, with every floor in force — a
1008 /// [`Transceiver::set_jitter_buffer_minimum_delay`], a playout delay, or
1009 /// what A/V sync needed. A floor shows up here, less the receiver's ~10 ms
1010 /// render delay.
1011 pub jitter_buffer_target_delay_s: f64,
1012 /// Cumulative minimum delay in seconds, over the same frames. For video
1013 /// this is libwebrtc's own computed minimum — jitter estimate plus decode
1014 /// and render time — not a floor the app set; read
1015 /// [`jitter_buffer_target_delay_s`](Self::jitter_buffer_target_delay_s)
1016 /// for that.
1017 pub jitter_buffer_minimum_delay_s: f64,
1018 /// Frames that have left the jitter buffer — the denominator for the three
1019 /// cumulative delays above.
1020 pub jitter_buffer_emitted_count: u64,
1021 /// Cumulative time from a frame's first packet arriving to it being
1022 /// decoded, in seconds, over [`InboundRtpStats::frames_decoded`].
1023 pub total_processing_delay_s: f64,
1024 /// The slowest timing frame of the last second, or `None` if none arrived
1025 /// in it. Video only.
1026 pub timing_frame: Option<TimingFrameInfo>,
1027 /// The decoder libwebrtc uses for this stream, e.g. `"libvpx"` or
1028 /// `"VideoToolbox"`; `None` before the first frame is decoded and for
1029 /// audio. A name longer than 127 bytes is cut to them.
1030 pub decoder_implementation: Option<String>,
1031 /// Whether that decoder is the hardware one; `None` when not reported.
1032 pub power_efficient_decoder: Option<bool>,
1033}
1034
1035impl InboundRtpStats {
1036 /// Average time a frame spent in the jitter buffer, or `None` before the
1037 /// first one left it.
1038 pub fn average_jitter_buffer_delay(&self) -> Option<std::time::Duration> {
1039 (self.jitter_buffer_emitted_count > 0).then(|| {
1040 std::time::Duration::from_secs_f64(
1041 (self.jitter_buffer_delay_s / self.jitter_buffer_emitted_count as f64).max(0.0),
1042 )
1043 })
1044 }
1045}
1046
1047/// One frame libwebrtc stamped at each stage of its trip, as reported by the
1048/// receiver (`goog_timing_frame_info`).
1049///
1050/// By default the sender marks a frame every 200 ms, plus any frame at least
1051/// five times the average size, and carries its stamps in the `video-timing`
1052/// RTP header extension, on the last packet of the frame; the receiver adds
1053/// its own. It is the only per-frame view of the packetizer and the pacer. Of
1054/// the timing frames that arrived in the last second, libwebrtc reports the
1055/// one that took longest, so this is the worst recent frame, not a typical
1056/// one.
1057///
1058/// The timestamps come in two groups, by the side that took them. All of them
1059/// are on our clock: libwebrtc moves the sender's onto it once it has estimated
1060/// the offset between the two clocks, and until then reports them as negative
1061/// values that are still right relative to each other. So differences within
1062/// one group are times: `sender.encode_finish_ms - sender.encode_start_ms` is
1063/// encode time, `receiver.decode_start_ms - receiver.receive_finish_ms` is how
1064/// long the frame waited in the jitter buffer. A difference across the two
1065/// groups, such as `receiver.receive_start_ms - sender.pacer_exit_ms`, is only
1066/// an estimate, off by the error in the clock offset, and meaningless while
1067/// the sender stamps are negative.
1068///
1069/// The same frame can be reported by several consecutive reads; compare
1070/// [`TimingFrameInfo::rtp_timestamp`] to tell a new sample from a repeat.
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub struct TimingFrameInfo {
1073 pub rtp_timestamp: u32,
1074 /// The sender marked this frame for its size.
1075 pub is_outlier: bool,
1076 /// The sender marked this frame because the periodic timer was due.
1077 pub is_timer_triggered: bool,
1078 pub sender: TimingFrameSenderTimestamps,
1079 pub receiver: TimingFrameReceiverTimestamps,
1080}
1081
1082/// The timestamps the sender took for a [`TimingFrameInfo`].
1083#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1084pub struct TimingFrameSenderTimestamps {
1085 /// When the frame was captured (pushed into the track):
1086 /// `encode_start_ms - capture_ms` is how long it waited for the encoder.
1087 pub capture_ms: i64,
1088 pub encode_start_ms: i64,
1089 pub encode_finish_ms: i64,
1090 /// When the encoded frame had been cut into packets and handed to the pacer.
1091 pub packetization_finish_ms: i64,
1092 /// When the pacer sent the frame's last packet.
1093 pub pacer_exit_ms: i64,
1094}
1095
1096/// The timestamps the receiver (this side) took for a [`TimingFrameInfo`].
1097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1098pub struct TimingFrameReceiverTimestamps {
1099 pub receive_start_ms: i64,
1100 pub receive_finish_ms: i64,
1101 pub decode_start_ms: i64,
1102 pub decode_finish_ms: i64,
1103}
1104
1105/// `RTCOutboundRtpStreamStats` subset.
1106#[derive(Debug, Clone)]
1107pub struct OutboundRtpStats {
1108 pub ssrc: u32,
1109 /// Audio or video. See [`StreamKind`].
1110 pub kind: StreamKind,
1111 /// The transceiver this stream belongs to (`RTCOutboundRtpStreamStats::mid`).
1112 /// Several tracks of one kind are told apart by this, not by
1113 /// [`StreamKind`]: match it against [`Transceiver::mid`]. `None` before the
1114 /// stream is negotiated.
1115 pub mid: Option<String>,
1116 /// The codec this stream carries, as its mime type (`"video/VP9"`,
1117 /// `"audio/opus"`), from the `RTCCodecStats` its `codec_id` names. `None`
1118 /// until the stream has a codec.
1119 pub codec_mime_type: Option<String>,
1120 /// 64-bit because `RTCSentRtpStreamStats` reports it that way. It was `u32`
1121 /// until 0.15.0, which wrapped silently after ~4.3 billion packets — about
1122 /// seven weeks at a thousand packets a second — and then reported a
1123 /// cumulative counter that had gone backwards.
1124 pub packets_sent: u64,
1125 pub bytes_sent: u64,
1126 /// Target encoder bitrate in bps.
1127 pub target_bitrate_bps: f64,
1128 /// Round-trip time in seconds; `0.0` if not yet measured.
1129 ///
1130 /// From the receiver's RTCP report about us
1131 /// (`RTCRemoteInboundRtpStreamStats`), which is where libwebrtc moved it in
1132 /// M7907 — so it stays `0.0` until the far end has sent one, and a zero is
1133 /// "not measured yet" rather than a zero-latency link.
1134 pub round_trip_time_s: f64,
1135 /// Cumulative round-trip time in seconds, from the same report.
1136 pub total_round_trip_time_s: f64,
1137 /// Fraction of this stream the receiver reports as lost, `0.0`–`1.0`.
1138 pub fraction_lost: f64,
1139 /// Packets the receiver reports as lost. Signed, per RFC 3550.
1140 pub packets_lost: i32,
1141 /// 64-bit for the same reason as [`OutboundRtpStats::packets_sent`].
1142 pub retransmitted_packets_sent: u64,
1143 /// Retransmissions the receiver asked this endpoint for.
1144 ///
1145 /// The requests themselves, where
1146 /// [`OutboundRtpStats::retransmitted_packets_sent`] is what was sent in
1147 /// answer to them. The two differ when a request went unanswered, and they
1148 /// count different things — one is requests, the other packets. Repair
1149 /// traffic moves before loss does, so this is the earliest sign that the
1150 /// path to a viewer is going bad.
1151 pub nack_count: u32,
1152 /// Keyframe requests the receiver sent because its decoder could not
1153 /// continue. The step past [`OutboundRtpStats::nack_count`]: a NACK asks
1154 /// for one packet again, a Picture Loss Indication says repair has been
1155 /// outrun and the stream has to restart from a fresh keyframe. Answering
1156 /// one costs a keyframe, which is the bitrate spike a viewer sees as the
1157 /// picture snapping back.
1158 pub pli_count: u32,
1159 /// Full Intra Refresh requests the receiver sent. Serves the same purpose
1160 /// as [`OutboundRtpStats::pli_count`] and which one a decoder sends depends
1161 /// on the codec, so a reader after "the stream had to be restarted" wants
1162 /// the two together.
1163 pub fir_count: u32,
1164 /// Encoded frames per second; `0.0` if not measured. Video only.
1165 pub frames_per_second: f64,
1166 pub frames_sent: u32,
1167 /// Encoded frame size; `0` for audio, and before the first frame.
1168 pub frame_width: u32,
1169 pub frame_height: u32,
1170 pub frames_encoded: u32,
1171 /// Cumulative encode time in seconds, over
1172 /// [`OutboundRtpStats::frames_encoded`].
1173 pub total_encode_time_s: f64,
1174 /// Cumulative time packets waited in the pacer before being sent, in
1175 /// seconds. Summed over packets, not frames: divide by
1176 /// [`OutboundRtpStats::packets_sent`].
1177 pub total_packet_send_delay_s: f64,
1178 /// The encoder libwebrtc uses for this stream, e.g. `"libvpx"` or
1179 /// `"VideoToolbox"`; `None` before the first frame is encoded and for
1180 /// audio. A name longer than 127 bytes, such as a simulcast adapter's
1181 /// composed name, is cut to them.
1182 pub encoder_implementation: Option<String>,
1183 /// Whether that encoder is the hardware one; `None` when not reported.
1184 pub power_efficient_encoder: Option<bool>,
1185}
1186
1187/// `RTCIceCandidatePairStats` subset.
1188#[derive(Debug, Clone)]
1189pub struct IceCandidatePairStats {
1190 /// Current RTT in seconds; `0.0` if not yet measured.
1191 pub current_round_trip_time_s: f64,
1192 /// Cumulative RTT in seconds across every check on this pair.
1193 pub total_round_trip_time_s: f64,
1194 pub priority: u64,
1195 pub state: IceCandidatePairState,
1196 /// Whether ICE selected this pair. The one to read rather than inferring
1197 /// the selected pair from `state` and `priority`.
1198 pub nominated: bool,
1199 pub writable: bool,
1200 /// Congestion-control estimates in bps; `0.0` when the engine has none yet.
1201 pub available_outgoing_bitrate_bps: f64,
1202 pub available_incoming_bitrate_bps: f64,
1203 /// Everything this pair carried — RTCP and data channel included, so wider
1204 /// than the per-stream RTP counters.
1205 pub bytes_sent: u64,
1206 pub bytes_received: u64,
1207 pub packets_sent: u64,
1208 pub packets_received: u64,
1209 /// Type of this pair's *local* candidate. [`IceCandidateType::Relay`] is
1210 /// what says the session is going through TURN.
1211 pub local_candidate_type: IceCandidateType,
1212 /// Transport to the TURN server, when relayed.
1213 pub local_relay_protocol: RelayProtocol,
1214}
1215
1216/// One data channel's counters (`RTCDataChannelStats`).
1217///
1218/// A message is what the application sent or received in one call, however
1219/// many SCTP chunks carried it. On a chunked channel (see
1220/// [`crate::DcChunking`]), libwebrtc sees each frame as a message, so these
1221/// count frames there.
1222#[derive(Debug, Clone, PartialEq, Eq)]
1223pub struct DataChannelStats {
1224 /// The channel's label, which may be empty; `None` when absent or longer
1225 /// than 62 bytes.
1226 pub label: Option<String>,
1227 /// The SCTP stream id; `None` before it is assigned.
1228 pub id: Option<u16>,
1229 pub state: DataChannelState,
1230 pub messages_sent: u32,
1231 pub bytes_sent: u64,
1232 pub messages_received: u32,
1233 pub bytes_received: u64,
1234}
1235
1236/// The SCTP association that carries a connection's data channels, as dcsctp
1237/// tracks it.
1238///
1239/// Counters are cumulative for the association; the rest are its state when
1240/// read. Together they say why a data-channel message is slow: a small
1241/// congestion window, a long round trip, or loss and retransmission.
1242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1243pub struct SctpStats {
1244 pub packets_sent: u64,
1245 pub messages_sent: u64,
1246 /// Packets that carried a retransmitted chunk: an upper bound on the
1247 /// packets resent, since one packet can carry new and resent chunks.
1248 pub packets_retransmitted: u64,
1249 /// Retransmitted payload and chunk headers.
1250 pub bytes_retransmitted: u64,
1251 pub packets_received: u64,
1252 pub messages_received: u64,
1253 /// The congestion window: how much may be in flight before an
1254 /// acknowledgement.
1255 pub congestion_window_bytes: u64,
1256 /// Chunks in flight, not yet acknowledged.
1257 pub unacked_chunks: u64,
1258 /// The smoothed round trip time, in milliseconds.
1259 pub smoothed_rtt_ms: i32,
1260 /// The receive window the peer last announced.
1261 pub peer_receive_window_bytes: u32,
1262 /// Whether both ends negotiated message interleaving, which lets a small
1263 /// message go out between the chunks of a large one.
1264 pub message_interleaving: bool,
1265}
1266
1267/// A snapshot of the stats delivered by [`PeerConnection::get_stats`].
1268#[derive(Debug, Clone, Default)]
1269pub struct StatsReport {
1270 pub inbound_rtp: Vec<InboundRtpStats>,
1271 pub outbound_rtp: Vec<OutboundRtpStats>,
1272 pub candidate_pairs: Vec<IceCandidatePairStats>,
1273 pub data_channels: Vec<DataChannelStats>,
1274 /// The SCTP association; `None` before a data channel has created it.
1275 pub sctp: Option<SctpStats>,
1276}
1277
1278// ── Data channel callbacks ────────────────────────────────────────────────────
1279
1280type MessageCb = Box<dyn for<'a> FnMut(&'a [u8], bool) + Send>;
1281type EventCb = Box<dyn FnMut() + Send>;
1282type StateCb = Box<dyn FnMut(DataChannelState) + Send>;
1283
1284/// One callback, shared so it can be called with its slot unlocked.
1285type Shared<F> = Arc<Mutex<F>>;
1286
1287/// A callback slot. A callback runs with the slot's lock released, so it may
1288/// replace any handler of its own channel; its own lock, held while it runs,
1289/// keeps calls to it in order.
1290struct Slot<F>(Mutex<Option<Shared<F>>>);
1291
1292impl<F> Slot<F> {
1293 fn empty() -> Self {
1294 Self(Mutex::new(None))
1295 }
1296
1297 fn set(&self, f: F) {
1298 *self.0.lock().unwrap() = Some(Arc::new(Mutex::new(f)));
1299 }
1300
1301 fn get(&self) -> Option<Shared<F>> {
1302 self.0.lock().unwrap().clone()
1303 }
1304}
1305
1306/// The native channel handle, shared with the callbacks that run on WebRTC's
1307/// threads. Valid for as long as the owning [`DataChannel`] lives: the native
1308/// observer is unregistered before the handle is destroyed.
1309#[derive(Clone, Copy)]
1310struct RawDc(*mut reactor_webrtc_sys::DataChannel);
1311// SAFETY: the native data channel is internally thread-safe.
1312unsafe impl Send for RawDc {}
1313unsafe impl Sync for RawDc {}
1314
1315impl RawDc {
1316 fn state(self) -> DataChannelState {
1317 DataChannelState::from_raw(unsafe {
1318 reactor_webrtc_sys::reactor_webrtc_data_channel_state(self.0)
1319 })
1320 }
1321 fn buffered_amount(self) -> u64 {
1322 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_buffered_amount(self.0) }
1323 }
1324 fn send(self, data: &[u8], binary: bool) -> bool {
1325 unsafe {
1326 reactor_webrtc_sys::reactor_webrtc_data_channel_send(
1327 self.0,
1328 data.as_ptr(),
1329 data.len(),
1330 binary as c_int,
1331 ) == 1
1332 }
1333 }
1334 fn close(self) {
1335 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_close(self.0) }
1336 }
1337}
1338
1339/// Per-channel state of a chunked channel.
1340struct Chunked {
1341 queue: Mutex<reactor_webrtc_dc_chunking::SendQueue>,
1342 // Held while frames move from the queue to the native channel, so the
1343 // frames of one message stay contiguous whoever pumps. Taken with
1344 // try_lock by every caller, after setting `pump_again`: a caller that
1345 // finds it busy leaves the work to the holder instead of waiting, so a
1346 // callback never blocks on a sender.
1347 pump: Mutex<()>,
1348 pump_again: AtomicBool,
1349 reassembler: Mutex<reactor_webrtc_dc_chunking::Reassembler>,
1350 // Where this channel's messages spend their time; see `chunking_times`.
1351 times: Mutex<ChunkingTimes>,
1352 // Whole messages that arrived before on_message was set: libwebrtc holds
1353 // messages for a channel with no observer, and this keeps that promise
1354 // for the frames the pump's observer had to accept. Bounded by
1355 // `send_buffer_limit` bytes.
1356 pending: Mutex<std::collections::VecDeque<(Vec<u8>, bool)>>,
1357 pending_bytes: AtomicU64,
1358 pending_limit: u64,
1359 // The caller's on_buffered_amount_low threshold. The native threshold
1360 // belongs to the pump (the queue's low-water mark).
1361 user_low_threshold: AtomicU64,
1362 user_low_armed: AtomicBool,
1363}
1364
1365/// Everything the native observer reaches through `userdata`.
1366struct ChannelCore {
1367 raw: RawDc,
1368 on_message: Slot<MessageCb>,
1369 on_state_change: Slot<StateCb>,
1370 // Their own slots, so on_open, on_close and on_state_change coexist.
1371 on_open: Slot<EventCb>,
1372 on_close: Slot<EventCb>,
1373 on_buffered_amount_low: Slot<EventCb>,
1374 registered: AtomicBool,
1375 // The owning connection's chunking negotiation, and this channel's
1376 // decision, made once it is open.
1377 negotiation: Option<Arc<crate::dc_chunking::DcNegotiation>>,
1378 decided: std::sync::OnceLock<Option<Chunked>>,
1379 // The threshold the caller set before the channel decided, so a chunked
1380 // channel can take it over.
1381 early_low_threshold: AtomicU64,
1382}
1383
1384impl ChannelCore {
1385 /// This channel's chunking state, deciding it on first call once open.
1386 fn chunked(&self) -> Option<&Chunked> {
1387 if let Some(decided) = self.decided.get() {
1388 return decided.as_ref();
1389 }
1390 let negotiation = self.negotiation.as_ref()?;
1391 if self.raw.state() != DataChannelState::Open {
1392 return None;
1393 }
1394 let ordered =
1395 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_ordered(self.raw.0) != 0 };
1396 let reliable =
1397 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_reliable(self.raw.0) != 0 };
1398 let chunked = match (negotiation.settings(), negotiation.remote()) {
1399 (Some(settings), Some(remote)) if ordered && reliable => {
1400 let config = settings.send_config(&remote);
1401 Some(Chunked {
1402 queue: Mutex::new(reactor_webrtc_dc_chunking::SendQueue::new(config)),
1403 pump: Mutex::new(()),
1404 pump_again: AtomicBool::new(false),
1405 reassembler: Mutex::new(reactor_webrtc_dc_chunking::Reassembler::new(
1406 settings.max_message_size,
1407 )),
1408 times: Mutex::new(ChunkingTimes::default()),
1409 pending: Mutex::new(std::collections::VecDeque::new()),
1410 pending_bytes: AtomicU64::new(0),
1411 pending_limit: settings.send_buffer_limit,
1412 user_low_threshold: AtomicU64::new(
1413 self.early_low_threshold.load(Ordering::SeqCst),
1414 ),
1415 user_low_armed: AtomicBool::new(false),
1416 })
1417 }
1418 _ => None,
1419 };
1420 let decided = self.decided.get_or_init(|| chunked).as_ref();
1421 if let Some(c) = decided {
1422 // The pump needs the native callbacks whether or not the caller
1423 // set any, and resumes at the queue's low-water mark.
1424 let low = c.queue.lock().unwrap().config().low_water;
1425 unsafe {
1426 reactor_webrtc_sys::reactor_webrtc_data_channel_set_low_threshold(self.raw.0, low)
1427 };
1428 self.ensure_registered();
1429 }
1430 decided
1431 }
1432
1433 fn ensure_registered(&self) {
1434 if !self.registered.swap(true, Ordering::SeqCst) {
1435 self.register();
1436 }
1437 }
1438
1439 fn register(&self) {
1440 let ud = self as *const ChannelCore as *mut c_void;
1441 unsafe {
1442 reactor_webrtc_sys::reactor_webrtc_data_channel_register_observer(
1443 self.raw.0,
1444 ud,
1445 dc_on_message,
1446 dc_on_state_change,
1447 dc_on_buffered_amount_low,
1448 );
1449 }
1450 }
1451
1452 /// Native buffered bytes plus, on a chunked channel, the queued ones.
1453 fn buffered_amount(&self) -> u64 {
1454 let native = self.raw.buffered_amount();
1455 match self.chunked() {
1456 Some(c) => native + c.queue.lock().unwrap().queued(),
1457 None => native,
1458 }
1459 }
1460
1461 /// Move frames from the queue to the native channel while it has room.
1462 fn pump(&self, c: &Chunked) {
1463 loop {
1464 // Ask first, then try the lock. A failed try_lock means another
1465 // thread holds it and has not yet re-checked the flag after
1466 // unlocking, so the request cannot be lost between its last pass
1467 // and its exit.
1468 c.pump_again.store(true, Ordering::SeqCst);
1469 let Ok(_guard) = c.pump.try_lock() else {
1470 return;
1471 };
1472 while c.pump_again.swap(false, Ordering::SeqCst) {
1473 loop {
1474 let check_start = Instant::now();
1475 let buffered = self.raw.buffered_amount();
1476 let build_start = Instant::now();
1477 let (frame, queued_left) = {
1478 let mut queue = c.queue.lock().unwrap();
1479 let frame = queue.next_frame(buffered);
1480 (frame, !queue.is_empty())
1481 };
1482 let Some(frame) = frame else {
1483 // Frames left behind wait for the native buffer to
1484 // drain to the low-water mark.
1485 if queued_left {
1486 c.times.lock().unwrap().stalled(Instant::now());
1487 }
1488 break;
1489 };
1490 let native_start = Instant::now();
1491 let sent = self.raw.send(&frame, true);
1492 let native_end = Instant::now();
1493 let steps = FrameSteps {
1494 check_start,
1495 build_start,
1496 native_start,
1497 };
1498 if !sent {
1499 // libwebrtc closes the channel on a failed send. The
1500 // peer may hold part of a message; nothing more can
1501 // follow it.
1502 c.queue.lock().unwrap().clear();
1503 c.times.lock().unwrap().cleared();
1504 return;
1505 }
1506 // Counted once libwebrtc took it: a refused frame never left.
1507 c.times
1508 .lock()
1509 .unwrap()
1510 .frame_sent(&frame, native_end, steps);
1511 }
1512 }
1513 drop(_guard);
1514 // A request that arrived after our last pass is ours to serve.
1515 if !c.pump_again.load(Ordering::SeqCst) {
1516 break;
1517 }
1518 }
1519 }
1520
1521 fn maybe_fire_user_low(&self, c: &Chunked) {
1522 let threshold = c.user_low_threshold.load(Ordering::SeqCst);
1523 if self.buffered_amount() <= threshold && c.user_low_armed.swap(false, Ordering::SeqCst) {
1524 if let Some(cb) = self.on_buffered_amount_low.get() {
1525 (*cb.lock().unwrap())();
1526 }
1527 }
1528 }
1529
1530 /// Hand a whole message to the caller, or hold it until on_message is set.
1531 ///
1532 /// The slot stays locked only while deciding, so a message cannot be
1533 /// held just after `on_message` flushed what was held.
1534 fn deliver(&self, c: &Chunked, data: Vec<u8>, binary: bool) {
1535 let cb = {
1536 let slot = self.on_message.0.lock().unwrap();
1537 match slot.clone() {
1538 Some(cb) => cb,
1539 None => {
1540 let size = data.len() as u64;
1541 if c.pending_bytes.load(Ordering::SeqCst) + size <= c.pending_limit {
1542 c.pending_bytes.fetch_add(size, Ordering::SeqCst);
1543 c.pending.lock().unwrap().push_back((data, binary));
1544 }
1545 return;
1546 }
1547 }
1548 };
1549 (*cb.lock().unwrap())(&data, binary);
1550 }
1551}
1552
1553extern "C" fn dc_on_message(ud: *mut c_void, data: *const u8, len: usize, binary: c_int) {
1554 let core = unsafe { &*(ud as *const ChannelCore) };
1555 let bytes = unsafe { std::slice::from_raw_parts(data, len) };
1556 let Some(c) = core.chunked() else {
1557 if let Some(cb) = core.on_message.get() {
1558 (*cb.lock().unwrap())(bytes, binary != 0);
1559 }
1560 return;
1561 };
1562 // Every frame of a chunked channel is sent as binary.
1563 let delivery = if binary != 0 {
1564 c.reassembler.lock().unwrap().push(bytes)
1565 } else {
1566 Err(reactor_webrtc_dc_chunking::FrameError::TypeChanged)
1567 };
1568 if let Ok(d) = &delivery {
1569 let completes = !matches!(d, reactor_webrtc_dc_chunking::Delivery::Pending);
1570 c.times
1571 .lock()
1572 .unwrap()
1573 .frame_received(completes, Instant::now());
1574 }
1575 match delivery {
1576 Ok(reactor_webrtc_dc_chunking::Delivery::Message { data, binary }) => {
1577 core.deliver(c, data, binary)
1578 }
1579 Ok(reactor_webrtc_dc_chunking::Delivery::Pending) => {}
1580 // Larger than this side's limit: dropped without buffering, and the
1581 // channel keeps working.
1582 Ok(reactor_webrtc_dc_chunking::Delivery::Dropped { .. }) => {}
1583 // The peer broke the wire format; the stream cannot be trusted.
1584 Err(_) => {
1585 c.reassembler.lock().unwrap().reset();
1586 c.times.lock().unwrap().reset_receive();
1587 core.raw.close();
1588 }
1589 }
1590}
1591
1592extern "C" fn dc_on_state_change(ud: *mut c_void, state: c_int) {
1593 let core = unsafe { &*(ud as *const ChannelCore) };
1594 let state = DataChannelState::from_raw(state);
1595 match state {
1596 // Decide at the Open transition, so the framing is fixed before the
1597 // first message either way.
1598 DataChannelState::Open => {
1599 core.chunked();
1600 }
1601 DataChannelState::Closed => {
1602 if let Some(Some(c)) = core.decided.get() {
1603 c.queue.lock().unwrap().clear();
1604 c.reassembler.lock().unwrap().reset();
1605 let mut times = c.times.lock().unwrap();
1606 times.cleared();
1607 times.reset_receive();
1608 }
1609 }
1610 _ => {}
1611 }
1612 if let Some(cb) = core.on_state_change.get() {
1613 (*cb.lock().unwrap())(state);
1614 }
1615 let event = match state {
1616 DataChannelState::Open => Some(&core.on_open),
1617 DataChannelState::Closed => Some(&core.on_close),
1618 _ => None,
1619 };
1620 if let Some(cb) = event.and_then(Slot::get) {
1621 (*cb.lock().unwrap())();
1622 }
1623}
1624
1625extern "C" fn dc_on_buffered_amount_low(ud: *mut c_void) {
1626 let core = unsafe { &*(ud as *const ChannelCore) };
1627 match core.decided.get() {
1628 // The caller's own threshold is checked here, from the native event,
1629 // never from inside a send: like libwebrtc, `send` does not call back.
1630 Some(Some(c)) => {
1631 core.pump(c);
1632 core.maybe_fire_user_low(c);
1633 }
1634 _ => {
1635 if let Some(cb) = core.on_buffered_amount_low.get() {
1636 (*cb.lock().unwrap())();
1637 }
1638 }
1639 }
1640}
1641
1642/// A data channel — either locally created or handed to `on_data_channel` by
1643/// the remote peer. Dropping releases the native handle.
1644///
1645/// On a channel whose connection negotiated chunking (see
1646/// [`is_chunked`](Self::is_chunked)), messages travel as frames: `send`
1647/// accepts payloads larger than libwebrtc's 16 MiB send buffer, queueing
1648/// them and feeding the native channel as it drains, and `on_message`
1649/// receives each message whole. Every other channel behaves exactly as a
1650/// plain libwebrtc data channel.
1651pub struct DataChannel {
1652 raw: *mut reactor_webrtc_sys::DataChannel,
1653 // Addressed by the native observer as `userdata`; boxed so its address
1654 // is stable for as long as the observer is registered.
1655 core: Box<ChannelCore>,
1656 // Keeps the factory's signaling/network threads alive for as long as this
1657 // channel exists — a caller can detach it and outlive both the connection
1658 // that created it and the factory that ultimately owns those threads.
1659 _factory: Arc<FactoryHandle>,
1660}
1661
1662// SAFETY: the native data channel is internally thread-safe; callbacks are
1663// serialized on the network/signaling thread and guarded by mutexes on the
1664// Rust side.
1665unsafe impl Send for DataChannel {}
1666unsafe impl Sync for DataChannel {}
1667
1668impl DataChannel {
1669 pub(crate) fn from_raw(
1670 raw: *mut reactor_webrtc_sys::DataChannel,
1671 factory: Arc<FactoryHandle>,
1672 ) -> Self {
1673 Self {
1674 raw,
1675 core: Box::new(ChannelCore {
1676 raw: RawDc(raw),
1677 on_message: Slot::empty(),
1678 on_state_change: Slot::empty(),
1679 on_open: Slot::empty(),
1680 on_close: Slot::empty(),
1681 on_buffered_amount_low: Slot::empty(),
1682 registered: AtomicBool::new(false),
1683 negotiation: None,
1684 decided: std::sync::OnceLock::new(),
1685 early_low_threshold: AtomicU64::new(0),
1686 }),
1687 _factory: factory,
1688 }
1689 }
1690
1691 pub(crate) fn with_dc_negotiation(
1692 mut self,
1693 negotiation: Arc<crate::dc_chunking::DcNegotiation>,
1694 ) -> Self {
1695 self.core.negotiation = Some(negotiation);
1696 self
1697 }
1698
1699 /// Whether this channel carries chunked messages.
1700 ///
1701 /// Decided once — at the `Open` transition, or the first time it is
1702 /// asked while open — and fixed from then on: a channel is chunked when
1703 /// its connection negotiated chunking with the peer and the channel is
1704 /// ordered and fully reliable (no `maxRetransmits`, no
1705 /// `maxPacketLifeTime`). Both ends see the same SDP and the same channel
1706 /// parameters, so they reach the same answer without signalling anything
1707 /// else. `false` before the channel opens.
1708 pub fn is_chunked(&self) -> bool {
1709 self.core.chunked().is_some()
1710 }
1711
1712 /// Where this channel's messages have spent their time since it opened,
1713 /// as running totals; `None` on a channel that is not chunked.
1714 ///
1715 /// On the sending side: the wait in the queue before a message's first
1716 /// frame leaves, the time from its first frame to its last, and how often
1717 /// and how long the native buffer held the queue back. On the receiving
1718 /// side: the time from a message's first frame to its last. See
1719 /// [`ChunkingStats`](crate::ChunkingStats).
1720 pub fn chunking_stats(&self) -> Option<crate::ChunkingStats> {
1721 self.core.chunked().map(|c| c.times.lock().unwrap().stats())
1722 }
1723
1724 /// Whether the channel delivers messages in order.
1725 pub fn ordered(&self) -> bool {
1726 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_ordered(self.raw) != 0 }
1727 }
1728
1729 /// Whether the channel retransmits until delivery: neither
1730 /// `maxRetransmits` nor `maxPacketLifeTime` is set.
1731 pub fn reliable(&self) -> bool {
1732 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_reliable(self.raw) != 0 }
1733 }
1734
1735 /// The label this channel was created with.
1736 pub fn label(&self) -> String {
1737 let mut buf = [0u8; 256];
1738 let n = unsafe {
1739 reactor_webrtc_sys::reactor_webrtc_data_channel_label(
1740 self.raw,
1741 buf.as_mut_ptr() as *mut c_char,
1742 buf.len() as c_int,
1743 )
1744 };
1745 if n < 0 {
1746 String::new()
1747 } else {
1748 unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
1749 .to_string_lossy()
1750 .into_owned()
1751 }
1752 }
1753
1754 /// Current readiness state.
1755 pub fn state(&self) -> DataChannelState {
1756 self.core.raw.state()
1757 }
1758
1759 /// Bytes waiting to be sent (backpressure signal). On a chunked channel
1760 /// this includes what is still queued beyond libwebrtc's own buffer.
1761 pub fn buffered_amount(&self) -> u64 {
1762 self.core.buffered_amount()
1763 }
1764
1765 /// Set the threshold below which `on_buffered_amount_low` fires. On a
1766 /// chunked channel it applies to [`buffered_amount`](Self::buffered_amount),
1767 /// queue included.
1768 pub fn set_buffered_amount_low_threshold(&self, threshold: u64) {
1769 self.core
1770 .early_low_threshold
1771 .store(threshold, Ordering::SeqCst);
1772 match self.core.chunked() {
1773 Some(c) => c.user_low_threshold.store(threshold, Ordering::SeqCst),
1774 None => unsafe {
1775 reactor_webrtc_sys::reactor_webrtc_data_channel_set_low_threshold(
1776 self.raw, threshold,
1777 )
1778 },
1779 }
1780 }
1781
1782 /// Send a message. `binary` selects the message type the receiver sees.
1783 ///
1784 /// On a chunked channel the message is queued and leaves as frames, so
1785 /// it may be larger than libwebrtc's 16 MiB send buffer, up to the
1786 /// effective max message size; a full queue or an oversized message is
1787 /// an [`Error::DataChannel`] and nothing is sent. Elsewhere this is a
1788 /// plain libwebrtc send.
1789 pub fn send(&self, data: &[u8], binary: bool) -> Result<()> {
1790 // Before anything else: deciding the chunking on a first send and the
1791 // state check below are answered on libwebrtc's network thread, so
1792 // they can wait there, and that wait is part of the enqueue.
1793 let entered = Instant::now();
1794 let Some(c) = self.core.chunked() else {
1795 return if self.core.raw.send(data, binary) {
1796 Ok(())
1797 } else {
1798 Err(Error::Webrtc("data channel send failed".into()))
1799 };
1800 };
1801 // A closed channel would take the message and drop it on the first
1802 // native send; refuse it as the plain path does.
1803 if self.state() != DataChannelState::Open {
1804 return Err(Error::Webrtc("data channel send failed".into()));
1805 }
1806 {
1807 // Held across both, so the queue and the send times stay in step
1808 // when several threads send at once.
1809 let mut queue = c.queue.lock().unwrap();
1810 queue
1811 .push(data.to_vec(), binary)
1812 .map_err(Error::DataChannel)?;
1813 let now = Instant::now();
1814 c.times
1815 .lock()
1816 .unwrap()
1817 .queued(now, (now - entered).as_secs_f64());
1818 }
1819 c.user_low_armed.store(true, Ordering::SeqCst);
1820 self.core.pump(c);
1821 Ok(())
1822 }
1823
1824 /// Block until everything queued has been handed to SCTP and libwebrtc's
1825 /// own buffer is empty, or `timeout` passes. Returns whether it drained.
1826 ///
1827 /// Don't call it (or [`close`](Self::close) with a drain timeout) from
1828 /// this channel's callbacks: they run on libwebrtc's network thread, which
1829 /// is the thread that sends, so nothing drains until the timeout passes.
1830 pub fn drain(&self, timeout: std::time::Duration) -> bool {
1831 // A timeout too large to add to now never expires.
1832 let deadline = std::time::Instant::now().checked_add(timeout);
1833 loop {
1834 if self.buffered_amount() == 0 {
1835 return true;
1836 }
1837 let expired = deadline.is_some_and(|d| std::time::Instant::now() >= d);
1838 if self.state() != DataChannelState::Open || expired {
1839 return false;
1840 }
1841 // Refill here too, rather than only from the native low-water
1842 // event, so a drain never waits on an event it already missed.
1843 if let Some(c) = self.core.chunked() {
1844 self.core.pump(c);
1845 }
1846 std::thread::sleep(std::time::Duration::from_millis(2));
1847 }
1848 }
1849
1850 /// Close the channel. A chunked channel first drains what it has queued,
1851 /// for up to `drain_timeout`, so a message already accepted by `send`
1852 /// is not cut short.
1853 pub fn close(&self, drain_timeout: std::time::Duration) {
1854 if self.core.chunked().is_some() {
1855 self.drain(drain_timeout);
1856 }
1857 self.core.raw.close();
1858 }
1859
1860 /// Receive handler — fires on every incoming message. The closure runs on
1861 /// a WebRTC network thread; return quickly or offload heavy work. On a
1862 /// chunked channel it fires once per whole message, including any that
1863 /// arrived before it was set.
1864 pub fn on_message(&self, cb: impl for<'a> FnMut(&'a [u8], bool) + Send + 'static) {
1865 // The new callback is published already locked, and the held messages
1866 // are taken out under the slot's lock: a message that arrives while
1867 // they are flushed waits on the callback, so the order holds, and the
1868 // callback can still replace any handler of this channel.
1869 let shared: Shared<MessageCb> = Arc::new(Mutex::new(Box::new(cb)));
1870 let mut running = shared.lock().unwrap();
1871 let held = {
1872 let mut slot = self.core.on_message.0.lock().unwrap();
1873 *slot = Some(Arc::clone(&shared));
1874 match self.core.decided.get() {
1875 Some(Some(c)) => {
1876 c.pending_bytes.store(0, Ordering::SeqCst);
1877 std::mem::take(&mut *c.pending.lock().unwrap())
1878 }
1879 _ => std::collections::VecDeque::new(),
1880 }
1881 };
1882 for (data, binary) in held {
1883 (*running)(&data, binary);
1884 }
1885 drop(running);
1886 self.reregister();
1887 }
1888
1889 /// State-change handler — fires for every transition including
1890 /// Connecting → Open → Closing → Closed.
1891 pub fn on_state_change(&self, cb: impl FnMut(DataChannelState) + Send + 'static) {
1892 self.core.on_state_change.set(Box::new(cb));
1893 self.reregister();
1894 }
1895
1896 /// Fires when the channel becomes `Open`. Independent of
1897 /// [`on_state_change`](Self::on_state_change) and [`on_close`](Self::on_close):
1898 /// setting one does not replace the others.
1899 pub fn on_open(&self, cb: impl FnMut() + Send + 'static) {
1900 self.core.on_open.set(Box::new(cb));
1901 self.reregister();
1902 }
1903
1904 /// Fires when the channel reaches `Closed`. Independent of
1905 /// [`on_state_change`](Self::on_state_change) and [`on_open`](Self::on_open).
1906 pub fn on_close(&self, cb: impl FnMut() + Send + 'static) {
1907 self.core.on_close.set(Box::new(cb));
1908 self.reregister();
1909 }
1910
1911 /// Flow-control handler — fires when `buffered_amount` drops at or below
1912 /// the threshold set by [`set_buffered_amount_low_threshold`](Self::set_buffered_amount_low_threshold).
1913 ///
1914 /// Like libwebrtc's, it never fires from inside [`send`](Self::send), so
1915 /// it may call `send` to refill. On a chunked channel it is checked when
1916 /// libwebrtc's buffer falls to the queue's low-water mark.
1917 pub fn on_buffered_amount_low(&self, cb: impl FnMut() + Send + 'static) {
1918 self.core.on_buffered_amount_low.set(Box::new(cb));
1919 self.reregister();
1920 }
1921
1922 // The native observer is registered once, by whichever comes first: a
1923 // setter, or a chunked channel deciding. It addresses the channel's core
1924 // and every callback reads its slot when it fires, so a later setter only
1925 // swaps the slot. Re-registering would wait on the signaling thread, which
1926 // runs the callbacks: a setter called from a callback while another
1927 // thread re-registered deadlocked.
1928 fn reregister(&self) {
1929 self.core.ensure_registered();
1930 }
1931}
1932
1933impl Drop for DataChannel {
1934 fn drop(&mut self) {
1935 // Unregisters the native observer before the core box is freed.
1936 unsafe { reactor_webrtc_sys::reactor_webrtc_data_channel_destroy(self.raw) }
1937 }
1938}
1939
1940// ── async-op bridges (block on a one-shot callback) ──────────────────────────
1941
1942type SdpTx = SyncSender<Result<SessionDescription>>;
1943type CompleteTx = SyncSender<Result<()>>;
1944
1945extern "C" fn sdp_ok(ud: *mut c_void, ty: *const c_char, sdp: *const c_char) {
1946 // Reclaims this callback's own strong ref (see the comment on `run_sdp`
1947 // for why the caller cannot be the sole owner of the box).
1948 let tx = unsafe { Arc::from_raw(ud as *const SdpTx) };
1949 let kind = unsafe { CStr::from_ptr(ty) }.to_string_lossy();
1950 let sdp = unsafe { CStr::from_ptr(sdp) }
1951 .to_string_lossy()
1952 .into_owned();
1953 let result = match SdpType::from_str(&kind) {
1954 Some(kind) => Ok(SessionDescription { kind, sdp }),
1955 None => Err(Error::Webrtc(format!("unknown sdp type: {kind}"))),
1956 };
1957 let _ = tx.try_send(result);
1958}
1959extern "C" fn sdp_err(ud: *mut c_void, message: *const c_char) {
1960 let tx = unsafe { Arc::from_raw(ud as *const SdpTx) };
1961 let msg = unsafe { CStr::from_ptr(message) }
1962 .to_string_lossy()
1963 .into_owned();
1964 let _ = tx.try_send(Err(Error::Webrtc(msg)));
1965}
1966extern "C" fn complete_cb(ud: *mut c_void, error: *const c_char) {
1967 let tx = unsafe { Arc::from_raw(ud as *const CompleteTx) };
1968 let r = if error.is_null() {
1969 Ok(())
1970 } else {
1971 Err(Error::Webrtc(
1972 unsafe { CStr::from_ptr(error) }
1973 .to_string_lossy()
1974 .into_owned(),
1975 ))
1976 };
1977 let _ = tx.try_send(r);
1978}
1979
1980type StatsTx = SyncSender<StatsReport>;
1981
1982/// One of [`ReactorStatEntry`]'s fixed string buffers; `None` when empty, which
1983/// is how the glue says absent.
1984fn stat_str(buf: &[c_char]) -> Option<String> {
1985 let bytes: Vec<u8> = buf
1986 .iter()
1987 .take_while(|&&c| c != 0)
1988 .map(|&c| c as u8)
1989 .collect();
1990 (!bytes.is_empty()).then(|| String::from_utf8_lossy(&bytes).into_owned())
1991}
1992
1993/// A stats string the C side cuts to fit its buffer rather than drop. The cut
1994/// counts bytes, so a UTF-8 character it split is dropped here instead of
1995/// showing up as a replacement character.
1996fn stat_str_prefix(buf: &[c_char]) -> Option<String> {
1997 let bytes: Vec<u8> = buf
1998 .iter()
1999 .take_while(|&&c| c != 0)
2000 .map(|&c| c as u8)
2001 .collect();
2002 let text = match std::str::from_utf8(&bytes) {
2003 Ok(text) => text.to_owned(),
2004 Err(e) if e.error_len().is_none() => {
2005 String::from_utf8_lossy(&bytes[..e.valid_up_to()]).into_owned()
2006 }
2007 Err(_) => String::from_utf8_lossy(&bytes).into_owned(),
2008 };
2009 (!text.is_empty()).then_some(text)
2010}
2011
2012extern "C" fn stats_cb(ud: *mut c_void, entries: *const ReactorStatEntry, count: c_int) {
2013 let tx = unsafe { Arc::from_raw(ud as *const StatsTx) };
2014 let slice = if entries.is_null() || count <= 0 {
2015 &[][..]
2016 } else {
2017 unsafe { std::slice::from_raw_parts(entries, count as usize) }
2018 };
2019 let mut report = StatsReport::default();
2020 for e in slice {
2021 match e.kind {
2022 0 => report.inbound_rtp.push(InboundRtpStats {
2023 ssrc: e.ssrc,
2024 kind: StreamKind::from_raw(e.stream_kind),
2025 mid: stat_str(&e.mid),
2026 codec_mime_type: stat_str(&e.codec_mime_type),
2027 packets_received: e.packets_received,
2028 bytes_received: e.bytes_received,
2029 jitter_s: e.jitter,
2030 packets_lost: e.packets_lost,
2031 nack_count: e.nack_count,
2032 pli_count: e.pli_count,
2033 fir_count: e.fir_count,
2034 total_decode_time_s: e.total_decode_time,
2035 frames_per_second: e.frames_per_second,
2036 frames_decoded: e.frames_decoded,
2037 frames_dropped: e.frames_dropped,
2038 frame_width: e.frame_width,
2039 frame_height: e.frame_height,
2040 jitter_buffer_delay_s: e.jitter_buffer_delay,
2041 jitter_buffer_target_delay_s: e.jitter_buffer_target_delay,
2042 jitter_buffer_minimum_delay_s: e.jitter_buffer_minimum_delay,
2043 jitter_buffer_emitted_count: e.jitter_buffer_emitted_count,
2044 total_processing_delay_s: e.total_processing_delay,
2045 timing_frame: (e.timing_frame_present != 0).then_some(TimingFrameInfo {
2046 rtp_timestamp: e.timing_frame_rtp_timestamp,
2047 is_outlier: e.timing_is_outlier != 0,
2048 is_timer_triggered: e.timing_is_timer_triggered != 0,
2049 sender: TimingFrameSenderTimestamps {
2050 capture_ms: e.timing_capture_ms,
2051 encode_start_ms: e.timing_encode_start_ms,
2052 encode_finish_ms: e.timing_encode_finish_ms,
2053 packetization_finish_ms: e.timing_packetization_finish_ms,
2054 pacer_exit_ms: e.timing_pacer_exit_ms,
2055 },
2056 receiver: TimingFrameReceiverTimestamps {
2057 receive_start_ms: e.timing_receive_start_ms,
2058 receive_finish_ms: e.timing_receive_finish_ms,
2059 decode_start_ms: e.timing_decode_start_ms,
2060 decode_finish_ms: e.timing_decode_finish_ms,
2061 },
2062 }),
2063 decoder_implementation: stat_str_prefix(&e.codec_implementation),
2064 power_efficient_decoder: power_efficient(e.power_efficient),
2065 }),
2066 1 => report.outbound_rtp.push(OutboundRtpStats {
2067 ssrc: e.ssrc,
2068 kind: StreamKind::from_raw(e.stream_kind),
2069 mid: stat_str(&e.mid),
2070 codec_mime_type: stat_str(&e.codec_mime_type),
2071 packets_sent: e.packets_sent,
2072 bytes_sent: e.bytes_sent,
2073 target_bitrate_bps: e.target_bitrate,
2074 round_trip_time_s: e.round_trip_time,
2075 total_round_trip_time_s: e.total_round_trip_time,
2076 fraction_lost: e.fraction_lost,
2077 packets_lost: e.packets_lost,
2078 retransmitted_packets_sent: e.retransmitted_packets_sent,
2079 nack_count: e.nack_count,
2080 pli_count: e.pli_count,
2081 fir_count: e.fir_count,
2082 frames_per_second: e.frames_per_second,
2083 frames_sent: e.frames_sent,
2084 frame_width: e.frame_width,
2085 frame_height: e.frame_height,
2086 frames_encoded: e.frames_encoded,
2087 total_encode_time_s: e.total_encode_time,
2088 total_packet_send_delay_s: e.total_packet_send_delay,
2089 encoder_implementation: stat_str_prefix(&e.codec_implementation),
2090 power_efficient_encoder: power_efficient(e.power_efficient),
2091 }),
2092 2 => report.candidate_pairs.push(IceCandidatePairStats {
2093 current_round_trip_time_s: e.current_round_trip_time,
2094 total_round_trip_time_s: e.total_round_trip_time,
2095 priority: e.priority,
2096 state: IceCandidatePairState::from_raw(e.pair_state),
2097 nominated: e.nominated != 0,
2098 writable: e.writable != 0,
2099 available_outgoing_bitrate_bps: e.available_outgoing_bitrate,
2100 available_incoming_bitrate_bps: e.available_incoming_bitrate,
2101 bytes_sent: e.bytes_sent,
2102 bytes_received: e.bytes_received,
2103 packets_sent: e.packets_sent,
2104 // The pair's own 64-bit counter, not kind 0's 32-bit one.
2105 packets_received: e.pair_packets_received,
2106 local_candidate_type: IceCandidateType::from_raw(e.local_candidate_type),
2107 local_relay_protocol: RelayProtocol::from_raw(e.local_relay_protocol),
2108 }),
2109 3 => report.data_channels.push(DataChannelStats {
2110 label: data_channel_label(e),
2111 id: u16::try_from(e.data_channel_id).ok(),
2112 state: DataChannelState::from_raw(e.data_channel_state),
2113 messages_sent: e.messages_sent,
2114 bytes_sent: e.bytes_sent,
2115 messages_received: e.messages_received,
2116 bytes_received: e.bytes_received,
2117 }),
2118 _ => {}
2119 }
2120 }
2121 let _ = tx.try_send(report);
2122}
2123
2124/// The stats entry's power-efficient flag: -1 unknown, 0 no, 1 yes.
2125fn power_efficient(v: i8) -> Option<bool> {
2126 (v >= 0).then_some(v == 1)
2127}
2128
2129/// A data channel's label from its stats entry. An empty label is a valid
2130/// label, so presence comes from its own flag rather than from emptiness.
2131fn data_channel_label(e: &ReactorStatEntry) -> Option<String> {
2132 (e.data_channel_label_present != 0).then(|| stat_str(&e.data_channel_label).unwrap_or_default())
2133}
2134
2135extern "C" fn sctp_cb(
2136 ud: *mut c_void,
2137 m: *const reactor_webrtc_sys::ReactorSctpMetrics,
2138 ok: c_int,
2139) {
2140 let tx = unsafe { Arc::from_raw(ud as *const SyncSender<Option<SctpStats>>) };
2141 let stats = (ok == 1 && !m.is_null()).then(|| {
2142 let m = unsafe { &*m };
2143 SctpStats {
2144 packets_sent: m.tx_packets,
2145 messages_sent: m.tx_messages,
2146 packets_retransmitted: m.rtx_packets,
2147 bytes_retransmitted: m.rtx_bytes,
2148 packets_received: m.rx_packets,
2149 messages_received: m.rx_messages,
2150 congestion_window_bytes: m.cwnd_bytes,
2151 unacked_chunks: m.unack_data,
2152 smoothed_rtt_ms: m.srtt_ms,
2153 peer_receive_window_bytes: m.peer_rwnd_bytes,
2154 message_interleaving: m.uses_message_interleaving != 0,
2155 }
2156 });
2157 let _ = tx.try_send(stats);
2158}
2159
2160// `call` dispatches onto a libwebrtc thread that invokes the C callback
2161// asynchronously; the public API gives no guarantee that thread has finished
2162// unwinding out of the callback (still touching `tx` inside its own `notify()`
2163// after `try_send`'s value became visible to `recv_timeout`) by the time this
2164// function's wait returns. Sharing the box via `Arc` instead of freeing it
2165// unilaterally here means whichever side — this caller, or the callback —
2166// finishes last is the one that frees it, closing that use-after-free window.
2167// Mirrors the AddRef/Release fix applied to StatsCallback on the C++ side.
2168//
2169// Two consequences of the `Arc::from_raw` in each callback above, since it's
2170// the contract those four `extern "C"` fns must honour:
2171// - Exactly-once delivery is now a *safety* requirement, not just a
2172// correctness one: a second `Arc::from_raw` on the same pointer double-frees
2173// once both callback-side refs drop. Holds today (`CreateSdpObserver` fires
2174// exactly one of `OnSuccess`/`OnFailure`; the `Set*DescObserver`s and
2175// `AddIceCandidate`'s completion each fire once; every early-return path in
2176// the glue invokes the callback before returning) but isn't enforced by the
2177// type system.
2178// - If a callback is *never* invoked (e.g. an in-flight completion dropped
2179// during peer-connection teardown), its ref is never reclaimed and the
2180// channel leaks. Deliberate — a leak beats the UAF it replaces — but it
2181// does mean "whichever side finishes last frees it" assumes the callback
2182// side eventually runs at all.
2183
2184fn run_stats(call: impl FnOnce(*mut c_void)) -> Result<StatsReport> {
2185 let (tx, rx) = sync_channel::<StatsReport>(1);
2186 let tx = Arc::new(tx);
2187 let p = Arc::into_raw(tx.clone());
2188 call(p as *mut c_void);
2189 let r = rx.recv_timeout(OP_TIMEOUT);
2190 drop(tx);
2191 r.map_err(|_| Error::Webrtc("get_stats timed out".into()))
2192}
2193
2194fn run_sdp(call: impl FnOnce(*mut c_void)) -> Result<SessionDescription> {
2195 let (tx, rx) = sync_channel::<Result<SessionDescription>>(1);
2196 let tx = Arc::new(tx);
2197 let p = Arc::into_raw(tx.clone());
2198 call(p as *mut c_void);
2199 let r = rx.recv_timeout(OP_TIMEOUT);
2200 drop(tx);
2201 r.map_err(|_| Error::Webrtc("sdp operation timed out".into()))?
2202}
2203
2204fn run_complete(call: impl FnOnce(*mut c_void)) -> Result<()> {
2205 await_complete(call).unwrap_or_else(|| Err(complete_timed_out()))
2206}
2207
2208/// [`run_complete`], telling a timeout apart: `None` when the operation did
2209/// not complete within [`OP_TIMEOUT`] — libwebrtc may still complete it.
2210fn await_complete(call: impl FnOnce(*mut c_void)) -> Option<Result<()>> {
2211 let (tx, rx) = sync_channel::<Result<()>>(1);
2212 let tx = Arc::new(tx);
2213 let p = Arc::into_raw(tx.clone());
2214 call(p as *mut c_void);
2215 let r = rx.recv_timeout(OP_TIMEOUT);
2216 drop(tx);
2217 r.ok()
2218}
2219
2220fn complete_timed_out() -> Error {
2221 Error::Webrtc("operation timed out".into())
2222}
2223
2224/// An `RTCPeerConnection`.
2225pub struct PeerConnection {
2226 raw: *mut reactor_webrtc_sys::PeerConnection,
2227 // Keeps the observer closures alive for the connection's lifetime. The
2228 // native side holds a pointer into this box.
2229 _observer: Box<ObserverState>,
2230 // Keeps the factory's signaling/worker/network threads alive for as long
2231 // as this connection exists — destroying it dispatches onto them, so it
2232 // must not be destroyed after they are.
2233 _factory: Arc<FactoryHandle>,
2234 // Opened by set_remote_description when the remote declares
2235 // FRAME_METADATA_URI; read per frame by the sender metadata transforms.
2236 frame_metadata_gate: crate::metadata::FrameMetadataGate,
2237 // RtcConfiguration::frame_metadata. When false this connection behaves like one
2238 // built before the capability existed: nothing is advertised, nothing is
2239 // mirrored, the gate never opens and no transform is installed.
2240 frame_metadata_enabled: bool,
2241 // The factory's DcChunking (unless RtcConfiguration::dc_chunking opted
2242 // this connection out) and, once negotiated, the peer's parameters.
2243 // Shared with the observer and every data channel.
2244 dc_negotiation: Arc<crate::dc_chunking::DcNegotiation>,
2245}
2246
2247// SAFETY: the native peer connection is internally thread-safe; observer
2248// callbacks are serialized on the signaling thread and guarded by mutexes.
2249unsafe impl Send for PeerConnection {}
2250unsafe impl Sync for PeerConnection {}
2251
2252impl PeerConnection {
2253 pub(crate) fn new(
2254 raw: *mut reactor_webrtc_sys::PeerConnection,
2255 observer: Box<ObserverState>,
2256 factory: Arc<FactoryHandle>,
2257 frame_metadata_enabled: bool,
2258 dc_negotiation: Arc<crate::dc_chunking::DcNegotiation>,
2259 ) -> Self {
2260 Self {
2261 raw,
2262 _observer: observer,
2263 _factory: factory,
2264 frame_metadata_gate: crate::metadata::FrameMetadataGate::new(),
2265 frame_metadata_enabled,
2266 dc_negotiation,
2267 }
2268 }
2269
2270 /// The chunking settings this connection takes part with: its factory's
2271 /// [`DcChunking`](crate::DcChunking), or `None` when the factory does not
2272 /// enable chunking or this connection opted out. A channel is only
2273 /// chunked when the peer declares chunking too.
2274 pub fn dc_chunking(&self) -> Option<&crate::DcChunking> {
2275 self.dc_negotiation.settings()
2276 }
2277
2278 /// Whether chunking was negotiated: this connection takes part, and the
2279 /// first offer/answer round to complete declared it on both sides. Fixed
2280 /// from then on; a later renegotiation neither adds nor drops it. Each
2281 /// channel still decides for itself: see [`DataChannel::is_chunked`].
2282 pub fn dc_chunking_negotiated(&self) -> bool {
2283 self.dc_negotiation.remote().is_some()
2284 }
2285
2286 // ── Signaling (blocking on the native callback) ──────────────────────────
2287
2288 /// Create an offer.
2289 ///
2290 /// Every offer advertises frame-metadata support as a session-level
2291 /// `a=x-reactor-frame-metadata:<version>`, because this crate does support it. A
2292 /// peer that does not understand the attribute ignores it — RFC 8866 §6 requires
2293 /// unrecognised attributes to be ignored.
2294 ///
2295 /// The declaration is what lets the answerer tell us it strips trailers, which
2296 /// is what opens this connection's
2297 /// [`FrameMetadataGate`](crate::FrameMetadataGate).
2298 pub fn create_offer(&self) -> Result<SessionDescription> {
2299 let offer = run_sdp(|ud| unsafe {
2300 reactor_webrtc_sys::reactor_webrtc_peer_connection_create_offer(
2301 self.raw, ud, sdp_ok, sdp_err,
2302 )
2303 })?;
2304 let offer = if self.frame_metadata_enabled {
2305 offer.with_frame_metadata()
2306 } else {
2307 offer
2308 };
2309 // Declared whenever this connection takes part in chunking.
2310 Ok(self.dc_negotiation.offer(offer))
2311 }
2312
2313 /// Create an answer.
2314 ///
2315 /// Mirrors the offer on frame metadata: the capability is declared only when the
2316 /// offer declared it. Introducing it in an answer that was not offered it is not
2317 /// something offer/answer can express, so a silent offer produces a silent answer
2318 /// and the gate stays closed in both directions.
2319 ///
2320 /// Requires [`set_remote_description`](Self::set_remote_description) to have been
2321 /// called with the offer first, which is already the only valid order.
2322 pub fn create_answer(&self) -> Result<SessionDescription> {
2323 let answer = run_sdp(|ud| unsafe {
2324 reactor_webrtc_sys::reactor_webrtc_peer_connection_create_answer(
2325 self.raw, ud, sdp_ok, sdp_err,
2326 )
2327 })?;
2328 // The gate is only ever armed when the flag is on, so this covers both "the
2329 // offer did not ask" and "this connection does not take part".
2330 let answer = if self.frame_metadata_gate.is_open() {
2331 answer.with_frame_metadata()
2332 } else {
2333 answer
2334 };
2335 // Mirrors the offer: declared only when the offer declared chunking
2336 // and this connection takes part.
2337 Ok(self.dc_negotiation.answer(answer))
2338 }
2339
2340 /// Apply the local description.
2341 ///
2342 /// Also runs the frame-metadata install, for the same reason
2343 /// [`set_remote_description`](Self::set_remote_description) does. An answerer
2344 /// applies the offer *before* it attaches its outbound tracks — apply, attach,
2345 /// answer — so at the point the remote description armed the gate a sender had
2346 /// no track to find metadata state on. By the time the answer is set locally it
2347 /// does. Installing at both points covers the offerer (armed by the answer) and
2348 /// the answerer (tracks attached after the offer) without either needing to know
2349 /// which role it is playing.
2350 ///
2351 /// On a connection that takes part in data-channel chunking, descriptions are
2352 /// applied one at a time: a call waits for one in progress on another thread.
2353 /// Do not call this from an observer callback, which runs on the signaling
2354 /// thread: an apply in progress elsewhere waits on that thread, and both stall
2355 /// until it times out.
2356 pub fn set_local_description(&self, sdp: &SessionDescription) -> Result<()> {
2357 // Chunking is recorded before the native call: a channel can open and
2358 // decide as soon as libwebrtc applies the description.
2359 self.dc_negotiation
2360 .apply_local(sdp, || self.set_description(sdp, true))?;
2361 self.install_frame_metadata_transforms();
2362 self.lock_negotiated_send_codecs();
2363 Ok(())
2364 }
2365
2366 /// Apply the remote description, and arm this connection's
2367 /// [`FrameMetadataGate`](crate::FrameMetadataGate) from it.
2368 ///
2369 /// The gate opens when `sdp` declares the capability and closes when it does
2370 /// not, on every call — so a renegotiation in which the peer drops support
2371 /// closes it again.
2372 ///
2373 /// On an answerer this runs before [`create_answer`](Self::create_answer), which
2374 /// is what lets the answer mirror the offer.
2375 ///
2376 /// Like [`set_local_description`](Self::set_local_description), not to be
2377 /// called from an observer callback.
2378 pub fn set_remote_description(&self, sdp: &SessionDescription) -> Result<()> {
2379 // Chunking is recorded before the native call, and undone if libwebrtc
2380 // rejects the description: a channel can open and decide as soon as
2381 // libwebrtc applies it, and one that decided first would stay plain
2382 // while its twin framed.
2383 self.dc_negotiation
2384 .apply_remote(sdp, || self.set_description(sdp, false))?;
2385 // After the native call, not before: a description libwebrtc rejected was
2386 // never applied, and must not move the gate.
2387 //
2388 // A disabled connection never arms the gate, so it never answers with the
2389 // capability and never installs a transform.
2390 self.frame_metadata_gate
2391 .set(self.frame_metadata_enabled && sdp.declares_frame_metadata());
2392 self.install_frame_metadata_transforms();
2393 self.lock_negotiated_send_codecs();
2394 Ok(())
2395 }
2396
2397 /// This connection's frame-metadata gate: what the remote peer declared.
2398 ///
2399 /// Cloneable and cheap. Reading it is diagnostic — the library already consults
2400 /// it when answering, when installing the transforms, and when appending a
2401 /// trailer, so a caller does not need to. It stays closed until
2402 /// [`set_remote_description`](Self::set_remote_description) sees a remote
2403 /// description that declares support.
2404 pub fn frame_metadata_gate(&self) -> crate::metadata::FrameMetadataGate {
2405 self.frame_metadata_gate.clone()
2406 }
2407
2408 /// Wire the frame-metadata steps into every video transceiver, now that the
2409 /// remote has said it strips trailers.
2410 ///
2411 /// Runs after the remote description has been applied, which is what makes it
2412 /// possible at all: libwebrtc creates a receiver's track while applying the
2413 /// description (the same point `on_track` fires), so both directions are
2414 /// reachable from a transceiver by the time this runs.
2415 ///
2416 /// Idempotent, and run from both `set_local_description` and
2417 /// `set_remote_description`: whichever of the two comes after the tracks were
2418 /// attached is the one that finds them. A slot installs its native transformer
2419 /// once and picks up a metadata step configured later on the next frame.
2420 ///
2421 /// Silent about failures on purpose. A transceiver with no track, a track whose
2422 /// Rust wrapper has already been dropped, or a native attach that declines —
2423 /// none of these are the caller's problem to handle, and none should fail
2424 /// applying a description. The consequence is only that metadata does not flow,
2425 /// which is the same as not having negotiated it.
2426 fn install_frame_metadata_transforms(&self) {
2427 if !self.frame_metadata_gate.is_open() {
2428 // Nothing to install. A step left over from a previous generation keeps
2429 // consulting the gate per frame, so a peer that dropped support stops
2430 // getting trailers without anything being detached here.
2431 return;
2432 }
2433 let pc_id = self.raw as usize;
2434 let transceivers = self.transceivers();
2435 // Prune slots for transceivers that libwebrtc stopped and freed internally
2436 // (ClearStoppedTransceivers) without going through our Drop path. Doing it
2437 // here closes the address-reuse window: a new transceiver at the same native
2438 // address would otherwise inherit a stale slot with installed=true and never
2439 // get a transformer of its own.
2440 let live_tc_ids: std::collections::HashSet<usize> =
2441 transceivers.iter().map(|tc| tc.transceiver_id()).collect();
2442 crate::sender_meta::prune_stale_slots(pc_id, &live_tc_ids);
2443 for tc in &transceivers {
2444 if tc.kind() != MediaKind::Video {
2445 continue;
2446 }
2447 // Composed, not exclusive: a caller's own transform on either side keeps
2448 // working, and attach_* returns a transformer to install only the first
2449 // time this side needs one.
2450 let id = tc.transceiver_id();
2451 // Per-track gate: a track created with frame_metadata off never
2452 // gets a trailer writer, whatever the connection negotiated.
2453 if crate::sender_meta::allowed(tc.sender_track_id()) {
2454 if let Some(source) = crate::sender_meta::lookup(tc.sender_track_id()) {
2455 if let Some(native) = crate::sender_meta::attach_embed(
2456 pc_id,
2457 id,
2458 source,
2459 self.frame_metadata_gate.clone(),
2460 ) {
2461 if tc
2462 .attach_native_transform(crate::sender_meta::Side::Send, &native)
2463 .is_err()
2464 {
2465 crate::sender_meta::release_install(
2466 pc_id,
2467 id,
2468 crate::sender_meta::Side::Send,
2469 );
2470 }
2471 }
2472 }
2473 }
2474 if let Some(queue) = crate::sender_meta::lookup_receiver(tc.receiver_track_id()) {
2475 if let Some(native) = crate::sender_meta::attach_strip(pc_id, id, queue) {
2476 if tc
2477 .attach_native_transform(crate::sender_meta::Side::Receive, &native)
2478 .is_err()
2479 {
2480 crate::sender_meta::release_install(
2481 pc_id,
2482 id,
2483 crate::sender_meta::Side::Receive,
2484 );
2485 }
2486 }
2487 }
2488 }
2489 }
2490
2491 /// Best-effort: apply [`Transceiver::try_lock_negotiated_send_codec`] to
2492 /// every video transceiver.
2493 ///
2494 /// Idempotent, and run from both `set_local_description` and
2495 /// `set_remote_description` for the same reason
2496 /// [`install_frame_metadata_transforms`](Self::install_frame_metadata_transforms)
2497 /// is: whichever of the two completes negotiation on this side is the one
2498 /// that finds a sender with a negotiated codec list to lock onto — the
2499 /// answerer's own sender is ready after its `set_local_description`, the
2500 /// offerer's only after the answer arrives via `set_remote_description`.
2501 ///
2502 /// Silent about failures on purpose, same as the metadata install: a
2503 /// transceiver with no preference set, no sender, or nothing negotiated
2504 /// yet on this call is not the caller's problem, and none of it should
2505 /// fail applying a description. Calling this again after the codec is
2506 /// already locked just re-confirms the same match.
2507 fn lock_negotiated_send_codecs(&self) {
2508 for tc in self.transceivers() {
2509 if tc.kind() != MediaKind::Video {
2510 continue;
2511 }
2512 tc.try_lock_negotiated_send_codec();
2513 }
2514 }
2515
2516 fn set_description(
2517 &self,
2518 sdp: &SessionDescription,
2519 local: bool,
2520 ) -> std::result::Result<(), ApplyError> {
2521 let ty = CString::new(sdp.kind.as_str()).unwrap();
2522 let body = CString::new(sdp.sdp.as_str())
2523 .map_err(|_| ApplyError::Rejected(Error::Webrtc("sdp contains a NUL byte".into())))?;
2524 let completed = await_complete(|ud| unsafe {
2525 if local {
2526 reactor_webrtc_sys::reactor_webrtc_peer_connection_set_local_description(
2527 self.raw,
2528 ty.as_ptr(),
2529 body.as_ptr(),
2530 ud,
2531 complete_cb,
2532 )
2533 } else {
2534 reactor_webrtc_sys::reactor_webrtc_peer_connection_set_remote_description(
2535 self.raw,
2536 ty.as_ptr(),
2537 body.as_ptr(),
2538 ud,
2539 complete_cb,
2540 )
2541 }
2542 });
2543 match completed {
2544 Some(r) => r.map_err(ApplyError::Rejected),
2545 None => Err(ApplyError::Unconfirmed(complete_timed_out())),
2546 }
2547 }
2548 /// Add a remote ICE candidate received out of band (trickle ICE).
2549 ///
2550 /// An empty [`IceCandidate::candidate`] string is the end-of-candidates
2551 /// marker (RFC 8838) and succeeds as a no-op rather than failing the
2552 /// candidate-string parse.
2553 pub fn add_ice_candidate(&self, candidate: &IceCandidate) -> Result<()> {
2554 let mid = CString::new(candidate.sdp_mid.clone().unwrap_or_default()).unwrap_or_default();
2555 let cand = CString::new(candidate.candidate.as_str())
2556 .map_err(|_| Error::Webrtc("candidate contains a NUL byte".into()))?;
2557 let idx = candidate.sdp_mline_index.unwrap_or(0) as c_int;
2558 run_complete(|ud| unsafe {
2559 reactor_webrtc_sys::reactor_webrtc_peer_connection_add_ice_candidate(
2560 self.raw,
2561 mid.as_ptr(),
2562 idx,
2563 cand.as_ptr(),
2564 ud,
2565 complete_cb,
2566 )
2567 })
2568 }
2569
2570 // ── Tracks / data channels ───────────────────────────────────────────────
2571 /// Add a local track (creates a sendrecv transceiver).
2572 ///
2573 /// Every track a peer publishes shares one MediaStream, so the remote can
2574 /// sync the audio it receives against the video.
2575 pub fn add_track(&self, track: &Track) -> Result<()> {
2576 let ok = unsafe {
2577 reactor_webrtc_sys::reactor_webrtc_peer_connection_add_track(self.raw, track.raw())
2578 };
2579 if ok == 1 {
2580 // Re-run after add_track so that an answerer that calls
2581 // set_remote_description → add_track → create_answer → set_local_description
2582 // does not have to wait for set_local_description to wire metadata.
2583 // Idempotent: a pre-negotiation call is a no-op (gate is still closed).
2584 self.install_frame_metadata_transforms();
2585 Ok(())
2586 } else {
2587 Err(Error::Webrtc("add_track failed".into()))
2588 }
2589 }
2590 /// Add a transceiver of `kind` with an explicit `direction` (e.g. recvonly
2591 /// to receive a remote track, sendonly to publish). Returns the transceiver
2592 /// so its `mid` can be read after `set_local_description`.
2593 pub fn add_transceiver(
2594 &self,
2595 kind: MediaKind,
2596 direction: TransceiverDirection,
2597 ) -> Result<Transceiver> {
2598 let media_kind = match kind {
2599 MediaKind::Audio => 0,
2600 MediaKind::Video => 1,
2601 MediaKind::Unknown => {
2602 return Err(Error::Webrtc("add_transceiver needs audio or video".into()))
2603 }
2604 };
2605 let raw = unsafe {
2606 reactor_webrtc_sys::reactor_webrtc_peer_connection_add_transceiver(
2607 self.raw,
2608 media_kind,
2609 direction.to_raw(),
2610 )
2611 };
2612 if raw.is_null() {
2613 Err(Error::Webrtc("add_transceiver failed".into()))
2614 } else {
2615 Ok(Transceiver::from_raw(
2616 raw,
2617 self.raw as usize,
2618 self.frame_metadata_gate.clone(),
2619 ))
2620 }
2621 }
2622
2623 /// All transceivers on this peer connection. After negotiation this includes
2624 /// transceivers auto-created from the remote description — use this to reach
2625 /// a receiving transceiver (e.g. to attach an encoded-frame transform to its
2626 /// receiver: match on [`Transceiver::kind`] and call
2627 /// [`Transceiver::set_receiver_transform`]).
2628 pub fn transceivers(&self) -> Vec<Transceiver> {
2629 let n = unsafe {
2630 reactor_webrtc_sys::reactor_webrtc_peer_connection_transceiver_count(self.raw)
2631 };
2632 (0..n)
2633 .filter_map(|i| {
2634 let raw = unsafe {
2635 reactor_webrtc_sys::reactor_webrtc_peer_connection_get_transceiver(self.raw, i)
2636 };
2637 (!raw.is_null()).then(|| {
2638 Transceiver::from_raw(raw, self.raw as usize, self.frame_metadata_gate.clone())
2639 })
2640 })
2641 .collect()
2642 }
2643
2644 /// Create an SDP-negotiated data channel.
2645 pub fn create_data_channel(&self, label: &str) -> Result<DataChannel> {
2646 let label =
2647 CString::new(label).map_err(|_| Error::Webrtc("label has a NUL byte".into()))?;
2648 let raw = unsafe {
2649 reactor_webrtc_sys::reactor_webrtc_peer_connection_create_data_channel(
2650 self.raw,
2651 label.as_ptr(),
2652 )
2653 };
2654 if raw.is_null() {
2655 Err(Error::Webrtc("create_data_channel returned null".into()))
2656 } else {
2657 Ok(DataChannel::from_raw(raw, Arc::clone(&self._factory))
2658 .with_dc_negotiation(Arc::clone(&self.dc_negotiation)))
2659 }
2660 }
2661
2662 // ── Stats ────────────────────────────────────────────────────────────────
2663
2664 /// Collect a stats snapshot from this peer connection.
2665 ///
2666 /// Blocks the current thread (up to [`OP_TIMEOUT`]) until the WebRTC
2667 /// engine delivers the report. The returned [`StatsReport`] contains only
2668 /// the three stat types surfaced through the C ABI:
2669 ///
2670 /// - [`StatsReport::inbound_rtp`] — per-SSRC receive statistics
2671 /// (packets, jitter, NACK count, decode time).
2672 /// - [`StatsReport::outbound_rtp`] — per-SSRC send statistics
2673 /// (bytes sent, target bitrate, RTT).
2674 /// - [`StatsReport::candidate_pairs`] — ICE candidate pair state and RTT.
2675 pub fn get_stats(&self) -> Result<StatsReport> {
2676 let mut report = run_stats(|ud| unsafe {
2677 reactor_webrtc_sys::reactor_webrtc_peer_connection_get_stats(self.raw, ud, stats_cb)
2678 })?;
2679 report.sctp = self.sctp_stats();
2680 Ok(report)
2681 }
2682
2683 /// The SCTP association's metrics, read on the network thread and waited
2684 /// for no longer than the stats report is. `None` when there are none, or
2685 /// when the network thread does not answer in time.
2686 fn sctp_stats(&self) -> Option<SctpStats> {
2687 let (tx, rx) = sync_channel::<Option<SctpStats>>(1);
2688 let tx = Arc::new(tx);
2689 let p = Arc::into_raw(tx.clone());
2690 unsafe {
2691 reactor_webrtc_sys::reactor_webrtc_peer_connection_sctp_metrics(
2692 self.raw,
2693 p as *mut c_void,
2694 sctp_cb,
2695 )
2696 };
2697 let r = rx.recv_timeout(OP_TIMEOUT);
2698 drop(tx);
2699 r.ok().flatten()
2700 }
2701
2702 /// Set aggregate bitrate limits on the peer connection.
2703 ///
2704 /// Each parameter is optional; pass `None` to keep the libwebrtc default
2705 /// for that field. All values are in bits per second.
2706 ///
2707 /// # Parameters
2708 ///
2709 /// - `min_bps` — floor handed to the congestion controller; it will not
2710 /// drop below this even when the network estimate is very low.
2711 /// - `start_bps` — initial encoder target. libwebrtc's built-in default
2712 /// is ~300 kbps, which causes a visible quality ramp-up on new
2713 /// connections. Set this close to your expected steady-state bitrate
2714 /// (e.g. `Some(4_000_000)` for a 4 Mbps stream) to reach quality
2715 /// quickly.
2716 /// - `max_bps` — ceiling; the GCC algorithm will not allocate above this.
2717 ///
2718 /// Can be called at any time after the peer connection is created,
2719 /// including after negotiation.
2720 pub fn set_bitrate(
2721 &self,
2722 min_bps: Option<i32>,
2723 start_bps: Option<i32>,
2724 max_bps: Option<i32>,
2725 ) -> crate::Result<()> {
2726 let mut err = [0 as std::os::raw::c_char; 256];
2727 let rc = unsafe {
2728 reactor_webrtc_sys::reactor_webrtc_peer_connection_set_bitrate(
2729 self.raw,
2730 min_bps.unwrap_or(-1),
2731 start_bps.unwrap_or(-1),
2732 max_bps.unwrap_or(-1),
2733 err.as_mut_ptr(),
2734 err.len() as std::os::raw::c_int,
2735 )
2736 };
2737 if rc != 0 {
2738 let reason = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }
2739 .to_string_lossy()
2740 .into_owned();
2741 return Err(crate::Error::Webrtc(if reason.is_empty() {
2742 "set_bitrate failed".into()
2743 } else {
2744 reason
2745 }));
2746 }
2747 Ok(())
2748 }
2749}
2750
2751impl Drop for PeerConnection {
2752 fn drop(&mut self) {
2753 // Release the composed transform slots first, while the transceivers can
2754 // still be enumerated. Keyed by (pc_id, tc_id), so leaving them would let a
2755 // recycled pointer on the same or another connection inherit stale callbacks.
2756 let pc_id = self.raw as usize;
2757 for tc in self.transceivers() {
2758 crate::sender_meta::forget_transceiver(pc_id, tc.transceiver_id());
2759 }
2760 // Destroy the native PC (stops callbacks) before the observer box drops.
2761 unsafe { reactor_webrtc_sys::reactor_webrtc_peer_connection_destroy(self.raw) }
2762 }
2763}
2764
2765#[cfg(test)]
2766mod codec_implementation_tests {
2767 use super::*;
2768
2769 fn buf(bytes: &[u8]) -> [c_char; 128] {
2770 let mut out = [0 as c_char; 128];
2771 for (o, &b) in out.iter_mut().zip(bytes) {
2772 *o = b as c_char;
2773 }
2774 out
2775 }
2776
2777 #[test]
2778 fn a_name_that_fits_reads_back_whole() {
2779 assert_eq!(
2780 stat_str_prefix(&buf(b"VideoToolbox")).as_deref(),
2781 Some("VideoToolbox")
2782 );
2783 }
2784
2785 #[test]
2786 fn an_empty_name_is_none() {
2787 assert_eq!(stat_str_prefix(&buf(b"")), None);
2788 }
2789
2790 #[test]
2791 fn a_character_the_cut_split_is_dropped() {
2792 // "é" is two bytes; the C side kept only the first of them.
2793 let cut = [b"SimulcastEncoderAdapter (caf".as_slice(), &[0xC3]].concat();
2794 assert_eq!(
2795 stat_str_prefix(&buf(&cut)).as_deref(),
2796 Some("SimulcastEncoderAdapter (caf")
2797 );
2798 }
2799
2800 #[test]
2801 fn a_name_cut_to_the_buffer_keeps_its_prefix() {
2802 let long = format!(
2803 "SimulcastEncoderAdapter ({})",
2804 ["VideoToolbox"; 10].join(", ")
2805 );
2806 let mut bytes = long.as_bytes()[..127].to_vec();
2807 bytes.push(0);
2808 let name = stat_str_prefix(&buf(&bytes)).expect("a name");
2809 assert_eq!(name.len(), 127);
2810 assert!(long.starts_with(&name));
2811 }
2812
2813 #[test]
2814 fn the_power_efficient_flag_is_tri_state() {
2815 assert_eq!(power_efficient(-1), None);
2816 assert_eq!(power_efficient(0), Some(false));
2817 assert_eq!(power_efficient(1), Some(true));
2818 }
2819}
2820
2821#[cfg(test)]
2822mod data_channel_label_tests {
2823 use super::*;
2824
2825 fn entry(label: &[u8], present: bool) -> ReactorStatEntry {
2826 // SAFETY: every field is a plain number or byte array.
2827 let mut e: ReactorStatEntry = unsafe { std::mem::zeroed() };
2828 for (dst, &b) in e.data_channel_label.iter_mut().zip(label) {
2829 *dst = b as c_char;
2830 }
2831 e.data_channel_label_present = present as u8;
2832 e
2833 }
2834
2835 #[test]
2836 fn an_empty_label_is_a_label() {
2837 assert_eq!(data_channel_label(&entry(b"", true)), Some(String::new()));
2838 }
2839
2840 #[test]
2841 fn a_label_reads_as_itself() {
2842 assert_eq!(
2843 data_channel_label(&entry(b"chat", true)),
2844 Some("chat".into())
2845 );
2846 }
2847
2848 #[test]
2849 fn an_absent_or_overlong_label_is_none() {
2850 assert_eq!(data_channel_label(&entry(b"", false)), None);
2851 }
2852}
2853
2854#[cfg(test)]
2855mod sdp_ice_credentials_tests {
2856 use super::*;
2857
2858 /// A two-section bundled description, as libwebrtc emits one.
2859 fn bundled() -> SessionDescription {
2860 SessionDescription {
2861 kind: SdpType::Answer,
2862 sdp: concat!(
2863 "v=0\r\n",
2864 "o=- 1 2 IN IP4 127.0.0.1\r\n",
2865 "s=-\r\n",
2866 "t=0 0\r\n",
2867 "a=group:BUNDLE 0 1\r\n",
2868 "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n",
2869 "a=mid:0\r\n",
2870 "a=ice-ufrag:jHFv\r\n",
2871 "a=ice-pwd:0123456789012345678901\r\n",
2872 "a=fingerprint:sha-256 AA:BB\r\n",
2873 "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n",
2874 "a=mid:1\r\n",
2875 "a=ice-ufrag:jHFv\r\n",
2876 "a=ice-pwd:0123456789012345678901\r\n",
2877 "a=fingerprint:sha-256 AA:BB\r\n",
2878 )
2879 .to_string(),
2880 }
2881 }
2882
2883 const UFRAG: &str = "CgAHFcpsamqt/IIl8YtGLBP8al/dIA";
2884 const PWD: &str = "iMyV3ZlbyUC8SBiy/AeG2OVaSJ5di54s";
2885
2886 #[test]
2887 fn replaces_every_section_not_just_the_first() {
2888 // Replacing one and leaving the other would produce an SDP that is
2889 // inconsistent rather than substituted, and bundled sections must agree.
2890 let out = bundled().with_ice_credentials(UFRAG, PWD).unwrap();
2891 assert_eq!(out.ice_ufrags(), vec![UFRAG, UFRAG]);
2892 assert_eq!(out.sdp.matches(&format!("a=ice-pwd:{PWD}")).count(), 2);
2893 assert!(!out.sdp.contains("jHFv"));
2894 }
2895
2896 #[test]
2897 fn leaves_everything_else_byte_for_byte() {
2898 let out = bundled().with_ice_credentials(UFRAG, PWD).unwrap();
2899 for keep in [
2900 "a=group:BUNDLE 0 1",
2901 "a=fingerprint:sha-256 AA:BB",
2902 "m=video 9 UDP/TLS/RTP/SAVPF 96",
2903 "a=mid:1",
2904 ] {
2905 assert!(out.sdp.contains(keep), "lost {keep:?}");
2906 }
2907 assert_eq!(out.sdp.lines().count(), bundled().sdp.lines().count());
2908 assert_eq!(out.kind, bundled().kind);
2909 }
2910
2911 #[test]
2912 fn the_fingerprint_survives_untouched() {
2913 // Load-bearing: DTLS is what keeps a relay out of the media, and it is
2914 // authenticated by this line. Rewriting it would silently break end-to-end
2915 // encryption rather than fail loudly.
2916 let before = bundled();
2917 let after = before.with_ice_credentials(UFRAG, PWD).unwrap();
2918 let fp = |s: &SessionDescription| -> Vec<String> {
2919 s.sdp
2920 .lines()
2921 .filter(|l| l.starts_with("a=fingerprint:"))
2922 .map(str::to_string)
2923 .collect()
2924 };
2925 assert_eq!(fp(&before), fp(&after));
2926 }
2927
2928 #[test]
2929 fn output_is_crlf_terminated() {
2930 let out = bundled().with_ice_credentials(UFRAG, PWD).unwrap();
2931 assert!(out.sdp.ends_with("\r\n"));
2932 assert_eq!(
2933 out.sdp.matches('\n').count(),
2934 out.sdp.matches("\r\n").count()
2935 );
2936 }
2937
2938 #[test]
2939 fn normalises_bare_lf_input_to_crlf() {
2940 let lf = SessionDescription {
2941 kind: SdpType::Offer,
2942 sdp: "v=0\na=ice-ufrag:jHFv\na=ice-pwd:0123456789012345678901\n".into(),
2943 };
2944 let out = lf.with_ice_credentials(UFRAG, PWD).unwrap();
2945 assert_eq!(out.sdp.matches("\r\n").count(), 3);
2946 }
2947
2948 #[test]
2949 fn rejects_a_ufrag_that_is_too_short_or_too_long() {
2950 let d = bundled();
2951 assert!(d.with_ice_credentials("abc", PWD).is_err());
2952 assert!(d.with_ice_credentials(&"a".repeat(257), PWD).is_err());
2953 assert!(d.with_ice_credentials(&"a".repeat(4), PWD).is_ok());
2954 assert!(d.with_ice_credentials(&"a".repeat(256), PWD).is_ok());
2955 }
2956
2957 #[test]
2958 fn rejects_a_password_below_the_rfc_minimum() {
2959 let d = bundled();
2960 assert!(d.with_ice_credentials(UFRAG, &"a".repeat(21)).is_err());
2961 assert!(d.with_ice_credentials(UFRAG, &"a".repeat(22)).is_ok());
2962 }
2963
2964 #[test]
2965 fn rejects_characters_outside_ice_char() {
2966 // Rejecting here keeps the failure attributable. Passed through, these
2967 // surface much later as a generic invalid-parameter error from libwebrtc.
2968 let d = bundled();
2969 for bad in [
2970 "has space",
2971 "has=equals",
2972 "has\r\ninjected:line",
2973 "acentuação",
2974 ] {
2975 assert!(
2976 d.with_ice_credentials(bad, PWD).is_err(),
2977 "{bad:?} was accepted as a ufrag"
2978 );
2979 }
2980 assert!(d
2981 .with_ice_credentials(UFRAG, "short but has spaces!!")
2982 .is_err());
2983 }
2984
2985 #[test]
2986 fn a_crlf_in_a_credential_cannot_inject_an_sdp_line() {
2987 // The alphabet check is what prevents this, and it is worth asserting
2988 // directly rather than trusting it as a side effect.
2989 let d = bundled();
2990 assert!(d
2991 .with_ice_credentials("aaaa\r\na=candidate:injected", PWD)
2992 .is_err());
2993 }
2994
2995 #[test]
2996 fn refuses_a_description_with_no_credentials_to_replace() {
2997 let empty = SessionDescription {
2998 kind: SdpType::Offer,
2999 sdp: "v=0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n".into(),
3000 };
3001 assert!(empty.with_ice_credentials(UFRAG, PWD).is_err());
3002 }
3003
3004 #[test]
3005 fn ice_ufrags_reads_each_section() {
3006 assert_eq!(bundled().ice_ufrags(), vec!["jHFv", "jHFv"]);
3007 let none = SessionDescription {
3008 kind: SdpType::Offer,
3009 sdp: "v=0\r\n".into(),
3010 };
3011 assert!(none.ice_ufrags().is_empty());
3012 }
3013}
3014
3015#[cfg(test)]
3016mod sdp_frame_metadata_tests {
3017 use super::*;
3018 use crate::metadata::{FRAME_METADATA_ATTRIBUTE, FRAME_METADATA_VERSION};
3019
3020 fn declaration() -> String {
3021 format!("a={FRAME_METADATA_ATTRIBUTE}:{FRAME_METADATA_VERSION}")
3022 }
3023
3024 /// A bundled audio+video description, as libwebrtc emits one, with `extra`
3025 /// spliced in at session level (after `t=`, before the first `m=`).
3026 fn described(extra: &str) -> SessionDescription {
3027 SessionDescription {
3028 kind: SdpType::Offer,
3029 sdp: format!(
3030 "v=0\r\n\
3031 o=- 1 2 IN IP4 127.0.0.1\r\n\
3032 s=-\r\n\
3033 t=0 0\r\n\
3034 a=group:BUNDLE 0 1\r\n\
3035 {extra}\
3036 m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
3037 c=IN IP4 0.0.0.0\r\n\
3038 a=mid:0\r\n\
3039 m=video 9 UDP/TLS/RTP/SAVPF 96\r\n\
3040 c=IN IP4 0.0.0.0\r\n\
3041 a=mid:1\r\n\
3042 a=fingerprint:sha-256 AA:BB\r\n"
3043 ),
3044 }
3045 }
3046
3047 fn bundled() -> SessionDescription {
3048 described("")
3049 }
3050
3051 #[test]
3052 fn declares_once_at_session_level() {
3053 let out = bundled().with_frame_metadata();
3054 assert!(out.declares_frame_metadata());
3055 assert_eq!(out.sdp.matches(&declaration()).count(), 1);
3056 }
3057
3058 #[test]
3059 fn inserted_before_the_first_media_section() {
3060 // RFC 8866 §5 puts session-level attributes after t=/z=/k= and before the
3061 // first media description; everything before the first m= is session level.
3062 let out = bundled().with_frame_metadata();
3063 let lines: Vec<&str> = out.sdp.lines().collect();
3064 let at = |needle: &str| lines.iter().position(|l| l.starts_with(needle)).unwrap();
3065 assert!(at("t=") < at("a=x-reactor-frame-metadata:"));
3066 assert!(at("a=x-reactor-frame-metadata:") < at("m="));
3067 }
3068
3069 #[test]
3070 fn declares_on_an_audio_only_description() {
3071 // Session level, so there is nothing about video to condition it on — and a
3072 // renegotiation that adds video must not have to introduce the capability.
3073 let audio_only = SessionDescription {
3074 kind: SdpType::Offer,
3075 sdp: "v=0\r\nt=0 0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\na=mid:0\r\n".into(),
3076 };
3077 let out = audio_only.with_frame_metadata();
3078 assert!(out.declares_frame_metadata());
3079 let lines: Vec<&str> = out.sdp.lines().collect();
3080 let declared = lines
3081 .iter()
3082 .position(|l| l.starts_with("a=x-reactor-frame-metadata:"))
3083 .expect("declaration");
3084 let first_media = lines.iter().position(|l| l.starts_with("m=")).expect("m=");
3085 assert!(
3086 declared < first_media,
3087 "declaration landed inside a media section"
3088 );
3089 }
3090
3091 #[test]
3092 fn declares_on_a_description_with_no_media_section() {
3093 let no_media = SessionDescription {
3094 kind: SdpType::Offer,
3095 sdp: "v=0\r\nt=0 0\r\n".into(),
3096 };
3097 assert!(no_media.with_frame_metadata().declares_frame_metadata());
3098 }
3099
3100 #[test]
3101 fn an_empty_description_is_left_alone() {
3102 // Emitting a lone attribute line would be invalid SDP, and there is nothing
3103 // useful to declare it on.
3104 let empty = SessionDescription {
3105 kind: SdpType::Offer,
3106 sdp: String::new(),
3107 };
3108 let out = empty.with_frame_metadata();
3109 assert!(out.sdp.is_empty());
3110 assert!(!out.declares_frame_metadata());
3111 }
3112
3113 #[test]
3114 fn is_idempotent() {
3115 let once = bundled().with_frame_metadata();
3116 let twice = once.with_frame_metadata();
3117 assert_eq!(once.sdp, twice.sdp);
3118 assert_eq!(once.sdp.matches(&declaration()).count(), 1);
3119 }
3120
3121 #[test]
3122 fn a_different_version_reads_as_unsupported() {
3123 // The version is the compatibility token: a peer speaking a future trailer
3124 // format must not look like a peer speaking this one.
3125 let future = described(&format!(
3126 "a={FRAME_METADATA_ATTRIBUTE}:{}\r\n",
3127 FRAME_METADATA_VERSION + 1
3128 ));
3129 assert!(!future.declares_frame_metadata());
3130 // …and declaring ours alongside it works.
3131 let out = future.with_frame_metadata();
3132 assert!(out.declares_frame_metadata());
3133 }
3134
3135 #[test]
3136 fn a_malformed_version_reads_as_unsupported() {
3137 for bad in ["", "abc", "1.0", "-1"] {
3138 let d = described(&format!("a={FRAME_METADATA_ATTRIBUTE}:{bad}\r\n"));
3139 assert!(
3140 !d.declares_frame_metadata(),
3141 "accepted version {bad:?} as ours"
3142 );
3143 }
3144 }
3145
3146 #[test]
3147 fn a_similar_attribute_name_does_not_match() {
3148 let d = described(&format!(
3149 "a={FRAME_METADATA_ATTRIBUTE}-2:{FRAME_METADATA_VERSION}\r\n"
3150 ));
3151 assert!(!d.declares_frame_metadata());
3152 }
3153
3154 #[test]
3155 fn leaves_everything_else_intact() {
3156 let before = bundled();
3157 let after = before.with_frame_metadata();
3158 for keep in [
3159 "a=group:BUNDLE 0 1",
3160 "m=audio 9 UDP/TLS/RTP/SAVPF 111",
3161 "m=video 9 UDP/TLS/RTP/SAVPF 96",
3162 "a=mid:1",
3163 "a=fingerprint:sha-256 AA:BB",
3164 ] {
3165 assert!(after.sdp.contains(keep), "lost {keep:?}");
3166 }
3167 assert_eq!(after.sdp.lines().count(), before.sdp.lines().count() + 1);
3168 assert_eq!(after.kind, before.kind);
3169 }
3170
3171 #[test]
3172 fn output_is_crlf_terminated() {
3173 let out = bundled().with_frame_metadata();
3174 assert!(out.sdp.ends_with("\r\n"));
3175 assert_eq!(
3176 out.sdp.matches('\n').count(),
3177 out.sdp.matches("\r\n").count()
3178 );
3179 }
3180}
3181
3182#[cfg(test)]
3183mod frame_metadata_gate_tests {
3184 use crate::metadata::FrameMetadataGate;
3185
3186 #[test]
3187 fn starts_closed() {
3188 // Closed-by-default is the safe direction: a sender that has not yet applied
3189 // a remote description must not append trailers.
3190 assert!(!FrameMetadataGate::new().is_open());
3191 }
3192
3193 #[test]
3194 fn clones_share_one_state() {
3195 let gate = FrameMetadataGate::new();
3196 let handed_to_transform = gate.clone();
3197 gate.set(true);
3198 assert!(handed_to_transform.is_open());
3199 // A renegotiation where the peer drops support closes it again.
3200 gate.set(false);
3201 assert!(!handed_to_transform.is_open());
3202 }
3203}