Skip to main content

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}