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}