Skip to main content

media_pp/elements/driver/webrtc/
peer.rs

1use std::{
2    collections::HashMap,
3    net::UdpSocket,
4    sync::{
5        Arc, Mutex,
6        atomic::{AtomicU64, Ordering},
7    },
8    time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_error, pp_info, pp_warn};
12use crossbeam_channel::{Receiver, Sender, TrySendError, bounded, unbounded};
13use ffmpeg_next as ffmpeg;
14use str0m::{
15    Event, Input, Output, Rtc,
16    change::{SdpOffer, SdpPendingOffer},
17    format::Codec,
18    media::{Direction, MediaKind, MediaTime, Mid},
19    net::{Protocol, Receive},
20};
21
22use crate::{
23    buffer::MediaBuffer,
24    bus::{Bus, BusEvent},
25    driver::{Driver, StopReceiver},
26    element::{Element, ElementType, element_pp_log},
27    error::Result,
28    time::{InvalidTimeBase, MediaTimestamp},
29};
30
31use super::{
32    command::{Command, TrackId, TrackOutState, WebRtcError},
33    stream_info::{StreamInfoProbe, WebRtcStreamInfo},
34    track::{AttachedTrack, TrackEndpoints, WebRtcHandle, WebRtcTrackSink, WebRtcTrackSource},
35};
36
37/// How often `WebRtcPeer::run` re-checks `stop`/its command channel while
38/// otherwise blocked on the UDP socket — see its own docs for why this is
39/// polling rather than a true multi-way wait.
40const POLL_INTERVAL: Duration = Duration::from_millis(20);
41
42/// Bound on the command channel (see [`Command`]) and on each attached
43/// track's inbound buffer (`WebRtcPeer` -> its `WebRtcTrackSource`). Once
44/// this many media buffers are backed up, the newest one is dropped
45/// instead of piling up in memory forever — the right call for live media,
46/// where a backed-up peer means falling behind, not something worth
47/// buffering indefinitely for (same reasoning as
48/// [`crate::queue::OverflowPolicy::DropNewest`]). Control traffic on the
49/// same channel (`AddTrack`/`SetAnswer`/`AcceptOffer`) is never dropped for
50/// capacity pressure — those call sites block on plain `send` instead of
51/// `try_send`. A disconnected peer still rejects the command; `set_answer`
52/// intentionally treats that case as a no-op.
53const CHANNEL_CAPACITY: usize = 128;
54/// The [`Driver`] — owns the [`Rtc`] session and its [`UdpSocket`], and
55/// drives str0m's sans-I/O poll loop on the dedicated thread
56/// [`crate::driver::DriverRunner::run`] gives it. Not a
57/// [`crate::element::SourceElement`]/[`crate::element::Source`]: it has no `src_pads()`
58/// dataflow graph of its own — see [`Driver`]'s own docs for why a
59/// connection with dynamically-appearing, independently bidirectional
60/// tracks doesn't fit that shape. Whatever it produces or consumes flows
61/// through the separate [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pairs it
62/// mints per track instead (see below).
63///
64/// `rtc`/`socket` must already be connected: the initial SDP offer/answer
65/// and ICE candidate setup happen via str0m directly, in the caller's own
66/// code, *before* [`WebRtcPeer::new`]. `WebRtcPeer` only takes over after
67/// signaling has established the connection; it does not provide a signaling
68/// server itself.
69///
70/// Every track — whether it's one this side requested via
71/// [`WebRtcHandle::add_track`] or one the remote peer added (`str0m`'s
72/// `Event::MediaAdded`, which — critically — *never fires for a track this
73/// side added itself*) — is attached the same way, the moment its `Mid`
74/// exists: a [`TrackEndpoints`] is minted from the track's negotiated
75/// direction and handed out through [`WebRtcHandle::next_track`], no
76/// closure required. A single `Direction::SendRecv` track therefore needs
77/// exactly one [`WebRtcHandle::add_track`] call (on either side) and one
78/// `next_track()` on *each* side — no separate outbound API, and no
79/// special-casing for which side happened to originate it.
80///
81/// What that direction allows is decided once, here, rather than left to
82/// the caller to observe: a `SendOnly` track yields a [`WebRtcTrackSink`]
83/// and no source, a `RecvOnly` one a [`WebRtcTrackSource`] and no sink.
84/// The endpoints are never re-issued, so a remote peer that renegotiates
85/// a different direction afterwards invalidates what the caller is
86/// holding; that is reported as
87/// [`WebRtcError::DirectionChanged`] on the [`Bus`] rather than quietly
88/// changing behavior underneath it.
89///
90/// Attachment itself is the same idea as
91/// [`crate::elements::TeeHandle::attach`]'s dynamic attachment, just
92/// without `Tee`'s `Mutex` (nothing but this one thread ever touches
93/// `tracks_in`).
94pub struct WebRtcPeer {
95    pp_log: PpLog,
96    name: Arc<str>,
97    rtc: Rtc,
98    socket: UdpSocket,
99    /// Where inbound data for each attached track goes: just a plain
100    /// `Sender`, not a `Box<dyn Sink>` — the matching `Receiver` lives
101    /// inside that track's own [`WebRtcTrackSource`], driven by *its own*
102    /// `Pipeline` on its own thread, so nothing here needs to know about
103    /// `ControlMsg` at all. Its codec cell is the same
104    /// `WebRtcTrackSource`'s [`WebRtcTrackSource::codec`] cell — written
105    /// here (from `Event::MediaData`), read there, from whatever thread the
106    /// caller checks it on. A separate one-slot channel confirms enough actual
107    /// payload information for `wait_stream_info` (including received H.264
108    /// SPS/PPS); both are shared across threads, while the map itself still
109    /// isn't (see below).
110    tracks_in: HashMap<Mid, TrackInState>,
111    tracks_out: HashMap<TrackId, TrackOutState>,
112    /// Pending outbound codec selections for locally-requested tracks. Each entry
113    /// moves into that track's [`WebRtcTrackSink`] when it attaches; a
114    /// remotely-added track has no such declaration and its caller supplies
115    /// one through [`WebRtcTrackSink::set_source_parameters`] — or
116    /// [`WebRtcTrackSink::set_codec`], with no parameters to hand — validated
117    /// against the endpoint's negotiated codec list before pushing packets.
118    track_codec: HashMap<TrackId, Codec>,
119    /// The currently negotiated codec families for each attached media
120    /// section, shared with both endpoints. A locally-created track is
121    /// attached while its offer is pending, so [`Command::SetAnswer`]
122    /// refreshes this cell after applying the answer.
123    negotiated_codecs: HashMap<Mid, Arc<Mutex<Vec<Codec>>>>,
124    /// The direction each attached track was handed out with, so
125    /// `Event::MediaChanged` can tell an actual renegotiation apart from a
126    /// re-announcement of the direction already in force. Keyed by `Mid`
127    /// because that is what the event carries.
128    track_direction: HashMap<Mid, Direction>,
129    /// Set when str0m reports the remote DTLS/SCTP connection closing. The
130    /// current `drive_until_timeout` call is still allowed to drain any
131    /// reciprocal protocol output before the run loop tears down tracks.
132    remote_closed: bool,
133    /// The one SDP exchange currently in flight (str0m only allows one at a
134    /// time — see `chat.rs`'s own `pending.is_some()` guard), plus which
135    /// `TrackId`s it covers, so [`Command::SetAnswer`] knows which entries
136    /// in `tracks_out` to flip from `Negotiating` to `Open`.
137    pending: Option<(SdpPendingOffer, Vec<TrackId>)>,
138    /// Shared with every [`WebRtcHandle`] clone, so `TrackId`s minted here
139    /// (for tracks the *remote* peer added — see the type docs) never
140    /// collide with ones `WebRtcHandle::add_track` mints.
141    next_id: Arc<AtomicU64>,
142    /// Cloned into every [`WebRtcTrackSink`] this element hands out via
143    /// [`WebRtcPeer::attach_track`] — including for tracks *this* side
144    /// requested, since `WebRtcTrackSink` is otherwise only ever
145    /// constructed from inside `run`.
146    command_tx: Sender<Command>,
147    command_rx: Receiver<Command>,
148    /// The other half of [`WebRtcHandle::next_track`] — one entry per
149    /// newly-attached track, in attachment order (see [`TrackId`]'s own
150    /// docs for why the caller has to match on it, not just take these in
151    /// order, when more than one track can appear).
152    new_track_tx: Sender<AttachedTrack>,
153    on_offer: Box<dyn FnMut(SdpOffer) + Send>,
154    on_keyframe_request: Box<dyn FnMut(TrackId) + Send>,
155}
156
157struct TrackInState {
158    data_tx: Sender<MediaBuffer>,
159    codec: Arc<Mutex<Option<Codec>>>,
160    stream_info_tx: Sender<WebRtcStreamInfo>,
161    stream_info_probe: StreamInfoProbe,
162    stream_info_sent: bool,
163}
164
165impl WebRtcPeer {
166    /// `rtc`/`socket` must already be connected — see the type-level docs.
167    /// `on_offer` receives every renegotiation offer this element generates
168    /// (via [`WebRtcHandle::add_track`]) for the caller to ship over its
169    /// own signaling transport; `on_keyframe_request` reports which
170    /// outbound track the remote peer wants a keyframe for (forward this to
171    /// whatever's encoding that track). Newly-attached tracks themselves
172    /// come from [`WebRtcHandle::next_track`], not a constructor argument.
173    pub fn new(
174        name: impl Into<String>,
175        rtc: Rtc,
176        socket: UdpSocket,
177        on_offer: impl FnMut(SdpOffer) + Send + 'static,
178        on_keyframe_request: impl FnMut(TrackId) + Send + 'static,
179    ) -> (Self, WebRtcHandle) {
180        let name: Arc<str> = name.into().into();
181        let pp_log = element_pp_log(ElementType::WebRtcPeer, &name, None);
182        pp_info!(
183            pp_log: &pp_log,
184            "created: local_addr={:?}",
185            socket.local_addr()
186        );
187        let (command_tx, command_rx) = bounded(CHANNEL_CAPACITY);
188        let (new_track_tx, new_track_rx) = unbounded();
189        let next_id = Arc::new(AtomicU64::new(0));
190        (
191            Self {
192                name,
193                pp_log,
194                rtc,
195                socket,
196                tracks_in: HashMap::new(),
197                tracks_out: HashMap::new(),
198                track_codec: HashMap::new(),
199                negotiated_codecs: HashMap::new(),
200                track_direction: HashMap::new(),
201                remote_closed: false,
202                pending: None,
203                next_id: next_id.clone(),
204                command_tx: command_tx.clone(),
205                command_rx,
206                new_track_tx,
207                on_offer: Box::new(on_offer),
208                on_keyframe_request: Box::new(on_keyframe_request),
209            },
210            WebRtcHandle {
211                next_id,
212                command_tx,
213                new_track_rx,
214            },
215        )
216    }
217
218    /// Mints a fresh [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pair for
219    /// `mid`/`kind` and hands both out via [`WebRtcHandle::next_track`] —
220    /// see the type docs for why this is the one path both locally- and
221    /// remotely-added tracks go through.
222    pub(super) fn attach_track(
223        &mut self,
224        id: TrackId,
225        mid: Mid,
226        kind: MediaKind,
227        direction: Direction,
228    ) {
229        pp_info!(
230            self,
231            "track attached: id={id:?}, mid={mid}, kind={kind:?}, direction={direction:?}"
232        );
233        self.track_direction.insert(mid, direction);
234        let negotiated_codecs = Arc::new(Mutex::new(self.codecs_for_mid(mid)));
235        self.negotiated_codecs
236            .insert(mid, negotiated_codecs.clone());
237
238        // Only build the inbound half when the direction can actually
239        // deliver on it: `tracks_in` is what `Event::MediaData` routes
240        // through, so leaving a receive-less track out of it is also what
241        // makes a stray inbound packet on it visibly a dropped one rather
242        // than something feeding a source nobody was given.
243        let outbound_codec = self.track_codec.remove(&id);
244        let sink = direction.is_sending().then(|| {
245            WebRtcTrackSink::new(
246                id,
247                kind,
248                outbound_codec,
249                negotiated_codecs.clone(),
250                self.command_tx.clone(),
251            )
252        });
253        let source = direction.is_receiving().then(|| {
254            let (tx, rx) = bounded(CHANNEL_CAPACITY);
255            let (stream_info_tx, stream_info_rx) = bounded(1);
256            let codec = Arc::new(Mutex::new(None));
257            self.tracks_in.insert(
258                mid,
259                TrackInState {
260                    data_tx: tx,
261                    codec: codec.clone(),
262                    stream_info_tx,
263                    stream_info_probe: StreamInfoProbe::new(),
264                    stream_info_sent: false,
265                },
266            );
267            WebRtcTrackSource::new(
268                id,
269                kind,
270                format!("webrtc-track-{}-in", id.0),
271                rx,
272                codec,
273                negotiated_codecs.clone(),
274                stream_info_rx,
275            )
276        });
277
278        let endpoints = match (sink, source) {
279            (Some(sink), Some(source)) => TrackEndpoints::SendRecv(sink, source),
280            (Some(sink), None) => TrackEndpoints::Send(sink),
281            (None, Some(source)) => TrackEndpoints::Recv(source),
282            (None, None) => TrackEndpoints::Inactive,
283        };
284        let _ = self.new_track_tx.send(AttachedTrack {
285            id,
286            mid,
287            kind,
288            endpoints,
289        });
290    }
291
292    fn codecs_for_mid(&mut self, mid: Mid) -> Vec<Codec> {
293        let Some(writer) = self.rtc.writer(mid) else {
294            return Vec::new();
295        };
296        let mut codecs = Vec::new();
297        for codec in writer.payload_params().map(|params| params.spec().codec) {
298            if !codecs.contains(&codec) {
299                codecs.push(codec);
300            }
301        }
302        codecs
303    }
304
305    fn refresh_codecs(&mut self, mid: Mid) {
306        let codecs = self.codecs_for_mid(mid);
307        if let Some(shared) = self.negotiated_codecs.get(&mid) {
308            *shared.lock().unwrap() = codecs;
309        }
310    }
311
312    fn apply_command(&mut self, cmd: Command, bus: &Bus) -> Result<()> {
313        match cmd {
314            Command::AddTrack(id, kind, direction, codec) => {
315                pp_info!(
316                    self,
317                    "add_track requested: id={id:?}, kind={kind:?}, direction={direction:?}, codec={codec:?}"
318                );
319                self.tracks_out
320                    .insert(id, TrackOutState::ToOpen(kind, direction));
321                self.track_codec.insert(id, codec);
322            }
323            Command::Push(id, codec, buf) => {
324                // A malformed media packet is local to this one track and
325                // buffer. Report and drop it without tearing down the
326                // entire live WebRTC connection, matching Queue's
327                // consume-error contract.
328                if let Err(error) = self.write_track(id, codec, buf) {
329                    bus.post(
330                        &self.pp_log,
331                        BusEvent::Error {
332                            element_type: ElementType::WebRtcPeer,
333                            name: self.name.clone(),
334                            error,
335                        },
336                    );
337                }
338            }
339            Command::SetAnswer(answer) => {
340                let Some((pending, ids)) = self.pending.take() else {
341                    return Ok(());
342                };
343                self.rtc
344                    .sdp_api()
345                    .accept_answer(pending, answer)
346                    .inspect_err(|error| pp_error!(self, "accept_answer failed: {error}"))
347                    .map_err(WebRtcError::from)?;
348                pp_info!(self, "renegotiation complete: {} track(s)", ids.len());
349                for id in ids {
350                    if let Some(state @ TrackOutState::Negotiating(_)) = self.tracks_out.get(&id) {
351                        let mid = state.mid().expect("Negotiating always carries a Mid");
352                        self.tracks_out.insert(id, TrackOutState::Open(mid));
353                        self.refresh_codecs(mid);
354                    }
355                }
356            }
357            Command::AcceptOffer(offer, reply) => {
358                let result = self
359                    .rtc
360                    .sdp_api()
361                    .accept_offer(offer)
362                    .inspect_err(|error| pp_error!(self, "accept_offer failed: {error}"))
363                    .map_err(WebRtcError::from);
364                if result.is_ok() {
365                    pp_info!(self, "accepted remote offer");
366                }
367                let _ = reply.send(result);
368            }
369        }
370        Ok(())
371    }
372
373    /// Starts a new SDP exchange if any track is waiting to be opened and
374    /// none is already in flight (str0m only allows one pending offer at a
375    /// time).
376    fn negotiate_if_needed(&mut self) {
377        if self.pending.is_some() {
378            return;
379        }
380        let to_open: Vec<TrackId> = self
381            .tracks_out
382            .iter()
383            .filter(|(_, s)| matches!(s, TrackOutState::ToOpen(..)))
384            .map(|(id, _)| *id)
385            .collect();
386        if to_open.is_empty() {
387            return;
388        }
389
390        let mut newly_negotiating = Vec::with_capacity(to_open.len());
391        let mut api = self.rtc.sdp_api();
392        for &id in &to_open {
393            let Some(TrackOutState::ToOpen(kind, direction)) = self.tracks_out.get(&id) else {
394                continue;
395            };
396            let (kind, direction) = (*kind, *direction);
397            let mid = api.add_media(kind, direction, None, None, None);
398            self.tracks_out.insert(id, TrackOutState::Negotiating(mid));
399            newly_negotiating.push((id, mid, kind, direction));
400        }
401
402        if let Some((offer, pending)) = api.apply() {
403            pp_info!(self, "renegotiation started: {} track(s)", to_open.len());
404            self.pending = Some((pending, to_open));
405            (self.on_offer)(offer);
406        }
407
408        // str0m never fires `Event::MediaAdded` for media *this side* just
409        // added (see the type docs) — so this is the only place these
410        // newly-minted `Mid`s ever reach `attach_track`, unlike the remote
411        // side's own `Event::MediaAdded` handling below.
412        for (id, mid, kind, direction) in newly_negotiating {
413            self.attach_track(id, mid, kind, direction);
414        }
415    }
416
417    fn write_track(&mut self, id: TrackId, codec: Option<Codec>, buf: MediaBuffer) -> Result<()> {
418        let Some(TrackOutState::Open(mid)) = self.tracks_out.get(&id) else {
419            // Not open yet (or unknown/never added) — dropped, see
420            // `WebRtcHandle::add_track`'s docs.
421            return Ok(());
422        };
423        let MediaBuffer::Packet(packet) = buf else {
424            return Ok(()); // Eos: nothing to write, nothing to flush
425        };
426        let Some(writer) = self.rtc.writer(*mid) else {
427            return Ok(());
428        };
429        let codec = codec.ok_or(WebRtcError::OutboundCodecNotDeclared(id))?;
430        // Never guess the first negotiated codec: str0m packetizes whatever
431        // bytes it receives under the selected payload type, so guessing VP8
432        // for an H.264 packet creates a valid-looking but mislabeled stream.
433        let mut negotiated = Vec::new();
434        let mut pt = None;
435        for params in writer.payload_params() {
436            let candidate = params.spec().codec;
437            if !negotiated.contains(&candidate) {
438                negotiated.push(candidate);
439            }
440            if candidate == codec && pt.is_none() {
441                pt = Some(params.pt());
442            }
443        }
444        let Some(pt) = pt else {
445            return Err(WebRtcError::OutboundCodecNotNegotiated {
446                track_id: id,
447                codec,
448                negotiated,
449            }
450            .into());
451        };
452        let data = packet.data().unwrap_or(&[]).to_vec();
453        let rtp_time = packet_rtp_time(&packet)?;
454        writer
455            .write(pt, Instant::now(), rtp_time, data)
456            .inspect_err(|error| pp_error!(self, "writer.write failed: {error}"))
457            .map_err(WebRtcError::from)?;
458        Ok(())
459    }
460
461    /// Drains every immediately-available str0m output (retransmits and
462    /// events), returning once str0m itself has nothing left to do until
463    /// the returned deadline.
464    fn drive_until_timeout(&mut self, bus: &Bus) -> Result<Instant> {
465        loop {
466            let output = self
467                .rtc
468                .poll_output()
469                .inspect_err(|error| pp_error!(self, "poll_output failed: {error}"))
470                .map_err(WebRtcError::from)?;
471            match output {
472                Output::Timeout(deadline) => return Ok(deadline),
473                Output::Transmit(t) => {
474                    // A single failed send (e.g. transient ICMP unreachable)
475                    // isn't fatal to the whole connection — str0m's own
476                    // retransmit/timeout logic handles loss.
477                    let _ = self.socket.send_to(&t.contents, t.destination);
478                }
479                Output::Event(event) => self.handle_event(event, bus),
480            }
481        }
482    }
483
484    /// Tells the remote peer this connection is over, instead of simply
485    /// going quiet.
486    ///
487    /// Without this a stopped peer is indistinguishable from a crashed or
488    /// unplugged one: nothing is sent, and the remote only finds out when
489    /// its own ICE checks time out — or, on a path that happens to return
490    /// ICMP port-unreachable, when a `recv_from` fails. Neither is a
491    /// contract; both are accidents of the network in between.
492    ///
493    /// `Rtc::close` queues a DTLS `close_notify` that only leaves via
494    /// `poll_output`, and str0m requires draining until it reports a
495    /// timeout, so this drives the loop one more time rather than
496    /// returning straight away. Best-effort throughout: this runs while
497    /// shutting down, so a send that fails has nowhere left to be
498    /// reported and nothing left to retry into.
499    fn close_connection(&mut self, bus: &Bus) {
500        if !self.rtc.is_alive() {
501            return; // already gone — nothing to notify, nothing to drain
502        }
503        if let Err(error) = self.rtc.close() {
504            pp_warn!(self, "close failed, ending without notifying: {error}");
505            return;
506        }
507        if let Err(error) = self.drive_until_timeout(bus) {
508            pp_warn!(self, "draining close_notify failed: {error}");
509            return;
510        }
511        pp_info!(self, "event=close phase=completed outcome=ok");
512    }
513
514    pub(super) fn handle_event(&mut self, event: Event, bus: &Bus) {
515        match event {
516            Event::MediaAdded(added) => {
517                // Only reached for media the *remote* peer added (see the
518                // type docs) — by definition already fully negotiated by
519                // the time we see this, so `Open` immediately: unlike a
520                // locally-requested track, there's no answer left to wait
521                // for before a `WebRtcTrackSink` bound to it can actually
522                // send.
523                let id = TrackId(self.next_id.fetch_add(1, Ordering::Relaxed));
524                self.tracks_out.insert(id, TrackOutState::Open(added.mid));
525                self.attach_track(id, added.mid, added.kind, added.direction);
526            }
527            Event::MediaData(data) => {
528                if let Some(track) = self.tracks_in.get_mut(&data.mid) {
529                    // Every packet, not just the first: cheap (one lock),
530                    // and correct if the remote side ever actually changes
531                    // codec mid-stream (rare, but the payload type is free
532                    // to vary packet-to-packet — see `WebRtcTrackSource::
533                    // codec`'s own docs for why this can't be pinned down
534                    // any earlier than "whatever the last packet said").
535                    let codec = data.params.spec();
536                    track.codec.lock().unwrap().replace(codec.codec);
537                    if !track.stream_info_sent
538                        && let Some(info) = track.stream_info_probe.observe(codec, &data.data)
539                    {
540                        // Signal before queueing the payload that completed
541                        // the information. Earlier payloads are already in
542                        // `data_tx`; all of them stay buffered while the caller
543                        // builds its downstream graph.
544                        track.stream_info_sent = track.stream_info_tx.try_send(info).is_ok();
545                    }
546
547                    let mut packet = ffmpeg::Packet::copy(&data.data);
548                    // `data.time` is str0m's own RTP timestamp (numerator)
549                    // over the codec's clock rate (denominator) — reused
550                    // as-is for pts/dts. No B-frame reordering happens over
551                    // RTP (decode order == transmit order), so pts and dts
552                    // are always the same value here.
553                    packet.set_time_base(ffmpeg::Rational::new(1, data.time.denom() as i32));
554                    let pts = data.time.numer() as i64;
555                    packet.set_pts(Some(pts));
556                    packet.set_dts(Some(pts));
557                    if data.is_keyframe() {
558                        let flags = packet.flags() | ffmpeg::codec::packet::Flags::KEY;
559                        packet.set_flags(flags);
560                    }
561                    match track
562                        .data_tx
563                        .try_send(MediaBuffer::Packet(Arc::new(packet)))
564                    {
565                        Ok(()) => {}
566                        Err(TrySendError::Full(_)) => {
567                            // This track's `WebRtcTrackSource` (or whatever
568                            // it feeds) isn't keeping up — drop the newest
569                            // buffer rather than let this grow unbounded
570                            // (see `CHANNEL_CAPACITY`'s docs).
571                            bus.post(
572                                &self.pp_log,
573                                BusEvent::Dropped {
574                                    element_type: ElementType::WebRtcPeer,
575                                    name: self.name.clone(),
576                                },
577                            );
578                        }
579                        Err(TrySendError::Disconnected(_)) => {
580                            // This track's `WebRtcTrackSource` is gone (its
581                            // own `Pipeline` finished) — stop trying to feed
582                            // it.
583                            self.tracks_in.remove(&data.mid);
584                        }
585                    }
586                }
587            }
588            Event::KeyframeRequest(req) => {
589                if let Some((&id, _)) = self
590                    .tracks_out
591                    .iter()
592                    .find(|(_, s)| s.mid() == Some(req.mid))
593                {
594                    pp_info!(self, "keyframe requested: id={id:?}, mid={}", req.mid);
595                    (self.on_keyframe_request)(id);
596                }
597            }
598            Event::Connected => {
599                pp_info!(self, "ICE+DTLS connected");
600            }
601            Event::IceConnectionStateChange(state) => {
602                pp_info!(self, "ICE connection state: {state:?}");
603            }
604            Event::MediaChanged(changed) => {
605                pp_info!(
606                    self,
607                    "media changed: mid={}, direction={:?}",
608                    changed.mid,
609                    changed.direction
610                );
611                self.refresh_codecs(changed.mid);
612                // A track's endpoints are minted once, from the direction it
613                // attached with, and `next_track` has already handed them to
614                // the caller — there is no way to hand out a half that did
615                // not exist then, or to take back one that no longer works.
616                // So a direction the remote peer actually changed makes what
617                // the caller holds wrong, and saying so is all this element
618                // can do about it. Recovering means tearing the track down
619                // and adding a new one.
620                //
621                // `MediaChanged` also fires when a renegotiation re-states
622                // the direction already in force; comparing keeps that from
623                // being reported as a change.
624                if let Some(&from) = self.track_direction.get(&changed.mid)
625                    && from != changed.direction
626                {
627                    self.track_direction.insert(changed.mid, changed.direction);
628                    bus.post(
629                        &self.pp_log,
630                        BusEvent::Error {
631                            element_type: ElementType::WebRtcPeer,
632                            name: self.name.clone(),
633                            error: WebRtcError::DirectionChanged {
634                                mid: changed.mid,
635                                from,
636                                to: changed.direction,
637                            }
638                            .into(),
639                        },
640                    );
641                }
642            }
643            Event::Closed => {
644                pp_info!(self, "event=close phase=remote_received outcome=ok");
645                self.remote_closed = true;
646            }
647            // `Event` is `#[non_exhaustive]` — data channels, stats, etc.
648            // are still outside this element's concern for now.
649            _ => {}
650        }
651    }
652}
653
654/// Converts a `Packet`'s `(pts, time_base)` into the `MediaTime` str0m
655/// expects for [`str0m::media::Writer::write`]. `MediaTime` is
656/// numer/denom *seconds* (str0m rebases it to the codec's RTP clock rate
657/// internally), but an FFmpeg `time_base` is numer/denom *seconds per
658/// tick* — so the elapsed time is `pts * numerator / denominator`, not
659/// `pts / denominator`. Most time bases in this codebase have numerator 1
660/// (e.g. `1/90_000`), which would hide a naive `pts / denominator`: an
661/// NTSC-style `1001/30_000` time base would make the RTP timestamp run
662/// ~1001x too fast.
663pub(super) fn packet_rtp_time(
664    packet: &ffmpeg::Packet,
665) -> std::result::Result<MediaTime, WebRtcError> {
666    let pts = packet.pts().ok_or(WebRtcError::MissingPacketPts)?;
667    let timestamp = MediaTimestamp::try_new(pts, packet.time_base()).map_err(
668        |InvalidTimeBase {
669             numerator,
670             denominator,
671         }| WebRtcError::InvalidPacketTimeBase {
672            numerator,
673            denominator,
674        },
675    )?;
676    to_str0m_media_time(timestamp)
677}
678
679/// Converts a validated `(pts, time_base)` into the `MediaTime` str0m
680/// expects for [`str0m::media::Writer::write`]. `MediaTime` is numer/denom
681/// *seconds* (str0m rebases it to the codec's RTP clock rate internally),
682/// but an FFmpeg `time_base` is numer/denom *seconds per tick* — so the
683/// elapsed time is `pts * numerator / denominator`, not `pts /
684/// denominator`. This keeps that exact `(pts * numerator, denominator)`
685/// rational rather than rescaling to some fixed target base first — a
686/// backend-specific conversion, so it lives here rather than on
687/// `MediaTimestamp` itself.
688fn to_str0m_media_time(timestamp: MediaTimestamp) -> std::result::Result<MediaTime, WebRtcError> {
689    let time_base = timestamp.time_base().get();
690    let numerator = time_base.numerator();
691    let denominator = time_base.denominator();
692    let pts = u64::try_from(timestamp.pts())
693        .map_err(|_| WebRtcError::NegativePacketPts(timestamp.pts()))?;
694    let frequency = str0m::media::Frequency::new(denominator as u32).ok_or(
695        WebRtcError::InvalidPacketTimeBase {
696            numerator,
697            denominator,
698        },
699    )?;
700    let numer = pts
701        .checked_mul(numerator as u64)
702        .ok_or(WebRtcError::PacketTimestampOverflow {
703            pts,
704            numerator,
705            denominator,
706        })?;
707    Ok(MediaTime::new(numer, frequency))
708}
709
710impl Element for WebRtcPeer {
711    fn name(&self) -> Arc<str> {
712        self.name.clone()
713    }
714
715    fn element_type(&self) -> ElementType {
716        ElementType::WebRtcPeer
717    }
718
719    fn pp_log(&self) -> &PpLog {
720        &self.pp_log
721    }
722
723    fn pp_log_mut(&mut self) -> &mut PpLog {
724        &mut self.pp_log
725    }
726}
727
728impl Driver for WebRtcPeer {
729    /// Drives str0m's poll loop. Every iteration: apply any commands from
730    /// `WebRtcHandle`/`WebRtcTrackSink`, start a renegotiation if a track
731    /// is waiting, drain str0m's own output (writing/dispatching as it
732    /// goes), check `stop`, then block on the UDP socket for at most
733    /// `POLL_INTERVAL` — capped below whatever str0m itself asked for, so
734    /// the command channel and `stop` are never starved for longer than
735    /// that even when nothing else is happening. There's no true
736    /// multi-way wait across the command channel, `stop`, *and* a raw
737    /// socket the way [`crate::elements::AppSource`] manages across two
738    /// `crossbeam_channel`s (a `UdpSocket` isn't `select!`-able), so this
739    /// is bounded polling instead — worst case `POLL_INTERVAL` of extra
740    /// latency for `Stop`/a fresh `add_track`, not unboundedly stuck.
741    ///
742    /// `stop`/the connection dying both clear `tracks_in` immediately, so
743    /// every already-handed-out `WebRtcTrackSource` sees its data channel
744    /// disconnect and ends with a final `Eos` right away, instead of
745    /// waiting for this whole `WebRtcPeer` to be dropped later by whatever
746    /// owns its `DriverRunner`. Neither `WebRtcPeer` nor its
747    /// `WebRtcTrackSource`s have a `Pause`/`Seek` concept — see
748    /// [`Driver`]'s own docs for why that's not just an oversight: freezing
749    /// this loop would starve ICE keepalives/DTLS retransmits, likely
750    /// dropping the connection rather than gracefully suspending it.
751    fn run(&mut self, stop: &StopReceiver, bus: &Bus) -> Result<()> {
752        pp_info!(self, "started");
753        let mut buf = vec![0u8; 2000];
754        loop {
755            while let Ok(cmd) = self.command_rx.try_recv() {
756                self.apply_command(cmd, bus)?;
757            }
758            self.negotiate_if_needed();
759
760            let deadline = self.drive_until_timeout(bus)?;
761            if self.remote_closed || !self.rtc.is_alive() || stop.is_stopped() {
762                pp_info!(
763                    self,
764                    "stopped rtc_alive={} remote_closed={}",
765                    self.rtc.is_alive(),
766                    self.remote_closed
767                );
768                if !self.remote_closed {
769                    self.close_connection(bus);
770                }
771                self.tracks_in.clear();
772                return Ok(());
773            }
774
775            let wait = deadline
776                .saturating_duration_since(Instant::now())
777                .min(POLL_INTERVAL)
778                .max(Duration::from_millis(1));
779            self.socket
780                .set_read_timeout(Some(wait))
781                .inspect_err(|error| pp_error!(self, "set_read_timeout failed: {error}"))
782                .map_err(WebRtcError::from)?;
783
784            match self.socket.recv_from(&mut buf) {
785                Ok((n, source)) => {
786                    let Ok(contents) = buf[..n].try_into() else {
787                        continue; // not a WebRTC datagram we recognize — ignore
788                    };
789                    let destination = self
790                        .socket
791                        .local_addr()
792                        .inspect_err(|error| pp_error!(self, "local_addr failed: {error}"))
793                        .map_err(WebRtcError::from)?;
794                    self.rtc
795                        .handle_input(Input::Receive(
796                            Instant::now(),
797                            Receive {
798                                proto: Protocol::Udp,
799                                source,
800                                destination,
801                                contents,
802                            },
803                        ))
804                        .inspect_err(|error| {
805                            pp_error!(self, "handle_input(Receive) failed: {error}")
806                        })
807                        .map_err(WebRtcError::from)?;
808                }
809                Err(e)
810                    if matches!(
811                        e.kind(),
812                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
813                    ) =>
814                {
815                    // Nothing arrived, but str0m still needs to be told
816                    // time has passed — its own internal clock only moves
817                    // forward via `Input::Timeout`, and *that* is what
818                    // makes the next `poll_output()` produce whatever's
819                    // next (retransmits, RTCP, the initial STUN checks,
820                    // ...). Skipping this on every timeout would leave
821                    // str0m stuck forever waiting for input that already
822                    // isn't coming.
823                    self.rtc
824                        .handle_input(Input::Timeout(Instant::now()))
825                        .inspect_err(|error| {
826                            pp_error!(self, "handle_input(Timeout) failed: {error}")
827                        })
828                        .map_err(WebRtcError::from)?;
829                }
830                Err(e) => {
831                    pp_error!(self, "recv_from failed: {e}");
832                    return Err(WebRtcError::from(e).into());
833                }
834            }
835        }
836    }
837}